Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/core/src/utils/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,11 @@ interface VueViewModel {
*/
export function isVueViewModel(wat: unknown): boolean {
// Not using Object.prototype.toString because in Vue 3 it would read the instance's Symbol(Symbol.toStringTag) property.
return !!(typeof wat === 'object' && wat !== null && ((wat as VueViewModel).__isVue || (wat as VueViewModel)._isVue));
return !!(
typeof wat === 'object' &&
wat !== null &&
((wat as VueViewModel).__isVue || (wat as VueViewModel)._isVue || (wat as { __v_isVNode?: boolean }).__v_isVNode)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l/m: Could we add a comment here explaining a little bit what we're doing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea absolutely

);
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/utils/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ function stringifyValue(
}

if (isVueViewModel(value)) {
return '[VueViewModel]';
// Check if it's a VNode (has __v_isVNode) vs a component instance (has _isVue/__isVue)
const isVNode = (value as { __v_isVNode?: boolean }).__v_isVNode;

return isVNode ? '[VueVNode]' : '[VueViewModel]';
}

// React's SyntheticEvent thingy
Expand Down
37 changes: 34 additions & 3 deletions packages/core/test/lib/utils/is.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,42 @@ describe('isInstanceOf()', () => {
});

describe('isVueViewModel()', () => {
test('should work as advertised', () => {
expect(isVueViewModel({ _isVue: true })).toEqual(true);
expect(isVueViewModel({ __isVue: true })).toEqual(true);
test('detects Vue 2 component instances with _isVue', () => {
const vue2Component = { _isVue: true, $el: {}, $data: {} };
expect(isVueViewModel(vue2Component)).toEqual(true);
});

test('detects Vue 3 component instances with __isVue', () => {
const vue3Component = { __isVue: true, $el: {}, $data: {} };
expect(isVueViewModel(vue3Component)).toEqual(true);
});

test('detects Vue 3 VNodes with __v_isVNode', () => {
const vueVNode = {
__v_isVNode: true,
__v_skip: true,
type: {},
props: {},
children: null,
};
expect(isVueViewModel(vueVNode)).toEqual(true);
});

test('does not detect plain objects', () => {
expect(isVueViewModel({ foo: true })).toEqual(false);
expect(isVueViewModel({ __v_skip: true })).toEqual(false); // __v_skip alone is not enough
expect(isVueViewModel({})).toEqual(false);
});

test('handles null and undefined', () => {
expect(isVueViewModel(null)).toEqual(false);
expect(isVueViewModel(undefined)).toEqual(false);
});

test('handles non-objects', () => {
expect(isVueViewModel('string')).toEqual(false);
expect(isVueViewModel(123)).toEqual(false);
expect(isVueViewModel(true)).toEqual(false);
});
});

Expand Down
51 changes: 51 additions & 0 deletions packages/vue/test/integration/VueIntegration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,55 @@ describe('Sentry.VueIntegration', () => {
]);
expect(loggerWarnings).toEqual([]);
});

it('does not trigger warning spam when normalizing Vue VNodes with high normalizeDepth', () => {
// This test reproduces the issue from https://github.com/getsentry/sentry-javascript/issues/18203
// where VNodes in console arguments would trigger recursive warning spam with captureConsoleIntegration

Sentry.init({
dsn: PUBLIC_DSN,
defaultIntegrations: false,
normalizeDepth: 10, // High depth that would cause the issue
integrations: [Sentry.captureConsoleIntegration({ levels: ['warn'] })],
});

const initialWarningCount = warnings.length;

// Create a mock VNode that simulates the problematic behavior from the original issue
// In the real scenario, accessing VNode properties during normalization would trigger Vue warnings
// which would then be captured and normalized again, creating a recursive loop
let propertyAccessCount = 0;
const mockVNode = {
__v_isVNode: true,
__v_skip: true,
type: {},
get ctx() {
// Simulate Vue's behavior where accessing ctx triggers a warning
propertyAccessCount++;
// eslint-disable-next-line no-console
console.warn('[Vue warn]: compilerOptions warning triggered by property access');
return { uid: 1 };
},
get props() {
propertyAccessCount++;
return {};
},
};

// Pass the mock VNode to console.warn, simulating what Vue does
// Without the fix, Sentry would try to normalize mockVNode, access its ctx property,
// which triggers another warning, which gets captured and normalized, creating infinite recursion
// eslint-disable-next-line no-console
console.warn('[Vue warn]: Original warning', mockVNode);

// With the fix, Sentry detects the VNode early and stringifies it as [VueVNode]
// without accessing its properties, so propertyAccessCount stays at 0
expect(propertyAccessCount).toBe(0);

// Only 1 warning should be captured (the original one)
// Without the fix, the count would multiply as ctx getter warnings get recursively captured
const warningCountAfter = warnings.length;
const newWarnings = warningCountAfter - initialWarningCount;
expect(newWarnings).toBe(1);
});
});
Loading