Skip to content

Commit 90ee2a4

Browse files
authored
fix(utils): Prevent iterating over VueViewModel (#8981)
Prevent stringifying VueViewModel objects which causes a warning when the object is logged to console. Instead, normalize it's string value to `"[VueViewModel]"` More details in #8980
1 parent 789e849 commit 90ee2a4

File tree

6 files changed

+72
-15
lines changed

6 files changed

+72
-15
lines changed

packages/browser/src/integrations/breadcrumbs.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,18 +183,6 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa
183183
* Creates breadcrumbs from console API calls
184184
*/
185185
function _consoleBreadcrumb(handlerData: HandlerData & { args: unknown[]; level: string }): void {
186-
// This is a hack to fix a Vue3-specific bug that causes an infinite loop of
187-
// console warnings. This happens when a Vue template is rendered with
188-
// an undeclared variable, which we try to stringify, ultimately causing
189-
// Vue to issue another warning which repeats indefinitely.
190-
// see: https://github.com/getsentry/sentry-javascript/pull/6010
191-
// see: https://github.com/getsentry/sentry-javascript/issues/5916
192-
for (let i = 0; i < handlerData.args.length; i++) {
193-
if (handlerData.args[i] === 'ref=Ref<') {
194-
handlerData.args[i + 1] = 'viewRef';
195-
break;
196-
}
197-
}
198186
const breadcrumb = {
199187
category: 'console',
200188
data: {

packages/utils/src/is.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,20 @@ export function isInstanceOf(wat: any, base: any): boolean {
179179
return false;
180180
}
181181
}
182+
183+
interface VueViewModel {
184+
// Vue3
185+
__isVue?: boolean;
186+
// Vue2
187+
_isVue?: boolean;
188+
}
189+
/**
190+
* Checks whether given value's type is a Vue ViewModel.
191+
*
192+
* @param wat A value to be checked.
193+
* @returns A boolean representing the result.
194+
*/
195+
export function isVueViewModel(wat: unknown): boolean {
196+
// Not using Object.prototype.toString because in Vue 3 it would read the instance's Symbol(Symbol.toStringTag) property.
197+
return !!(typeof wat === 'object' && wat !== null && ((wat as VueViewModel).__isVue || (wat as VueViewModel)._isVue));
198+
}

packages/utils/src/normalize.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Primitive } from '@sentry/types';
22

3-
import { isNaN, isSyntheticEvent } from './is';
3+
import { isNaN, isSyntheticEvent, isVueViewModel } from './is';
44
import type { MemoFunc } from './memo';
55
import { memoBuilder } from './memo';
66
import { convertToPlainObject } from './object';
@@ -214,6 +214,10 @@ function stringifyValue(
214214
return '[Document]';
215215
}
216216

217+
if (isVueViewModel(value)) {
218+
return '[VueViewModel]';
219+
}
220+
217221
// React's SyntheticEvent thingy
218222
if (isSyntheticEvent(value)) {
219223
return '[SyntheticEvent]';

packages/utils/src/string.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isRegExp, isString } from './is';
1+
import { isRegExp, isString, isVueViewModel } from './is';
22

33
export { escapeStringForRegex } from './vendor/escapeStringForRegex';
44

@@ -76,7 +76,16 @@ export function safeJoin(input: any[], delimiter?: string): string {
7676
for (let i = 0; i < input.length; i++) {
7777
const value = input[i];
7878
try {
79-
output.push(String(value));
79+
// This is a hack to fix a Vue3-specific bug that causes an infinite loop of
80+
// console warnings. This happens when a Vue template is rendered with
81+
// an undeclared variable, which we try to stringify, ultimately causing
82+
// Vue to issue another warning which repeats indefinitely.
83+
// see: https://github.com/getsentry/sentry-javascript/pull/8981
84+
if (isVueViewModel(value)) {
85+
output.push('[VueViewModel]');
86+
} else {
87+
output.push(String(value));
88+
}
8089
} catch (e) {
8190
output.push('[value cannot be serialized]');
8291
}

packages/utils/test/is.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isNaN,
88
isPrimitive,
99
isThenable,
10+
isVueViewModel,
1011
} from '../src/is';
1112
import { supportsDOMError, supportsDOMException, supportsErrorEvent } from '../src/supports';
1213
import { resolvedSyncPromise } from '../src/syncpromise';
@@ -134,3 +135,12 @@ describe('isNaN()', () => {
134135
expect(isNaN(new Date())).toEqual(false);
135136
});
136137
});
138+
139+
describe('isVueViewModel()', () => {
140+
test('should work as advertised', () => {
141+
expect(isVueViewModel({ _isVue: true })).toEqual(true);
142+
expect(isVueViewModel({ __isVue: true })).toEqual(true);
143+
144+
expect(isVueViewModel({ foo: true })).toEqual(false);
145+
});
146+
});

packages/utils/test/normalize.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,17 @@ describe('normalize()', () => {
476476
foo: '[SyntheticEvent]',
477477
});
478478
});
479+
480+
test('known classes like `VueViewModel`', () => {
481+
const obj = {
482+
foo: {
483+
_isVue: true,
484+
},
485+
};
486+
expect(normalize(obj)).toEqual({
487+
foo: '[VueViewModel]',
488+
});
489+
});
479490
});
480491

481492
describe('can limit object to depth', () => {
@@ -618,6 +629,24 @@ describe('normalize()', () => {
618629
});
619630
});
620631

632+
test('normalizes value on every iteration of decycle and takes care of things like `VueViewModel`', () => {
633+
const obj = {
634+
foo: {
635+
_isVue: true,
636+
},
637+
baz: NaN,
638+
qux: function qux(): void {
639+
/* no-empty */
640+
},
641+
};
642+
const result = normalize(obj);
643+
expect(result).toEqual({
644+
foo: '[VueViewModel]',
645+
baz: '[NaN]',
646+
qux: '[Function: qux]',
647+
});
648+
});
649+
621650
describe('skips normalizing objects marked with a non-enumerable property __sentry_skip_normalization__', () => {
622651
test('by leaving non-serializable values intact', () => {
623652
const someFun = () => undefined;

0 commit comments

Comments
 (0)