diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 6641e1e87296..4e0def64abee 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -223,24 +223,27 @@ export class SentrySpan implements Span { * use `spanToJSON(span)` instead. */ public getSpanJSON(): SpanJSON { - return dropUndefinedKeys({ - data: this._attributes, - description: this._name, - op: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], - parent_span_id: this._parentSpanId, - span_id: this._spanId, - start_timestamp: this._startTime, - status: getStatusMessage(this._status), - timestamp: this._endTime, - trace_id: this._traceId, - origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, - profile_id: this._attributes[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined, - exclusive_time: this._attributes[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME] as number | undefined, - measurements: timedEventsToMeasurements(this._events), - is_segment: (this._isStandaloneSpan && getRootSpan(this) === this) || undefined, - segment_id: this._isStandaloneSpan ? getRootSpan(this).spanContext().spanId : undefined, - links: convertSpanLinksForEnvelope(this._links), - }); + return dropUndefinedKeys( + { + data: this._attributes, + description: this._name, + op: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], + parent_span_id: this._parentSpanId, + span_id: this._spanId, + start_timestamp: this._startTime, + status: getStatusMessage(this._status), + timestamp: this._endTime, + trace_id: this._traceId, + origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + profile_id: this._attributes[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined, + exclusive_time: this._attributes[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME] as number | undefined, + measurements: timedEventsToMeasurements(this._events), + is_segment: (this._isStandaloneSpan && getRootSpan(this) === this) || undefined, + segment_id: this._isStandaloneSpan ? getRootSpan(this).spanContext().spanId : undefined, + links: convertSpanLinksForEnvelope(this._links), + }, + 1, + ); } /** @inheritdoc */ diff --git a/packages/core/src/utils-hoist/object.ts b/packages/core/src/utils-hoist/object.ts index 7826e960d982..6b2d81b4da83 100644 --- a/packages/core/src/utils-hoist/object.ts +++ b/packages/core/src/utils-hoist/object.ts @@ -211,17 +211,22 @@ export function extractExceptionKeysForMessage(exception: Record(inputValue: T): T { +export function dropUndefinedKeys(inputValue: T, depth = Infinity): T { // This map keeps track of what already visited nodes map to. // Our Set - based memoBuilder doesn't work here because we want to the output object to have the same circular // references as the input object. const memoizationMap = new Map(); // This function just proxies `_dropUndefinedKeys` to keep the `memoBuilder` out of this function's API - return _dropUndefinedKeys(inputValue, memoizationMap); + return _dropUndefinedKeys(inputValue, memoizationMap, depth); } -function _dropUndefinedKeys(inputValue: T, memoizationMap: Map): T { +function _dropUndefinedKeys(inputValue: T, memoizationMap: Map, depth: number): T { + // If the max. depth is reached, return the input value as is + if (!depth) { + return inputValue; + } + // Early return for primitive values if (inputValue === null || typeof inputValue !== 'object') { return inputValue; diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index 07907d8b9b4f..de4b302053d2 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -91,14 +91,17 @@ export function httpRequestToRequestData(request: { // This is non-standard, but may be set on e.g. Next.js or Express requests const cookies = (request as PolymorphicRequest).cookies; - return dropUndefinedKeys({ - url: absoluteUrl, - method: request.method, - query_string: extractQueryParamsFromUrl(url), - headers: headersToDict(headers), - cookies, - data, - }); + return dropUndefinedKeys( + { + url: absoluteUrl, + method: request.method, + query_string: extractQueryParamsFromUrl(url), + headers: headersToDict(headers), + cookies, + data, + }, + 1, + ); } function getAbsoluteUrl({ diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 7cb19fbacf3c..22e0755fdd91 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -42,16 +42,19 @@ export function spanToTransactionTraceContext(span: Span): TraceContext { const { spanId: span_id, traceId: trace_id } = span.spanContext(); const { data, op, parent_span_id, status, origin, links } = spanToJSON(span); - return dropUndefinedKeys({ - parent_span_id, - span_id, - trace_id, - data, - op, - status, - origin, - links, - }); + return dropUndefinedKeys( + { + parent_span_id, + span_id, + trace_id, + data, + op, + status, + origin, + links, + }, + 1, + ); } /** @@ -147,20 +150,23 @@ export function spanToJSON(span: Span): SpanJSON { if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) { const { attributes, startTime, name, endTime, parentSpanId, status, links } = span; - return dropUndefinedKeys({ - span_id, - trace_id, - data: attributes, - description: name, - parent_span_id: parentSpanId, - start_timestamp: spanTimeInputToSeconds(startTime), - // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time - timestamp: spanTimeInputToSeconds(endTime) || undefined, - status: getStatusMessage(status), - op: attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], - origin: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, - links: convertSpanLinksForEnvelope(links), - }); + return dropUndefinedKeys( + { + span_id, + trace_id, + data: attributes, + description: name, + parent_span_id: parentSpanId, + start_timestamp: spanTimeInputToSeconds(startTime), + // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time + timestamp: spanTimeInputToSeconds(endTime) || undefined, + status: getStatusMessage(status), + op: attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], + origin: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + links: convertSpanLinksForEnvelope(links), + }, + 1, + ); } // Finally, at least we have `spanContext()`.... diff --git a/packages/core/test/utils-hoist/object.test.ts b/packages/core/test/utils-hoist/object.test.ts index e1c32f163193..a3d68ccca1d3 100644 --- a/packages/core/test/utils-hoist/object.test.ts +++ b/packages/core/test/utils-hoist/object.test.ts @@ -208,6 +208,33 @@ describe('dropUndefinedKeys()', () => { }); }); + test('arrays with depth=1', () => { + expect( + dropUndefinedKeys( + { + a: [ + 1, + undefined, + { + a: 1, + b: undefined, + }, + ], + }, + 1, + ), + ).toStrictEqual({ + a: [ + 1, + undefined, + { + a: 1, + b: undefined, + }, + ], + }); + }); + test('nested objects', () => { expect( dropUndefinedKeys({ @@ -232,6 +259,65 @@ describe('dropUndefinedKeys()', () => { }); }); + test('nested objects with depth=1', () => { + expect( + dropUndefinedKeys( + { + a: 1, + b: { + c: 2, + d: undefined, + e: { + f: 3, + g: undefined, + }, + }, + c: undefined, + }, + 1, + ), + ).toStrictEqual({ + a: 1, + b: { + c: 2, + d: undefined, + e: { + f: 3, + g: undefined, + }, + }, + }); + }); + + test('nested objects with depth=2', () => { + expect( + dropUndefinedKeys( + { + a: 1, + b: { + c: 2, + d: undefined, + e: { + f: 3, + g: undefined, + }, + }, + c: undefined, + }, + 2, + ), + ).toStrictEqual({ + a: 1, + b: { + c: 2, + e: { + f: 3, + g: undefined, + }, + }, + }); + }); + describe('class instances', () => { class MyClass { public a = 'foo'; diff --git a/packages/replay-internal/src/integration.ts b/packages/replay-internal/src/integration.ts index 4ec1a357eac4..8115e4e657a7 100644 --- a/packages/replay-internal/src/integration.ts +++ b/packages/replay-internal/src/integration.ts @@ -356,7 +356,7 @@ function loadReplayOptionsFromClient(initialOptions: InitialReplayPluginOptions, const finalOptions: ReplayPluginOptions = { sessionSampleRate: 0, errorSampleRate: 0, - ...dropUndefinedKeys(initialOptions), + ...dropUndefinedKeys(initialOptions, 1), }; const replaysSessionSampleRate = parseSampleRate(opt.replaysSessionSampleRate);