Skip to content

Commit 059b535

Browse files
committed
fix(core): add VNode detection for vue objects
1 parent ea3183f commit 059b535

File tree

4 files changed

+94
-5
lines changed

4 files changed

+94
-5
lines changed

packages/core/src/utils/is.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,11 @@ interface VueViewModel {
201201
*/
202202
export function isVueViewModel(wat: unknown): boolean {
203203
// Not using Object.prototype.toString because in Vue 3 it would read the instance's Symbol(Symbol.toStringTag) property.
204-
return !!(typeof wat === 'object' && wat !== null && ((wat as VueViewModel).__isVue || (wat as VueViewModel)._isVue));
204+
return !!(
205+
typeof wat === 'object' &&
206+
wat !== null &&
207+
((wat as VueViewModel).__isVue || (wat as VueViewModel)._isVue || (wat as { __v_isVNode?: boolean }).__v_isVNode)
208+
);
205209
}
206210

207211
/**

packages/core/src/utils/normalize.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ function stringifyValue(
217217
}
218218

219219
if (isVueViewModel(value)) {
220-
return '[VueViewModel]';
220+
// Check if it's a VNode (has __v_isVNode) vs a component instance (has _isVue/__isVue)
221+
const isVNode = (value as { __v_isVNode?: boolean }).__v_isVNode;
222+
223+
return isVNode ? '[VueVNode]' : '[VueViewModel]';
221224
}
222225

223226
// React's SyntheticEvent thingy

packages/core/test/lib/utils/is.test.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,42 @@ describe('isInstanceOf()', () => {
121121
});
122122

123123
describe('isVueViewModel()', () => {
124-
test('should work as advertised', () => {
125-
expect(isVueViewModel({ _isVue: true })).toEqual(true);
126-
expect(isVueViewModel({ __isVue: true })).toEqual(true);
124+
test('detects Vue 2 component instances with _isVue', () => {
125+
const vue2Component = { _isVue: true, $el: {}, $data: {} };
126+
expect(isVueViewModel(vue2Component)).toEqual(true);
127+
});
128+
129+
test('detects Vue 3 component instances with __isVue', () => {
130+
const vue3Component = { __isVue: true, $el: {}, $data: {} };
131+
expect(isVueViewModel(vue3Component)).toEqual(true);
132+
});
133+
134+
test('detects Vue 3 VNodes with __v_isVNode', () => {
135+
const vueVNode = {
136+
__v_isVNode: true,
137+
__v_skip: true,
138+
type: {},
139+
props: {},
140+
children: null,
141+
};
142+
expect(isVueViewModel(vueVNode)).toEqual(true);
143+
});
127144

145+
test('does not detect plain objects', () => {
128146
expect(isVueViewModel({ foo: true })).toEqual(false);
147+
expect(isVueViewModel({ __v_skip: true })).toEqual(false); // __v_skip alone is not enough
148+
expect(isVueViewModel({})).toEqual(false);
149+
});
150+
151+
test('handles null and undefined', () => {
152+
expect(isVueViewModel(null)).toEqual(false);
153+
expect(isVueViewModel(undefined)).toEqual(false);
154+
});
155+
156+
test('handles non-objects', () => {
157+
expect(isVueViewModel('string')).toEqual(false);
158+
expect(isVueViewModel(123)).toEqual(false);
159+
expect(isVueViewModel(true)).toEqual(false);
129160
});
130161
});
131162

packages/vue/test/integration/VueIntegration.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,55 @@ describe('Sentry.VueIntegration', () => {
9393
]);
9494
expect(loggerWarnings).toEqual([]);
9595
});
96+
97+
it('does not trigger warning spam when normalizing Vue VNodes with high normalizeDepth', () => {
98+
// This test reproduces the issue from https://github.com/getsentry/sentry-javascript/issues/18203
99+
// where VNodes in console arguments would trigger recursive warning spam with captureConsoleIntegration
100+
101+
Sentry.init({
102+
dsn: PUBLIC_DSN,
103+
defaultIntegrations: false,
104+
normalizeDepth: 10, // High depth that would cause the issue
105+
integrations: [Sentry.captureConsoleIntegration({ levels: ['warn'] })],
106+
});
107+
108+
const initialWarningCount = warnings.length;
109+
110+
// Create a mock VNode that simulates the problematic behavior from the original issue
111+
// In the real scenario, accessing VNode properties during normalization would trigger Vue warnings
112+
// which would then be captured and normalized again, creating a recursive loop
113+
let propertyAccessCount = 0;
114+
const mockVNode = {
115+
__v_isVNode: true,
116+
__v_skip: true,
117+
type: {},
118+
get ctx() {
119+
// Simulate Vue's behavior where accessing ctx triggers a warning
120+
propertyAccessCount++;
121+
// eslint-disable-next-line no-console
122+
console.warn('[Vue warn]: compilerOptions warning triggered by property access');
123+
return { uid: 1 };
124+
},
125+
get props() {
126+
propertyAccessCount++;
127+
return {};
128+
},
129+
};
130+
131+
// Pass the mock VNode to console.warn, simulating what Vue does
132+
// Without the fix, Sentry would try to normalize mockVNode, access its ctx property,
133+
// which triggers another warning, which gets captured and normalized, creating infinite recursion
134+
// eslint-disable-next-line no-console
135+
console.warn('[Vue warn]: Original warning', mockVNode);
136+
137+
// With the fix, Sentry detects the VNode early and stringifies it as [VueVNode]
138+
// without accessing its properties, so propertyAccessCount stays at 0
139+
expect(propertyAccessCount).toBe(0);
140+
141+
// Only 1 warning should be captured (the original one)
142+
// Without the fix, the count would multiply as ctx getter warnings get recursively captured
143+
const warningCountAfter = warnings.length;
144+
const newWarnings = warningCountAfter - initialWarningCount;
145+
expect(newWarnings).toBe(1);
146+
});
96147
});

0 commit comments

Comments
 (0)