Skip to content

Commit b74e199

Browse files
authored
fix: Properly remove undefined keys from apm payload (getsentry#2414)
1 parent 7b118c8 commit b74e199

File tree

4 files changed

+145
-35
lines changed

4 files changed

+145
-35
lines changed

packages/apm/src/span.ts

+24-33
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { getCurrentHub, Hub } from '@sentry/hub';
44
import { Span as SpanInterface, SpanContext, SpanStatus } from '@sentry/types';
5-
import { isInstanceOf, logger, timestampWithMs, uuid4 } from '@sentry/utils';
5+
import { dropUndefinedKeys, isInstanceOf, logger, timestampWithMs, uuid4 } from '@sentry/utils';
66

77
// TODO: Should this be exported?
88
export const TRACEPARENT_REGEXP = new RegExp(
@@ -314,43 +314,34 @@ export class Span implements SpanInterface, SpanContext {
314314
* @inheritDoc
315315
*/
316316
public getTraceContext(): object {
317-
// JSON.parse + JSON.stringify to remove undefined values
318-
// tslint:disable-next-line: no-unsafe-any
319-
return JSON.parse(
320-
JSON.stringify({
321-
data: this.data,
322-
description: this.description,
323-
op: this.op,
324-
parent_span_id: this._parentSpanId,
325-
span_id: this._spanId,
326-
// Undefined status will be dropped by `JSON.stringify` anyway so it's safe to read it directly like that
327-
status: this.tags.status,
328-
tags: this.tags,
329-
trace_id: this._traceId,
330-
}),
331-
);
317+
return dropUndefinedKeys({
318+
data: this.data,
319+
description: this.description,
320+
op: this.op,
321+
parent_span_id: this._parentSpanId,
322+
span_id: this._spanId,
323+
status: this.tags.status,
324+
tags: this.tags,
325+
trace_id: this._traceId,
326+
});
332327
}
333328

334329
/**
335330
* @inheritDoc
336331
*/
337332
public toJSON(): object {
338-
// JSON.parse + JSON.stringify to remove undefined values
339-
// tslint:disable-next-line: no-unsafe-any
340-
return JSON.parse(
341-
JSON.stringify({
342-
data: this.data,
343-
description: this.description,
344-
op: this.op,
345-
parent_span_id: this._parentSpanId,
346-
sampled: this.sampled,
347-
span_id: this._spanId,
348-
start_timestamp: this.startTimestamp,
349-
tags: this.tags,
350-
timestamp: this.timestamp,
351-
trace_id: this._traceId,
352-
transaction: this.transaction,
353-
}),
354-
);
333+
return dropUndefinedKeys({
334+
data: this.data,
335+
description: this.description,
336+
op: this.op,
337+
parent_span_id: this._parentSpanId,
338+
sampled: this.sampled,
339+
span_id: this._spanId,
340+
start_timestamp: this.startTimestamp,
341+
tags: this.tags,
342+
timestamp: this.timestamp,
343+
trace_id: this._traceId,
344+
transaction: this.transaction,
345+
});
355346
}
356347
}

packages/apm/test/span.test.ts

+32
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,27 @@ describe('Span', () => {
154154
expect(serialized).toHaveProperty('span_id', 'd');
155155
expect(serialized).toHaveProperty('trace_id', 'c');
156156
});
157+
158+
test('should drop all `undefined` values', () => {
159+
const spanA = new Span({ traceId: 'a', spanId: 'b' }) as any;
160+
const spanB = new Span({
161+
parentSpanId: spanA._spanId,
162+
sampled: false,
163+
spanId: 'd',
164+
traceId: 'c',
165+
});
166+
const serialized = spanB.toJSON();
167+
expect(serialized).toHaveProperty('start_timestamp');
168+
delete (serialized as { start_timestamp: number }).start_timestamp;
169+
expect(serialized).toStrictEqual({
170+
data: {},
171+
parent_span_id: 'b',
172+
sampled: false,
173+
span_id: 'd',
174+
tags: {},
175+
trace_id: 'c',
176+
});
177+
});
157178
});
158179

159180
describe('finish', () => {
@@ -231,5 +252,16 @@ describe('Span', () => {
231252
const context = span.getTraceContext();
232253
expect((context as any).status).toBe('resource_exhausted');
233254
});
255+
256+
test('should drop all `undefined` values', () => {
257+
const spanB = new Span({ spanId: 'd', traceId: 'c' });
258+
const context = spanB.getTraceContext();
259+
expect(context).toStrictEqual({
260+
data: {},
261+
span_id: 'd',
262+
tags: {},
263+
trace_id: 'c',
264+
});
265+
});
234266
});
235267
});

packages/utils/src/object.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ExtendedError, WrappedFunction } from '@sentry/types';
22

3-
import { isElement, isError, isEvent, isInstanceOf, isPrimitive, isSyntheticEvent } from './is';
3+
import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive, isSyntheticEvent } from './is';
44
import { Memo } from './memo';
55
import { getFunctionName, htmlTreeAsString } from './misc';
66
import { truncate } from './string';
@@ -351,3 +351,26 @@ export function extractExceptionKeysForMessage(exception: any, maxLength: number
351351

352352
return '';
353353
}
354+
355+
/**
356+
* Given any object, return the new object with removed keys that value was `undefined`.
357+
* Works recursively on objects and arrays.
358+
*/
359+
export function dropUndefinedKeys<T>(val: T): T {
360+
if (isPlainObject(val)) {
361+
const obj = val as { [key: string]: any };
362+
const rv: { [key: string]: any } = {};
363+
for (const key of Object.keys(obj)) {
364+
if (typeof obj[key] !== 'undefined') {
365+
rv[key] = dropUndefinedKeys(obj[key]);
366+
}
367+
}
368+
return rv as T;
369+
}
370+
371+
if (Array.isArray(val)) {
372+
return val.map(dropUndefinedKeys) as any;
373+
}
374+
375+
return val;
376+
}

packages/utils/test/object.test.ts

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { extractExceptionKeysForMessage, fill, normalize, urlEncode } from '../src/object';
1+
import { dropUndefinedKeys, extractExceptionKeysForMessage, fill, normalize, urlEncode } from '../src/object';
22

33
describe('fill()', () => {
44
test('wraps a method by calling a replacement function on it', () => {
@@ -551,3 +551,67 @@ describe('extractExceptionKeysForMessage()', () => {
551551
expect(extractExceptionKeysForMessage({ barbazquxfoo: '_', baz: '_', qux: '_' }, 10)).toEqual('barbazquxf...');
552552
});
553553
});
554+
555+
describe('dropUndefinedKeys()', () => {
556+
test('simple case', () => {
557+
expect(
558+
dropUndefinedKeys({
559+
a: 1,
560+
b: undefined,
561+
c: null,
562+
d: 'd',
563+
}),
564+
).toStrictEqual({
565+
a: 1,
566+
c: null,
567+
d: 'd',
568+
});
569+
});
570+
571+
test('arrays', () => {
572+
expect(
573+
dropUndefinedKeys({
574+
a: [
575+
1,
576+
undefined,
577+
{
578+
a: 1,
579+
b: undefined,
580+
},
581+
],
582+
}),
583+
).toStrictEqual({
584+
a: [
585+
1,
586+
undefined,
587+
{
588+
a: 1,
589+
},
590+
],
591+
});
592+
});
593+
594+
test('nested objects', () => {
595+
expect(
596+
dropUndefinedKeys({
597+
a: 1,
598+
b: {
599+
c: 2,
600+
d: undefined,
601+
e: {
602+
f: 3,
603+
g: undefined,
604+
},
605+
},
606+
}),
607+
).toStrictEqual({
608+
a: 1,
609+
b: {
610+
c: 2,
611+
e: {
612+
f: 3,
613+
},
614+
},
615+
});
616+
});
617+
});

0 commit comments

Comments
 (0)