Skip to content

Commit

Permalink
fix(react-native): reinstate the make-safe module to allow recursiv…
Browse files Browse the repository at this point in the history
…e meta-data in React Native applications

This reverts commit 99a0cb4
  • Loading branch information
lemnik committed Jan 27, 2022
1 parent c84c923 commit 69bdd2a
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 21 deletions.
82 changes: 82 additions & 0 deletions packages/core/lib/derecursify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const isArray = require('./es-utils/is-array')

const isSafeLiteral = (obj) => (
typeof obj === 'string' || obj instanceof String ||
typeof obj === 'number' || obj instanceof Number ||
typeof obj === 'boolean' || obj instanceof Boolean
)

const isError = o => (
o instanceof Error || /^\[object (Error|(Dom)?Exception)]$/.test(Object.prototype.toString.call(o))
)

const throwsMessage = err => '[Throws: ' + (err ? err.message : '?') + ']'

const safelyGetProp = (obj, propName) => {
try {
return obj[propName]
} catch (err) {
return throwsMessage(err)
}
}

/**
* Similar to `safe-json-stringify` this function rebuilds an object graph without any circular references.
* This requirement is different to `JSON.parse(safeJsonStringify(data))` in three key ways:
* - `toJSON` methods are not called
* - there is no redaction or fixed depth limit
*
* @param data the value to be made safe for the ReactNative bridge
* @returns a safe version of the given `data`
*/
module.exports = function (data) {
const seen = []

const visit = (obj) => {
if (obj === null || obj === undefined) return obj

if (isSafeLiteral(obj)) {
return obj
}

if (isError(obj)) {
return visit({ name: obj.name, message: obj.message })
}

if (obj instanceof Date) {
return obj.toISOString()
}

if (seen.includes(obj)) {
// circular references are replaced and marked
return '[Circular]'
}

// handle arrays, and all iterable non-array types (such as Set)
if (isArray(obj) || obj[Symbol.iterator]) {
seen.push(obj)
const safeArray = []
try {
for (const value of obj) {
safeArray.push(visit(value))
}
} catch (err) {
// if retrieving the Iterator fails
return throwsMessage(err)
}
seen.pop()
return safeArray
}

seen.push(obj)
const safeObj = {}
for (const propName in obj) {
safeObj[propName] = visit(safelyGetProp(obj, propName))
}
seen.pop()

return safeObj
}

return visit(data)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import makeSafe from '../make-safe'
import derecursift from '../derecursify'

describe('delivery: react native makeSafe', () => {
it('leaves simple types intact', () => {
Expand All @@ -25,7 +25,7 @@ describe('delivery: react native makeSafe', () => {
_undefined: undefined
}

const result = makeSafe(data)
const result = derecursift(data)

/* eslint-disable-next-line @typescript-eslint/no-dynamic-delete */
delete data[symbol] // we don't copy Symbol keys over
Expand All @@ -50,13 +50,13 @@ describe('delivery: react native makeSafe', () => {
enumerable: true
})

const result = makeSafe(object)
const result = derecursift(object)
expect(result).toStrictEqual({ badProperty: '[Throws: failure]' })
})

it('when they are properties', () => {
const value = { errorProp: new Error('something wrong') }
const result = makeSafe(value)
const result = derecursift(value)
expect(result).toStrictEqual({ errorProp: { name: 'Error', message: 'something wrong' } })
})
})
Expand All @@ -66,7 +66,7 @@ describe('delivery: react native makeSafe', () => {
const object: { self?: any } = {}
object.self = object

const result = makeSafe(object)
const result = derecursift(object)
expect(result).toStrictEqual({ self: '[Circular]' })
})

Expand All @@ -77,15 +77,15 @@ describe('delivery: react native makeSafe', () => {

outer.inner.parent = outer

const result = makeSafe(outer)
const result = derecursift(outer)
expect(result).toStrictEqual({ inner: { parent: '[Circular]' } })
})

it('when in arrays', () => {
const array: any[] = [{}, {}]
array[0].circularRef = array

const result = makeSafe(array)
const result = derecursift(array)
expect(result).toStrictEqual([{ circularRef: '[Circular]' }, {}])
})

Expand All @@ -96,7 +96,7 @@ describe('delivery: react native makeSafe', () => {

object.container = values

const result = makeSafe(values)
const result = derecursift(values)
expect(result).toStrictEqual([{ container: '[Circular]' }])
})

Expand All @@ -112,7 +112,7 @@ describe('delivery: react native makeSafe', () => {
someObject: metaData
}]

const result = makeSafe(array)
const result = derecursift(array)
expect(result).toStrictEqual([
{
someObject: {
Expand Down
6 changes: 4 additions & 2 deletions packages/delivery-react-native/delivery.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const derecursify = require('@bugsnag/core/lib/derecursify')

module.exports = (client, NativeClient) => ({
sendEvent: (payload, cb = () => {}) => {
const event = payload.events[0]
Expand All @@ -17,10 +19,10 @@ module.exports = (client, NativeClient) => ({
app: event.app,
device: event.device,
threads: event.threads,
breadcrumbs: event.breadcrumbs,
breadcrumbs: derecursify(event.breadcrumbs),
context: event.context,
user: event._user,
metadata: event._metadata,
metadata: derecursify(event._metadata),
groupingHash: event.groupingHash,
apiKey: event.apiKey,
nativeStack: nativeStack
Expand Down
8 changes: 4 additions & 4 deletions packages/plugin-react-native-client-sync/client-sync.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { DeviceEventEmitter, NativeEventEmitter, NativeModules, Platform } = require('react-native')
const makeSafe = require('@bugsnag/delivery-react-native/make-safe')
const derecursify = require('@bugsnag/core/lib/derecursify')

module.exports = (NativeClient) => ({
load: (client) => {
Expand All @@ -8,7 +8,7 @@ module.exports = (NativeClient) => ({
// to JSON() method doesn't get called before passing the object over the
// bridge. This happens in the remote debugger and means the "message"
// property is incorrectly named "name"
NativeClient.leaveBreadcrumb(makeSafe(breadcrumb))
NativeClient.leaveBreadcrumb(derecursify(breadcrumb))
}, true)

const origSetUser = client.setUser
Expand All @@ -29,9 +29,9 @@ module.exports = (NativeClient) => ({
client.addMetadata = function (section, key, value) {
const ret = origAddMetadata.apply(this, arguments)
if (typeof key === 'object') {
NativeClient.addMetadata(section, key)
NativeClient.addMetadata(section, derecursify(key))
} else {
NativeClient.addMetadata(section, { [key]: makeSafe(value) })
NativeClient.addMetadata(section, { [key]: derecursify(value) })
}
return ret
}
Expand Down
6 changes: 2 additions & 4 deletions packages/plugin-react-native-client-sync/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@
"author": "Bugsnag",
"license": "MIT",
"devDependencies": {
"@bugsnag/core": "^7.15.1",
"@bugsnag/delivery-react-native": "^7.15.1"
"@bugsnag/core": "^7.15.1"
},
"peerDependencies": {
"@bugsnag/core": "^7.0.0",
"@bugsnag/delivery-react-native": "^7.0.0"
"@bugsnag/core": "^7.0.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export class BreadcrumbsJsManualScenario extends Scenario {
const metaData = {
from: 'javascript'
}

// ensure that circular references are safely handled
metaData.circle = metaData

Bugsnag.leaveBreadcrumb('oh crumbs', metaData, 'state')
Bugsnag.notify(new Error('BreadcrumbsJsManualScenario'))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ export class MetadataJsScenario extends Scenario {
}

run () {
const recursiveMetadata = {}
recursiveMetadata.data = 'some valid data'
recursiveMetadata.circle = recursiveMetadata

Bugsnag.addMetadata('jsdata', 'some_more_data', 'set via client')
Bugsnag.addMetadata('jsdata', 'redacted_data', 'not present')
Bugsnag.addMetadata('jsdata', 'recursive', recursiveMetadata)
Bugsnag.notify(new Error('MetadataJsScenario'), (event) => {
event.addMetadata('jsdata', 'even_more_data', 'set via event')
event.addMetadata('jsarraydata', 'items', ['a', 'b', 'c'])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"name": "oh crumbs",
"timestamp": "^\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}:\\d{2}:[\\d\\.]+Z?$",
"metaData": {
"from": "javascript"
"from": "javascript",
"circle": "[Circular]"
}
}
}
2 changes: 2 additions & 0 deletions test/react-native/features/metadata.feature
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Scenario: Setting metadata (JS)
And the event "metaData.jsdata.some_more_data" equals "set via client"
And the event "metaData.jsdata.even_more_data" equals "set via event"
And the event "metaData.jsdata.redacted_data" equals "[REDACTED]"
And the event "metaData.jsdata.recursive.data" equals "some valid data"
And the event "metaData.jsdata.recursive.circle" equals "[Circular]"
And the error payload field "events.0.metaData.jsarraydata.items" is an array with 3 elements

Scenario: Setting metadata (native handled)
Expand Down

0 comments on commit 69bdd2a

Please sign in to comment.