Skip to content

Commit

Permalink
Max properties
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish committed Mar 7, 2022
1 parent 61c79ef commit 96b9ce5
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 71 deletions.
14 changes: 7 additions & 7 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
* @returns A new event with more information.
*/
protected _prepareEvent(event: Event, scope?: Scope, hint?: EventHint): PromiseLike<Event | null> {
const { normalizeDepth = 3 } = this.getOptions();
const { normalizeDepth = 3, normalizeMaxProperties = 1_000 } = this.getOptions();
const prepared: Event = {
...event,
event_id: event.event_id || (hint && hint.event_id ? hint.event_id : uuid4()),
Expand Down Expand Up @@ -380,7 +380,7 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
evt.sdkProcessingMetadata = { ...evt.sdkProcessingMetadata, normalizeDepth: normalize(normalizeDepth) };
}
if (typeof normalizeDepth === 'number' && normalizeDepth > 0) {
return this._normalizeEvent(evt, normalizeDepth);
return this._normalizeEvent(evt, normalizeDepth, normalizeMaxProperties);
}
return evt;
});
Expand All @@ -396,7 +396,7 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
* @param event Event
* @returns Normalized event
*/
protected _normalizeEvent(event: Event | null, depth: number): Event | null {
protected _normalizeEvent(event: Event | null, depth: number, maxProperties: number): Event | null {
if (!event) {
return null;
}
Expand All @@ -407,18 +407,18 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
breadcrumbs: event.breadcrumbs.map(b => ({
...b,
...(b.data && {
data: normalize(b.data, depth),
data: normalize(b.data, depth, maxProperties),
}),
})),
}),
...(event.user && {
user: normalize(event.user, depth),
user: normalize(event.user, depth, maxProperties),
}),
...(event.contexts && {
contexts: normalize(event.contexts, depth),
contexts: normalize(event.contexts, depth, maxProperties),
}),
...(event.extra && {
extra: normalize(event.extra, depth),
extra: normalize(event.extra, depth, maxProperties),
}),
};
// event.contexts.trace stores information about a Transaction. Similarly,
Expand Down
11 changes: 11 additions & 0 deletions packages/types/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ export interface Options {
*/
normalizeDepth?: number;

/**
* Maximum number of properties or elements that the normalization algorithm will output.
* Used when normalizing an event before sending, on all of the listed attributes:
* - `breadcrumbs.data`
* - `user`
* - `contexts`
* - `extra`
* Defaults to `1000`
*/
normalizeMaxProperties?: number;

/**
* Controls how many milliseconds to wait before shutting down. The default is
* SDK-specific but typically around 2 seconds. Setting this too low can cause
Expand Down
128 changes: 64 additions & 64 deletions packages/utils/src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ExtendedError, WrappedFunction } from '@sentry/types';

import { htmlTreeAsString } from './browser';
import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive, isSyntheticEvent } from './is';
import { memoBuilder, MemoFunc } from './memo';
import { memoBuilder } from './memo';
import { getFunctionName } from './stacktrace';
import { truncate } from './string';

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

type UnknownMaybeToJson = unknown & { toJSON?: () => string };

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

// If we reach the maximum depth, serialize whatever is left
if (depth === 0) {
return serializeValue(value);
}
function walk(key: string, value: UnknownMaybeToJson, depth: number = +Infinity): unknown {
// If we reach the maximum depth, serialize whatever is left
if (depth === 0) {
return serializeValue(value);
}

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
// If value implements `toJSON` method, call it and return early
if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
return value.toJSON();
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
// If value implements `toJSON` method, call it and return early
if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
return value.toJSON();
}

// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
// pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
// all along), we're done.
const serializable = makeSerializable(value, key);
if (isPrimitive(serializable)) {
return serializable;
}
// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
// pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
// all along), we're done.
const serializable = makeSerializable(value, key);
if (isPrimitive(serializable)) {
return serializable;
}

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

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

// If we already walked that branch, bail out, as it's circular reference
if (memoize(value)) {
return '[Circular ~]';
}
// If we already walked that branch, bail out, as it's circular reference
if (memoize(value)) {
return '[Circular ~]';
}

// Walk all keys of the source
for (const innerKey in source) {
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
if (!Object.prototype.hasOwnProperty.call(source, innerKey)) {
continue;
let propertyCount = 0;
// Walk all keys of the source
for (const innerKey in source) {
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
if (!Object.prototype.hasOwnProperty.call(source, innerKey)) {
continue;
}

if (propertyCount >= maxProperties) {
acc[innerKey] = '[MaxProperties ~]';
break;
}

propertyCount += 1;

// Recursively walk through all the child nodes
const innerValue: UnknownMaybeToJson = source[innerKey];
acc[innerKey] = walk(innerKey, innerValue, depth - 1);
}
// Recursively walk through all the child nodes
const innerValue: any = source[innerKey];
acc[innerKey] = walk(innerKey, innerValue, depth - 1, memo);
}

// Once walked through all the branches, remove the parent from memo storage
unmemoize(value);
// Once walked through all the branches, remove the parent from memo storage
unmemoize(value);

// Return accumulated values
return acc;
}
// Return accumulated values
return acc;
}

/**
* normalize()
*
* - Creates a copy to prevent original input mutation
* - Skip non-enumerablers
* - Calls `toJSON` if implemented
* - Removes circular references
* - Translates non-serializeable values (undefined/NaN/Functions) to serializable format
* - Translates known global objects/Classes to a string representations
* - Takes care of Error objects serialization
* - Optionally limit depth of final output
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function normalize(input: any, depth?: number): any {
try {
// since we're at the outermost level, there is no key
return walk('', input, depth);
return walk('', input as UnknownMaybeToJson, depth);
} catch (_oO) {
return '**non-serializable**';
}
Expand Down
45 changes: 45 additions & 0 deletions packages/utils/test/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,51 @@ describe('normalize()', () => {
});
});

describe('can limit max properties', () => {
test('object', () => {
const obj = {
nope: 'here',
foo: {
one: 1,
two: 2,
three: 3,
four: 4,
five: 5,
six: 6,
seven: 7,
},
after: 'more',
};

expect(normalize(obj, 10, 5)).toEqual({
nope: 'here',
foo: {
one: 1,
two: 2,
three: 3,
four: 4,
five: 5,
six: '[MaxProperties ~]',
},
after: 'more',
});
});

test('array', () => {
const obj = {
nope: 'here',
foo: new Array(100).fill('s'),
after: 'more',
};

expect(normalize(obj, 10, 5)).toEqual({
nope: 'here',
foo: ['s', 's', 's', 's', 's', '[MaxProperties ~]'],
after: 'more',
});
});
});

test('normalizes value on every iteration of decycle and takes care of things like Reacts SyntheticEvents', () => {
const obj = {
foo: {
Expand Down

0 comments on commit 96b9ce5

Please sign in to comment.