Skip to content

Commit 96b9ce5

Browse files
committed
Max properties
1 parent 61c79ef commit 96b9ce5

File tree

4 files changed

+127
-71
lines changed

4 files changed

+127
-71
lines changed

packages/core/src/baseclient.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
346346
* @returns A new event with more information.
347347
*/
348348
protected _prepareEvent(event: Event, scope?: Scope, hint?: EventHint): PromiseLike<Event | null> {
349-
const { normalizeDepth = 3 } = this.getOptions();
349+
const { normalizeDepth = 3, normalizeMaxProperties = 1_000 } = this.getOptions();
350350
const prepared: Event = {
351351
...event,
352352
event_id: event.event_id || (hint && hint.event_id ? hint.event_id : uuid4()),
@@ -380,7 +380,7 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
380380
evt.sdkProcessingMetadata = { ...evt.sdkProcessingMetadata, normalizeDepth: normalize(normalizeDepth) };
381381
}
382382
if (typeof normalizeDepth === 'number' && normalizeDepth > 0) {
383-
return this._normalizeEvent(evt, normalizeDepth);
383+
return this._normalizeEvent(evt, normalizeDepth, normalizeMaxProperties);
384384
}
385385
return evt;
386386
});
@@ -396,7 +396,7 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
396396
* @param event Event
397397
* @returns Normalized event
398398
*/
399-
protected _normalizeEvent(event: Event | null, depth: number): Event | null {
399+
protected _normalizeEvent(event: Event | null, depth: number, maxProperties: number): Event | null {
400400
if (!event) {
401401
return null;
402402
}
@@ -407,18 +407,18 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
407407
breadcrumbs: event.breadcrumbs.map(b => ({
408408
...b,
409409
...(b.data && {
410-
data: normalize(b.data, depth),
410+
data: normalize(b.data, depth, maxProperties),
411411
}),
412412
})),
413413
}),
414414
...(event.user && {
415-
user: normalize(event.user, depth),
415+
user: normalize(event.user, depth, maxProperties),
416416
}),
417417
...(event.contexts && {
418-
contexts: normalize(event.contexts, depth),
418+
contexts: normalize(event.contexts, depth, maxProperties),
419419
}),
420420
...(event.extra && {
421-
extra: normalize(event.extra, depth),
421+
extra: normalize(event.extra, depth, maxProperties),
422422
}),
423423
};
424424
// event.contexts.trace stores information about a Transaction. Similarly,

packages/types/src/options.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ export interface Options {
101101
*/
102102
normalizeDepth?: number;
103103

104+
/**
105+
* Maximum number of properties or elements that the normalization algorithm will output.
106+
* Used when normalizing an event before sending, on all of the listed attributes:
107+
* - `breadcrumbs.data`
108+
* - `user`
109+
* - `contexts`
110+
* - `extra`
111+
* Defaults to `1000`
112+
*/
113+
normalizeMaxProperties?: number;
114+
104115
/**
105116
* Controls how many milliseconds to wait before shutting down. The default is
106117
* SDK-specific but typically around 2 seconds. Setting this too low can cause

packages/utils/src/object.ts

Lines changed: 64 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ExtendedError, WrappedFunction } from '@sentry/types';
44

55
import { htmlTreeAsString } from './browser';
66
import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive, isSyntheticEvent } from './is';
7-
import { memoBuilder, MemoFunc } from './memo';
7+
import { memoBuilder } from './memo';
88
import { getFunctionName } from './stacktrace';
99
import { truncate } from './string';
1010

@@ -300,85 +300,85 @@ function makeSerializable<T>(value: T, key?: any): T | string {
300300
return value;
301301
}
302302

303+
type UnknownMaybeToJson = unknown & { toJSON?: () => string };
304+
303305
/**
304-
* Walks an object to perform a normalization on it
306+
* normalize()
305307
*
306-
* @param key of object that's walked in current iteration
307-
* @param value object to be walked
308-
* @param depth Optional number indicating how deep should walking be performed
309-
* @param memo Optional Memo class handling decycling
308+
* - Creates a copy to prevent original input mutation
309+
* - Skip non-enumerable
310+
* - Calls `toJSON` if implemented
311+
* - Removes circular references
312+
* - Translates non-serializable values (undefined/NaN/Functions) to serializable format
313+
* - Translates known global objects/Classes to a string representations
314+
* - Takes care of Error objects serialization
315+
* - Optionally limit depth of final output
316+
* - Optionally limit max number of properties/elements for each object/array
310317
*/
311-
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
312-
export function walk(key: string, value: any, depth: number = +Infinity, memo: MemoFunc = memoBuilder()): any {
313-
const [memoize, unmemoize] = memo;
318+
export function normalize(input: unknown, depth: number = +Infinity, maxProperties: number = +Infinity): any {
319+
const [memoize, unmemoize] = memoBuilder();
314320

315-
// If we reach the maximum depth, serialize whatever is left
316-
if (depth === 0) {
317-
return serializeValue(value);
318-
}
321+
function walk(key: string, value: UnknownMaybeToJson, depth: number = +Infinity): unknown {
322+
// If we reach the maximum depth, serialize whatever is left
323+
if (depth === 0) {
324+
return serializeValue(value);
325+
}
319326

320-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
321-
// If value implements `toJSON` method, call it and return early
322-
if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
323-
return value.toJSON();
324-
}
325-
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
327+
// If value implements `toJSON` method, call it and return early
328+
if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
329+
return value.toJSON();
330+
}
326331

327-
// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
328-
// pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
329-
// all along), we're done.
330-
const serializable = makeSerializable(value, key);
331-
if (isPrimitive(serializable)) {
332-
return serializable;
333-
}
332+
// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
333+
// pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
334+
// all along), we're done.
335+
const serializable = makeSerializable(value, key);
336+
if (isPrimitive(serializable)) {
337+
return serializable;
338+
}
334339

335-
// Create source that we will use for the next iteration. It will either be an objectified error object (`Error` type
336-
// with extracted key:value pairs) or the input itself.
337-
const source = getWalkSource(value);
340+
// Create source that we will use for the next iteration. It will either be an objectified error object (`Error` type
341+
// with extracted key:value pairs) or the input itself.
342+
const source = getWalkSource(value);
338343

339-
// Create an accumulator that will act as a parent for all future itterations of that branch
340-
const acc: { [key: string]: any } = Array.isArray(value) ? [] : {};
344+
// Create an accumulator that will act as a parent for all future itterations of that branch
345+
const acc: { [key: string]: any } = Array.isArray(value) ? [] : {};
341346

342-
// If we already walked that branch, bail out, as it's circular reference
343-
if (memoize(value)) {
344-
return '[Circular ~]';
345-
}
347+
// If we already walked that branch, bail out, as it's circular reference
348+
if (memoize(value)) {
349+
return '[Circular ~]';
350+
}
346351

347-
// Walk all keys of the source
348-
for (const innerKey in source) {
349-
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
350-
if (!Object.prototype.hasOwnProperty.call(source, innerKey)) {
351-
continue;
352+
let propertyCount = 0;
353+
// Walk all keys of the source
354+
for (const innerKey in source) {
355+
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
356+
if (!Object.prototype.hasOwnProperty.call(source, innerKey)) {
357+
continue;
358+
}
359+
360+
if (propertyCount >= maxProperties) {
361+
acc[innerKey] = '[MaxProperties ~]';
362+
break;
363+
}
364+
365+
propertyCount += 1;
366+
367+
// Recursively walk through all the child nodes
368+
const innerValue: UnknownMaybeToJson = source[innerKey];
369+
acc[innerKey] = walk(innerKey, innerValue, depth - 1);
352370
}
353-
// Recursively walk through all the child nodes
354-
const innerValue: any = source[innerKey];
355-
acc[innerKey] = walk(innerKey, innerValue, depth - 1, memo);
356-
}
357371

358-
// Once walked through all the branches, remove the parent from memo storage
359-
unmemoize(value);
372+
// Once walked through all the branches, remove the parent from memo storage
373+
unmemoize(value);
360374

361-
// Return accumulated values
362-
return acc;
363-
}
375+
// Return accumulated values
376+
return acc;
377+
}
364378

365-
/**
366-
* normalize()
367-
*
368-
* - Creates a copy to prevent original input mutation
369-
* - Skip non-enumerablers
370-
* - Calls `toJSON` if implemented
371-
* - Removes circular references
372-
* - Translates non-serializeable values (undefined/NaN/Functions) to serializable format
373-
* - Translates known global objects/Classes to a string representations
374-
* - Takes care of Error objects serialization
375-
* - Optionally limit depth of final output
376-
*/
377-
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
378-
export function normalize(input: any, depth?: number): any {
379379
try {
380380
// since we're at the outermost level, there is no key
381-
return walk('', input, depth);
381+
return walk('', input as UnknownMaybeToJson, depth);
382382
} catch (_oO) {
383383
return '**non-serializable**';
384384
}

packages/utils/test/object.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,51 @@ describe('normalize()', () => {
533533
});
534534
});
535535

536+
describe('can limit max properties', () => {
537+
test('object', () => {
538+
const obj = {
539+
nope: 'here',
540+
foo: {
541+
one: 1,
542+
two: 2,
543+
three: 3,
544+
four: 4,
545+
five: 5,
546+
six: 6,
547+
seven: 7,
548+
},
549+
after: 'more',
550+
};
551+
552+
expect(normalize(obj, 10, 5)).toEqual({
553+
nope: 'here',
554+
foo: {
555+
one: 1,
556+
two: 2,
557+
three: 3,
558+
four: 4,
559+
five: 5,
560+
six: '[MaxProperties ~]',
561+
},
562+
after: 'more',
563+
});
564+
});
565+
566+
test('array', () => {
567+
const obj = {
568+
nope: 'here',
569+
foo: new Array(100).fill('s'),
570+
after: 'more',
571+
};
572+
573+
expect(normalize(obj, 10, 5)).toEqual({
574+
nope: 'here',
575+
foo: ['s', 's', 's', 's', 's', '[MaxProperties ~]'],
576+
after: 'more',
577+
});
578+
});
579+
});
580+
536581
test('normalizes value on every iteration of decycle and takes care of things like Reacts SyntheticEvents', () => {
537582
const obj = {
538583
foo: {

0 commit comments

Comments
 (0)