Skip to content

Commit 90a8fcf

Browse files
author
Konrad Dysput
committed
node, sdk-core: Implement similar fix to the breadcrumbs storage and limit object depth
1 parent be3376b commit 90a8fcf

3 files changed

Lines changed: 85 additions & 13 deletions

File tree

‎packages/node/src/breadcrumbs/FileBreadcrumbsStorage.ts‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AttributeType,
23
BacktraceAttachment,
34
BacktraceAttachmentProvider,
45
Breadcrumb,
@@ -107,9 +108,8 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage {
107108
timestamp: TimeHelper.now(),
108109
type: BreadcrumbType[rawBreadcrumb.type].toLowerCase(),
109110
level: BreadcrumbLogLevel[rawBreadcrumb.level].toLowerCase(),
110-
attributes: rawBreadcrumb.attributes,
111+
attributes: this.prepareAttributes(rawBreadcrumb.attributes),
111112
};
112-
113113
const breadcrumbJson = JSON.stringify(breadcrumb, jsonEscaper());
114114
const jsonLength = breadcrumbJson.length + 1; // newline
115115
const sizeLimit = this._limits.maximumTotalBreadcrumbsSize;
@@ -123,6 +123,45 @@ export class FileBreadcrumbsStorage implements BreadcrumbsStorage {
123123
return id;
124124
}
125125

126+
private prepareAttributes(attributes?: Record<string, AttributeType>): Record<string, AttributeType> | undefined {
127+
const result: Record<string, AttributeType> = {};
128+
if (!attributes) {
129+
return undefined;
130+
}
131+
for (const key in attributes) {
132+
const value = attributes[key];
133+
switch (typeof value) {
134+
case 'number':
135+
case 'boolean':
136+
case 'string':
137+
case 'undefined':
138+
result[key] = value;
139+
break;
140+
case 'bigint':
141+
result[key] = (value as bigint).toString();
142+
break;
143+
case 'object': {
144+
if (!value) {
145+
result[key] = value;
146+
break;
147+
}
148+
try {
149+
if (value instanceof Date) {
150+
result[key] = value.toISOString();
151+
} else if (value instanceof URL) {
152+
result[key] = value.toString();
153+
}
154+
} catch {
155+
// revoked proxy or broken object — drop it
156+
}
157+
// drop all other objects
158+
break;
159+
}
160+
}
161+
}
162+
return result;
163+
}
164+
126165
private static getFileName(index: number) {
127166
return `${FILE_PREFIX}-${index}`;
128167
}

‎packages/sdk-core/src/common/limitObjectDepth.ts‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,43 @@ type DeepPartial<T extends object> = Partial<{ [K in keyof T]: T[K] extends obje
22

33
const REMOVED_PLACEHOLDER = '<removed>';
44

5-
export type Limited<T extends object> = DeepPartial<T> | typeof REMOVED_PLACEHOLDER;
5+
export type Limited<T> = (T extends object ? DeepPartial<T> : T) | typeof REMOVED_PLACEHOLDER;
6+
7+
export function limitObjectDepth<T>(val: T, depth: number): Limited<T> {
8+
if (typeof val !== 'object' || !val) {
9+
return val as Limited<T>;
10+
}
611

7-
export function limitObjectDepth<T extends object>(obj: T, depth: number): Limited<T> {
812
if (!(depth < Infinity)) {
9-
return obj;
13+
return val as Limited<T>;
1014
}
1115

1216
if (depth < 0) {
1317
return REMOVED_PLACEHOLDER;
1418
}
1519

16-
const limitIfObject = (value: unknown) =>
17-
typeof value === 'object' && value ? limitObjectDepth(value, depth - 1) : value;
20+
try {
21+
if ('toJSON' in val && typeof val.toJSON === 'function') {
22+
return limitObjectDepth(val.toJSON(), depth);
23+
}
24+
} catch (err) {
25+
if (err instanceof TypeError) {
26+
return REMOVED_PLACEHOLDER;
27+
}
28+
// broken toJSON — fall through to iterate own properties
29+
}
30+
31+
const limitChild = (value: unknown) => limitObjectDepth(value, depth - 1);
1832

19-
const result: DeepPartial<T> = {};
20-
for (const key in obj) {
21-
const value = obj[key];
33+
const result: DeepPartial<T & object> = {};
34+
for (const key in val) {
35+
const value = val[key];
2236
if (Array.isArray(value)) {
23-
result[key] = value.map(limitIfObject) as never;
37+
result[key] = value.map(limitChild) as never;
2438
} else {
25-
result[key] = limitIfObject(value) as never;
39+
result[key] = limitChild(value) as never;
2640
}
2741
}
2842

29-
return result;
43+
return result as Limited<T>;
3044
}

‎packages/sdk-core/tests/breadcrumbs/breadcrumbsCreationTests.spec.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AttributeType } from '../../src/index.js';
12
import { BreadcrumbsManager } from '../../src/modules/breadcrumbs/BreadcrumbsManager.js';
23
import { BreadcrumbLogLevel, BreadcrumbType } from '../../src/modules/breadcrumbs/index.js';
34
import { InMemoryBreadcrumbsStorage } from '../../src/modules/breadcrumbs/storage/InMemoryBreadcrumbsStorage.js';
@@ -123,4 +124,22 @@ describe('Breadcrumbs creation tests', () => {
123124

124125
expect(breadcrumb.attributes).toMatchObject(attributes);
125126
});
127+
it('Should handle breadcrumb with not serializable attributes', () => {
128+
const message = 'test';
129+
const level = BreadcrumbLogLevel.Warning;
130+
const attributes = {
131+
url: new URL('https://example.com/path?q=1'),
132+
date: new Date(),
133+
objectCreatePrototype: Object.create(Date.prototype),
134+
destroyedUrl: { ...new URL('https://example.com/path?q=1'), date: new Date() },
135+
} as unknown as Record<string, AttributeType>;
136+
const storage = new InMemoryBreadcrumbsStorage({ maximumBreadcrumbs: 100 });
137+
const breadcrumbsManager = new BreadcrumbsManager(undefined, { storage: () => storage });
138+
breadcrumbsManager.initialize();
139+
breadcrumbsManager.log(message, level, attributes);
140+
const [breadcrumb] = JSON.parse(storage.get() as string);
141+
142+
expect(breadcrumb.attributes['url']).toBeDefined();
143+
expect(breadcrumb.attributes['date']).toBeDefined();
144+
});
126145
});

0 commit comments

Comments
 (0)