Skip to content

Commit a9e52e7

Browse files
authored
fix(utils): Ensure dropUndefinedKeys() does not break class instances (#10245)
The old implementation of `dropUndefinedKeys()` could break things, because it used `isPlainObject()` which actually does not check if something is a POJO, but just any object. So any class instance found somewhere would be broken by this. This PR fixes this to check for POJOs better, and leaves class instances alone. Supersedes https://github.com/getsentry/sentry-javascript/pull/10242/files
1 parent a380b93 commit a9e52e7

File tree

4 files changed

+63
-2
lines changed

4 files changed

+63
-2
lines changed

packages/utils/src/is.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export function isPrimitive(wat: unknown): wat is Primitive {
106106
}
107107

108108
/**
109-
* Checks whether given value's type is an object literal
109+
* Checks whether given value's type is an object literal, or a class instance.
110110
* {@link isPlainObject}.
111111
*
112112
* @param wat A value to be checked.

packages/utils/src/object.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export function dropUndefinedKeys<T>(inputValue: T): T {
222222
}
223223

224224
function _dropUndefinedKeys<T>(inputValue: T, memoizationMap: Map<unknown, unknown>): T {
225-
if (isPlainObject(inputValue)) {
225+
if (isPojo(inputValue)) {
226226
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
227227
const memoVal = memoizationMap.get(inputValue);
228228
if (memoVal !== undefined) {
@@ -263,6 +263,19 @@ function _dropUndefinedKeys<T>(inputValue: T, memoizationMap: Map<unknown, unkno
263263
return inputValue;
264264
}
265265

266+
function isPojo(input: unknown): input is Record<string, unknown> {
267+
if (!isPlainObject(input)) {
268+
return false;
269+
}
270+
271+
try {
272+
const name = (Object.getPrototypeOf(input) as { constructor: { name: string } }).constructor.name;
273+
return !name || name === 'Object';
274+
} catch {
275+
return true;
276+
}
277+
}
278+
266279
/**
267280
* Ensure that something is an object.
268281
*

packages/utils/test/is.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isErrorEvent,
66
isInstanceOf,
77
isNaN,
8+
isPlainObject,
89
isPrimitive,
910
isThenable,
1011
isVueViewModel,
@@ -144,3 +145,27 @@ describe('isVueViewModel()', () => {
144145
expect(isVueViewModel({ foo: true })).toEqual(false);
145146
});
146147
});
148+
149+
describe('isPlainObject', () => {
150+
class MyClass {
151+
public foo: string = 'bar';
152+
}
153+
154+
it.each([
155+
[{}, true],
156+
[true, false],
157+
[false, false],
158+
[undefined, false],
159+
[null, false],
160+
['', false],
161+
[1, false],
162+
[0, false],
163+
[{ aha: 'yes' }, true],
164+
[new Object({ aha: 'yes' }), true],
165+
[new String('aa'), false],
166+
[new MyClass(), true],
167+
[{ ...new MyClass() }, true],
168+
])('%s is %s', (value, expected) => {
169+
expect(isPlainObject(value)).toBe(expected);
170+
});
171+
});

packages/utils/test/object.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,29 @@ describe('dropUndefinedKeys()', () => {
210210
});
211211
});
212212

213+
describe('class instances', () => {
214+
class MyClass {
215+
public a = 'foo';
216+
public b = undefined;
217+
}
218+
219+
test('ignores class instance', () => {
220+
const instance = new MyClass();
221+
const result = dropUndefinedKeys(instance);
222+
expect(result).toEqual({ a: 'foo', b: undefined });
223+
expect(result).toBeInstanceOf(MyClass);
224+
expect(Object.prototype.hasOwnProperty.call(result, 'b')).toBe(true);
225+
});
226+
227+
test('ignores nested instances', () => {
228+
const instance = new MyClass();
229+
const result = dropUndefinedKeys({ a: [instance] });
230+
expect(result).toEqual({ a: [instance] });
231+
expect(result.a[0]).toBeInstanceOf(MyClass);
232+
expect(Object.prototype.hasOwnProperty.call(result.a[0], 'b')).toBe(true);
233+
});
234+
});
235+
213236
test('should not throw on objects with circular reference', () => {
214237
const chicken: any = {
215238
food: undefined,

0 commit comments

Comments
 (0)