Skip to content

Commit 14bc59b

Browse files
authored
fix: Prevent decycling from referencing original objects and pull original function name
* fix: Prevent decycling from referencing original objects * fix: preserve correct name when wrapping * test: Update raven-node tests for new node version
1 parent 5f3e0b0 commit 14bc59b

File tree

10 files changed

+93
-52
lines changed

10 files changed

+93
-52
lines changed

packages/browser/src/integrations/breadcrumbs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Breadcrumb, Integration, SentryBreadcrumbHint, Severity } from '@sentry
33
import { isFunction, isString } from '@sentry/utils/is';
44
import { logger } from '@sentry/utils/logger';
55
import { getEventDescription, getGlobalObject, parseUrl } from '@sentry/utils/misc';
6-
import { deserialize, fill, serializeObject } from '@sentry/utils/object';
6+
import { deserialize, fill, safeNormalize } from '@sentry/utils/object';
77
import { includes, safeJoin } from '@sentry/utils/string';
88
import { supportsBeacon, supportsHistory, supportsNativeFetch } from '@sentry/utils/supports';
99
import { BrowserClient } from '../client';
@@ -130,7 +130,7 @@ export class Breadcrumbs implements Integration {
130130
category: 'console',
131131
data: {
132132
extra: {
133-
arguments: serializeObject(args, 2),
133+
arguments: safeNormalize(args, 3),
134134
},
135135
logger: 'console',
136136
},
@@ -141,7 +141,7 @@ export class Breadcrumbs implements Integration {
141141
if (level === 'assert') {
142142
if (args[0] === false) {
143143
breadcrumbData.message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`;
144-
breadcrumbData.data.extra.arguments = serializeObject(args.slice(1), 2);
144+
breadcrumbData.data.extra.arguments = safeNormalize(args.slice(1), 3);
145145
}
146146
}
147147

packages/browser/src/integrations/helpers.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { captureException, getCurrentHub, withScope } from '@sentry/core';
22
import { Mechanism, SentryEvent, SentryWrappedFunction } from '@sentry/types';
33
import { isFunction } from '@sentry/utils/is';
44
import { htmlTreeAsString } from '@sentry/utils/misc';
5-
import { serializeObject } from '@sentry/utils/object';
5+
import { safeNormalize } from '@sentry/utils/object';
66

77
const debounceDuration: number = 1000;
88
let keypressTimeout: number | undefined;
@@ -90,7 +90,7 @@ export function wrap(
9090

9191
processedEvent.extra = {
9292
...processedEvent.extra,
93-
arguments: serializeObject(args, 2),
93+
arguments: safeNormalize(args, 3),
9494
};
9595

9696
return processedEvent;
@@ -134,6 +134,17 @@ export function wrap(
134134
},
135135
});
136136

137+
// Restore original function name (not all browsers allow that)
138+
try {
139+
Object.defineProperty(sentryWrapped, 'name', {
140+
get(): string {
141+
return fn.name;
142+
},
143+
});
144+
} catch (_oO) {
145+
/*no-empty*/
146+
}
147+
137148
return sentryWrapped;
138149
}
139150

packages/browser/test/integration/console-logs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
console.log('One');
22
console.warn('Two', { a: 1 });
3-
console.error('Error 2', { b: { c: 1 } });
3+
console.error('Error 2', { b: { c: [] } });
44
function a() {
55
throw new Error('Error thrown 3');
66
}

packages/browser/test/integration/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1663,7 +1663,7 @@ for (var idx in frames) {
16631663
assert.lengthOf(sentryData.breadcrumbs, 3);
16641664
assert.deepEqual(sentryData.breadcrumbs[0].data.extra.arguments, ['One']);
16651665
assert.deepEqual(sentryData.breadcrumbs[1].data.extra.arguments, ['Two', { a: 1 }]);
1666-
assert.deepEqual(sentryData.breadcrumbs[2].data.extra.arguments, ['Error 2', { b: '[Object]' }]);
1666+
assert.deepEqual(sentryData.breadcrumbs[2].data.extra.arguments, ['Error 2', { b: { c: '[Array]' } }]);
16671667
done();
16681668
}
16691669
}

packages/browser/test/integrations/helpers.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ describe('wrap()', () => {
2222
expect(wrap(num)).equal(num);
2323
});
2424

25+
it('should preserve correct function name when accessed', () => {
26+
const namedFunction = () => 1337;
27+
expect(wrap(namedFunction)).not.equal(namedFunction);
28+
expect(namedFunction.name).equal('namedFunction');
29+
expect(wrap(namedFunction).name).equal('namedFunction');
30+
});
31+
2532
it('bail out with the original if accessing custom props go bad', () => {
2633
const fn = (() => 1337) as SentryWrappedFunction;
2734
fn.__sentry__ = false;

packages/core/src/integrations/extraerrordata.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ interface ExtendedError extends Error {
1111
[key: string]: unknown;
1212
}
1313

14+
/** JSDoc */
15+
interface ExtraErrorDataOptions {
16+
depth?: number;
17+
}
18+
1419
/** Patch toString calls to return proper name for wrapped functions */
1520
export class ExtraErrorData implements Integration {
1621
/**
@@ -23,6 +28,11 @@ export class ExtraErrorData implements Integration {
2328
*/
2429
public static id: string = 'ExtraErrorData';
2530

31+
/**
32+
* @inheritDoc
33+
*/
34+
public constructor(private readonly options: ExtraErrorDataOptions = { depth: 3 }) {}
35+
2636
/**
2737
* @inheritDoc
2838
*/
@@ -50,7 +60,7 @@ export class ExtraErrorData implements Integration {
5060
let extra = {
5161
...event.extra,
5262
};
53-
const normalizedErrorData = safeNormalize(errorData);
63+
const normalizedErrorData = safeNormalize(errorData, this.options.depth);
5464
if (!isString(normalizedErrorData)) {
5565
extra = {
5666
...event.extra,

packages/raven-node/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
{
22
"name": "raven",
33
"description": "A standalone (Node.js) client for Sentry",
4-
"keywords": ["debugging", "errors", "exceptions", "logging", "raven", "sentry"],
4+
"keywords": [
5+
"debugging",
6+
"errors",
7+
"exceptions",
8+
"logging",
9+
"raven",
10+
"sentry"
11+
],
512
"version": "2.6.4",
613
"repository": "git://github.com/getsentry/raven-js.git",
714
"license": "BSD-2-Clause",
@@ -13,8 +20,7 @@
1320
},
1421
"scripts": {
1522
"lint": "eslint .",
16-
"test":
17-
"NODE_ENV=test istanbul cover _mocha -- --reporter dot && NODE_ENV=test coffee ./test/run.coffee",
23+
"test": "NODE_ENV=test istanbul cover _mocha -- --reporter dot && NODE_ENV=test coffee ./test/run.coffee",
1824
"test-mocha": "NODE_ENV=test mocha",
1925
"test-full": "npm run test && cd test/instrumentation && ./run.sh"
2026
},

packages/raven-node/test/raven.client.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1411,7 +1411,12 @@ describe('raven.Client', function() {
14111411
var y = Object.create(null);
14121412
y.c = 'd';
14131413
console.log(x, y);
1414-
client.getContext().breadcrumbs[0].message.should.equal("{ a: 'b' } { c: 'd' }");
1414+
client
1415+
.getContext()
1416+
.breadcrumbs[0].message.should.be.oneOf(
1417+
"{ a: 'b' } { c: 'd' }",
1418+
"[Object: null prototype] { a: 'b' } [Object: null prototype] { c: 'd' }"
1419+
);
14151420
done();
14161421
});
14171422
});

packages/raven-node/test/raven.utils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,11 @@ describe('raven.utils', function() {
238238
var callback = function(frames) {
239239
var frame1 = frames.pop();
240240
frame1.in_app.should.be.false;
241-
frame1.filename.should.equal('querystring.js');
241+
frame1.filename.should.be.oneOf('querystring.js', 'internal/querystring.js');
242242

243243
var frame2 = frames.pop();
244244
frame2.in_app.should.be.false;
245-
frame2.filename.should.equal('querystring.js');
245+
frame2.filename.should.be.oneOf('querystring.js', 'internal/querystring.js');
246246

247247
done();
248248
};

packages/utils/src/object.ts

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SentryWrappedFunction } from '@sentry/types';
2-
import { isArray, isNaN, isPlainObject, isPrimitive, isSyntheticEvent, isUndefined } from './is';
2+
import { isArray, isError, isNaN, isPlainObject, isPrimitive, isSyntheticEvent, isUndefined } from './is';
33
import { Memo } from './memo';
44
import { truncate } from './string';
55

@@ -120,19 +120,19 @@ function jsonSize(value: any): number {
120120
}
121121

122122
/** JSDoc */
123-
function serializeValue<T>(value: T): T | string {
123+
function serializeValue(value: any): string {
124124
const type = Object.prototype.toString.call(value);
125125

126+
// Node.js REPL notation
126127
if (typeof value === 'string') {
127128
return truncate(value, 40);
128129
} else if (type === '[object Object]') {
129-
// Node.js REPL notation
130130
return '[Object]';
131131
} else if (type === '[object Array]') {
132-
// Node.js REPL notation
133132
return '[Array]';
134133
} else {
135-
return normalizeValue(value) as T;
134+
const normalized = normalizeValue(value);
135+
return isPrimitive(normalized) ? `${normalized}` : (type as string);
136136
}
137137
}
138138

@@ -270,31 +270,27 @@ function objectifyError(error: ExtendedError): object {
270270
* - serializes Error objects
271271
* - filter global objects
272272
*/
273-
function normalizeValue(value: any, key?: any): any {
274-
if (key === 'domain' && typeof value === 'object' && (value as { _events: any })._events) {
273+
function normalizeValue<T>(value: T, key?: any): T | string {
274+
if (key === 'domain' && typeof value === 'object' && ((value as unknown) as { _events: any })._events) {
275275
return '[Domain]';
276276
}
277277

278278
if (key === 'domainEmitter') {
279279
return '[DomainEmitter]';
280280
}
281281

282-
if (typeof (global as any) !== 'undefined' && value === global) {
282+
if (typeof (global as any) !== 'undefined' && (value as unknown) === global) {
283283
return '[Global]';
284284
}
285285

286-
if (typeof (window as any) !== 'undefined' && value === window) {
286+
if (typeof (window as any) !== 'undefined' && (value as unknown) === window) {
287287
return '[Window]';
288288
}
289289

290-
if (typeof (document as any) !== 'undefined' && value === document) {
290+
if (typeof (document as any) !== 'undefined' && (value as unknown) === document) {
291291
return '[Document]';
292292
}
293293

294-
if (value instanceof Error) {
295-
return objectifyError(value);
296-
}
297-
298294
// tslint:disable-next-line:strict-type-predicates
299295
if (typeof Event !== 'undefined' && value instanceof Event) {
300296
return Object.getPrototypeOf(value) ? value.constructor.name : 'Event';
@@ -314,7 +310,7 @@ function normalizeValue(value: any, key?: any): any {
314310
}
315311

316312
if (typeof value === 'function') {
317-
return `[Function: ${(value as () => void).name || '<unknown-function-name>'}]`;
313+
return `[Function: ${value.name || '<unknown-function-name>'}]`;
318314
}
319315

320316
return value;
@@ -326,31 +322,34 @@ function normalizeValue(value: any, key?: any): any {
326322
* @param obj Object to be decycled
327323
* @param memo Optional Memo class handling decycling
328324
*/
329-
export function decycle(obj: any, memo: Memo = new Memo()): any {
330-
// tslint:disable-next-line:no-unsafe-any
331-
const copy = isArray(obj) ? obj.slice() : isPlainObject(obj) ? assign({}, obj) : obj;
332-
const normalized = normalizeValue(obj);
325+
export function decycle(obj: any, depth: number = +Infinity, memo: Memo = new Memo()): any {
326+
if (depth === 0) {
327+
return serializeValue(obj);
328+
}
333329

334330
// If an object was normalized to its string form, we should just bail out as theres no point in going down that branch
335-
if (typeof normalized === 'string') {
331+
const normalized = normalizeValue(obj);
332+
if (isPrimitive(normalized)) {
336333
return normalized;
337334
}
338335

339-
if (!isPrimitive(obj)) {
340-
if (memo.memoize(obj)) {
341-
return '[Circular ~]';
342-
}
343-
// tslint:disable-next-line
344-
for (const key in obj) {
345-
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
346-
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
347-
continue;
348-
}
349-
// tslint:disable-next-line
350-
copy[key] = decycle(obj[key], memo);
336+
// tslint:disable-next-line:no-unsafe-any
337+
const source = (isError(obj) ? objectifyError(obj) : obj) as {
338+
[key: string]: any;
339+
};
340+
const copy = isArray(obj) ? [] : {};
341+
342+
if (memo.memoize(obj)) {
343+
return '[Circular ~]';
344+
}
345+
for (const key in source) {
346+
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
347+
if (!Object.prototype.hasOwnProperty.call(source, key)) {
348+
continue;
351349
}
352-
memo.unmemoize(obj);
350+
(copy as { [key: string]: any })[key] = decycle(source[key], depth - 1, memo);
353351
}
352+
memo.unmemoize(obj);
354353

355354
return copy;
356355
}
@@ -362,19 +361,22 @@ export function decycle(obj: any, memo: Memo = new Memo()): any {
362361
* translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
363362
* and takes care of Error objects serialization
364363
*/
365-
function serializer(options: { normalize: boolean } = { normalize: true }): (key: string, value: any) => any {
366-
// tslint:disable-next-line
367-
return (key: string, value: object) => (options.normalize ? normalizeValue(decycle(value), key) : decycle(value));
364+
function serializer(
365+
options: { normalize?: boolean; depth?: number } = { normalize: true },
366+
): (key: string, value: any) => any {
367+
return (key: string, value: object) =>
368+
// tslint:disable-next-line
369+
options.normalize ? normalizeValue(decycle(value, options.depth), key) : decycle(value, options.depth);
368370
}
369371

370372
/**
371373
* safeNormalize()
372374
*
373375
* Creates a copy of the input by applying serializer function on it and parsing it back to unify the data
374376
*/
375-
export function safeNormalize(input: any): any {
377+
export function safeNormalize(input: any, depth?: number): any {
376378
try {
377-
return JSON.parse(JSON.stringify(input, serializer({ normalize: true })));
379+
return JSON.parse(JSON.stringify(input, serializer({ normalize: true, depth })));
378380
} catch (_oO) {
379381
return '**non-serializable**';
380382
}

0 commit comments

Comments
 (0)