Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export {
} from './utils/misc';
export { isNodeEnv, loadModule } from './utils/node';
export { normalize, normalizeToSize, normalizeUrlToBase } from './utils/normalize';
export { setNormalizationDepthOverrideHint, setSkipNormalizationHint } from './utils/normalizationHints';
export {
addNonEnumerableProperty,
convertToPlainObject,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/integrations/extraerrordata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { IntegrationFn } from '../types-hoist/integration';
import { debug } from '../utils/debug-logger';
import { isError, isPlainObject } from '../utils/is';
import { normalize } from '../utils/normalize';
import { addNonEnumerableProperty } from '../utils/object';
import { setSkipNormalizationHint } from '../utils/normalizationHints';
import { truncate } from '../utils/string';

const INTEGRATION_NAME = 'ExtraErrorData';
Expand Down Expand Up @@ -66,7 +66,7 @@ function _enhanceEventWithErrorData(
if (isPlainObject(normalizedErrorData)) {
// We mark the error data as "already normalized" here, because we don't want other normalization procedures to
// potentially truncate the data we just already normalized, with a certain depth setting.
addNonEnumerableProperty(normalizedErrorData, '__sentry_skip_normalization__', true);
setSkipNormalizationHint(normalizedErrorData);
contexts[exceptionName] = normalizedErrorData;
}

Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { captureException } from './exports';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from './semanticAttributes';
import { startSpanManual } from './tracing';
import { normalize } from './utils/normalize';
import { addNonEnumerableProperty } from './utils/object';
import { setNormalizationDepthOverrideHint } from './utils/normalizationHints';

interface SentryTrpcMiddlewareOptions {
/** Whether to include procedure inputs in reported events. Defaults to `false`. */
Expand Down Expand Up @@ -53,9 +53,8 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) {
procedure_type: type,
};

addNonEnumerableProperty(
setNormalizationDepthOverrideHint(
trpcContext,
'__sentry_override_normalization_depth__',
1 + // 1 for context.input + the normal normalization depth
(clientOptions?.normalizeDepth ?? 5), // 5 is a sane depth
);
Expand Down
41 changes: 41 additions & 0 deletions packages/core/src/utils/normalizationHints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { addNonEnumerableProperty } from './object';

/**
* Internal symbols for normalization behavior. JSON and other structured user payloads cannot
* carry these keys, so they cannot spoof SDK-only normalization hints.
*/
const SENTRY_SKIP_NORMALIZATION = Symbol('sentry.skipNormalization');
const SENTRY_OVERRIDE_NORMALIZATION_DEPTH = Symbol('sentry.overrideNormalizationDepth');

/** Marks an object so `normalize` returns it unchanged (already-normalized SDK data). */
export function setSkipNormalizationHint(obj: object): void {
addNonEnumerableProperty(obj, SENTRY_SKIP_NORMALIZATION, true);
}

/** Overrides remaining normalization depth from this object downward (e.g. Redux / Pinia state). */
export function setNormalizationDepthOverrideHint(obj: object, depth: number): void {
addNonEnumerableProperty(obj, SENTRY_OVERRIDE_NORMALIZATION_DEPTH, depth);
}

/** @internal */
export function hasSkipNormalizationHint(value: unknown): value is object {
if (typeof value !== 'object' || value === null) {
return false;
}
if (!Object.prototype.hasOwnProperty.call(value, SENTRY_SKIP_NORMALIZATION)) {
return false;
}
return Boolean((value as Record<symbol, unknown>)[SENTRY_SKIP_NORMALIZATION]);
}

/** @internal */
export function getNormalizationDepthOverrideHint(value: unknown): number | undefined {
if (typeof value !== 'object' || value === null) {
return undefined;
}
if (!Object.prototype.hasOwnProperty.call(value, SENTRY_OVERRIDE_NORMALIZATION_DEPTH)) {
return undefined;
}
const v = (value as Record<symbol, unknown>)[SENTRY_OVERRIDE_NORMALIZATION_DEPTH];
return typeof v === 'number' ? v : undefined;
}
18 changes: 7 additions & 11 deletions packages/core/src/utils/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Primitive } from '../types-hoist/misc';
import { isSyntheticEvent, isVueViewModel } from './is';
import { getNormalizationDepthOverrideHint, hasSkipNormalizationHint } from './normalizationHints';
import { convertToPlainObject } from './object';
import { getFunctionName, getVueInternalName } from './stacktrace';

Expand Down Expand Up @@ -101,20 +102,15 @@ function visit(

// From here on, we can assert that `value` is either an object or an array.

// Do not normalize objects that we know have already been normalized. As a general rule, the
// "__sentry_skip_normalization__" property should only be used sparingly and only should only be set on objects that
// have already been normalized.
if ((value as ObjOrArray<unknown>)['__sentry_skip_normalization__']) {
// Do not normalize objects that we know have already been normalized. Hints use internal symbols
// (see normalizationHints.ts) so user-controlled JSON cannot spoof them.
if (hasSkipNormalizationHint(value)) {
return value as ObjOrArray<unknown>;
}

// We can set `__sentry_override_normalization_depth__` on an object to ensure that from there
// We keep a certain amount of depth.
// This should be used sparingly, e.g. we use it for the redux integration to ensure we get a certain amount of state.
const remainingDepth =
typeof (value as ObjOrArray<unknown>)['__sentry_override_normalization_depth__'] === 'number'
? ((value as ObjOrArray<unknown>)['__sentry_override_normalization_depth__'] as number)
: depth;
// Override remaining depth from this node (e.g. Redux / Pinia state). Set via setNormalizationDepthOverrideHint.
const overrideDepth = getNormalizationDepthOverrideHint(value);
const remainingDepth = overrideDepth !== undefined ? overrideDepth : depth;

// We're also done if we've reached the max depth
if (remainingDepth === 0) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/utils/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa
* @param name The name of the property to be set
* @param value The value to which to set the property
*/
export function addNonEnumerableProperty(obj: object, name: string, value: unknown): void {
export function addNonEnumerableProperty(obj: object, name: string | symbol, value: unknown): void {
Comment thread
cursor[bot] marked this conversation as resolved.
try {
Object.defineProperty(obj, name, {
// enumerable: false, // the default, so we can save on bundle size by not explicitly setting it
Expand All @@ -62,7 +62,7 @@ export function addNonEnumerableProperty(obj: object, name: string, value: unkno
configurable: true,
});
} catch {
DEBUG_BUILD && debug.log(`Failed to add non-enumerable property "${name}" to object`, obj);
DEBUG_BUILD && debug.log(`Failed to add non-enumerable property "${String(name)}" to object`, obj);
}
}

Expand Down
14 changes: 7 additions & 7 deletions packages/core/test/lib/utils/normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import { describe, expect, test, vi } from 'vitest';
import { addNonEnumerableProperty, normalize } from '../../../src';
import { normalize, setNormalizationDepthOverrideHint, setSkipNormalizationHint } from '../../../src';
import * as isModule from '../../../src/utils/is';
import * as stacktraceModule from '../../../src/utils/stacktrace';

Expand Down Expand Up @@ -655,15 +655,15 @@ describe('normalize()', () => {
});
});

describe('skips normalizing objects marked with a non-enumerable property __sentry_skip_normalization__', () => {
describe('skips normalizing objects marked with setSkipNormalizationHint (internal symbol)', () => {
test('by leaving non-serializable values intact', () => {
const someFun = () => undefined;
const alreadyNormalizedObj = {
nan: NaN,
fun: someFun,
};

addNonEnumerableProperty(alreadyNormalizedObj, '__sentry_skip_normalization__', true);
setSkipNormalizationHint(alreadyNormalizedObj);

const result = normalize(alreadyNormalizedObj);
expect(result).toEqual({
Expand All @@ -681,7 +681,7 @@ describe('normalize()', () => {
},
};

addNonEnumerableProperty(alreadyNormalizedObj, '__sentry_skip_normalization__', true);
setSkipNormalizationHint(alreadyNormalizedObj);

const obj = {
foo: {
Expand All @@ -703,7 +703,7 @@ describe('normalize()', () => {
});
});

describe('overrides normalization depth with a non-enumerable property __sentry_override_normalization_depth__', () => {
describe('overrides normalization depth with setNormalizationDepthOverrideHint', () => {
test('by increasing depth if it is higher', () => {
const normalizationTarget = {
foo: 'bar',
Expand All @@ -717,7 +717,7 @@ describe('normalize()', () => {
},
};

addNonEnumerableProperty(normalizationTarget, '__sentry_override_normalization_depth__', 3);
setNormalizationDepthOverrideHint(normalizationTarget, 3);

const result = normalize(normalizationTarget, 1);

Expand Down Expand Up @@ -745,7 +745,7 @@ describe('normalize()', () => {
},
};

addNonEnumerableProperty(normalizationTarget, '__sentry_override_normalization_depth__', 1);
setNormalizationDepthOverrideHint(normalizationTarget, 1);
Comment thread
cursor[bot] marked this conversation as resolved.

const result = normalize(normalizationTarget, 3);

Expand Down
11 changes: 8 additions & 3 deletions packages/react/src/redux.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Scope } from '@sentry/core';
import { addBreadcrumb, addNonEnumerableProperty, getClient, getCurrentScope, getGlobalScope } from '@sentry/core';
import {
addBreadcrumb,
getClient,
getCurrentScope,
getGlobalScope,
setNormalizationDepthOverrideHint,
} from '@sentry/core';

interface Action<T = any> {
type: T;
Expand Down Expand Up @@ -138,9 +144,8 @@ function createReduxEnhancer(enhancerOptions?: Partial<SentryEnhancerOptions>):

// Set the normalization depth of the redux state to the configured `normalizeDepth` option or a sane number as a fallback
const newStateContext = { state: { type: 'redux', value: transformedState } };
addNonEnumerableProperty(
setNormalizationDepthOverrideHint(
newStateContext,
'__sentry_override_normalization_depth__',
3 + // 3 layers for `state.value.transformedState`
normalizationDepth, // rest for the actual state
);
Expand Down
11 changes: 8 additions & 3 deletions packages/vue/src/pinia.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { addBreadcrumb, addNonEnumerableProperty, getClient, getCurrentScope, getGlobalScope } from '@sentry/core';
import {
addBreadcrumb,
getClient,
getCurrentScope,
getGlobalScope,
setNormalizationDepthOverrideHint,
} from '@sentry/core';
import type { Ref } from 'vue';

// Inline Pinia types
Expand Down Expand Up @@ -112,9 +118,8 @@ export const createSentryPiniaPlugin: (
state: piniaStateContext,
};

addNonEnumerableProperty(
setNormalizationDepthOverrideHint(
newState,
'__sentry_override_normalization_depth__',
3 + // 3 layers for `state.value.transformedState
normalizationDepth, // rest for the actual state
);
Expand Down
Loading