From 90ee2a419634fcdd334c2aba9b6b27f00cc0fccc Mon Sep 17 00:00:00 2001 From: cyx <30902641+Duncanxyz@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:29:51 +0800 Subject: [PATCH] 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 --- .../browser/src/integrations/breadcrumbs.ts | 12 -------- packages/utils/src/is.ts | 17 +++++++++++ packages/utils/src/normalize.ts | 6 +++- packages/utils/src/string.ts | 13 +++++++-- packages/utils/test/is.test.ts | 10 +++++++ packages/utils/test/normalize.test.ts | 29 +++++++++++++++++++ 6 files changed, 72 insertions(+), 15 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index e41bedc8bf1c..f71361b7d96e 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -183,18 +183,6 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa * Creates breadcrumbs from console API calls */ function _consoleBreadcrumb(handlerData: HandlerData & { args: unknown[]; level: string }): void { - // This is a hack to fix a Vue3-specific bug that causes an infinite loop of - // console warnings. This happens when a Vue template is rendered with - // an undeclared variable, which we try to stringify, ultimately causing - // Vue to issue another warning which repeats indefinitely. - // see: https://github.com/getsentry/sentry-javascript/pull/6010 - // see: https://github.com/getsentry/sentry-javascript/issues/5916 - for (let i = 0; i < handlerData.args.length; i++) { - if (handlerData.args[i] === 'ref=Ref<') { - handlerData.args[i + 1] = 'viewRef'; - break; - } - } const breadcrumb = { category: 'console', data: { diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 350826cb567c..61a94053a265 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -179,3 +179,20 @@ export function isInstanceOf(wat: any, base: any): boolean { return false; } } + +interface VueViewModel { + // Vue3 + __isVue?: boolean; + // Vue2 + _isVue?: boolean; +} +/** + * Checks whether given value's type is a Vue ViewModel. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +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)); +} diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index 1f200640dbb1..7c1adaa32ccc 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -1,6 +1,6 @@ import type { Primitive } from '@sentry/types'; -import { isNaN, isSyntheticEvent } from './is'; +import { isNaN, isSyntheticEvent, isVueViewModel } from './is'; import type { MemoFunc } from './memo'; import { memoBuilder } from './memo'; import { convertToPlainObject } from './object'; @@ -214,6 +214,10 @@ function stringifyValue( return '[Document]'; } + if (isVueViewModel(value)) { + return '[VueViewModel]'; + } + // React's SyntheticEvent thingy if (isSyntheticEvent(value)) { return '[SyntheticEvent]'; diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 73efe8ef625e..743b25c3deef 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -1,4 +1,4 @@ -import { isRegExp, isString } from './is'; +import { isRegExp, isString, isVueViewModel } from './is'; export { escapeStringForRegex } from './vendor/escapeStringForRegex'; @@ -76,7 +76,16 @@ export function safeJoin(input: any[], delimiter?: string): string { for (let i = 0; i < input.length; i++) { const value = input[i]; try { - output.push(String(value)); + // This is a hack to fix a Vue3-specific bug that causes an infinite loop of + // console warnings. This happens when a Vue template is rendered with + // an undeclared variable, which we try to stringify, ultimately causing + // Vue to issue another warning which repeats indefinitely. + // see: https://github.com/getsentry/sentry-javascript/pull/8981 + if (isVueViewModel(value)) { + output.push('[VueViewModel]'); + } else { + output.push(String(value)); + } } catch (e) { output.push('[value cannot be serialized]'); } diff --git a/packages/utils/test/is.test.ts b/packages/utils/test/is.test.ts index 6a4172e4a595..da9d77a44fde 100644 --- a/packages/utils/test/is.test.ts +++ b/packages/utils/test/is.test.ts @@ -7,6 +7,7 @@ import { isNaN, isPrimitive, isThenable, + isVueViewModel, } from '../src/is'; import { supportsDOMError, supportsDOMException, supportsErrorEvent } from '../src/supports'; import { resolvedSyncPromise } from '../src/syncpromise'; @@ -134,3 +135,12 @@ describe('isNaN()', () => { expect(isNaN(new Date())).toEqual(false); }); }); + +describe('isVueViewModel()', () => { + test('should work as advertised', () => { + expect(isVueViewModel({ _isVue: true })).toEqual(true); + expect(isVueViewModel({ __isVue: true })).toEqual(true); + + expect(isVueViewModel({ foo: true })).toEqual(false); + }); +}); diff --git a/packages/utils/test/normalize.test.ts b/packages/utils/test/normalize.test.ts index d13631d43a9a..fda1798c3792 100644 --- a/packages/utils/test/normalize.test.ts +++ b/packages/utils/test/normalize.test.ts @@ -476,6 +476,17 @@ describe('normalize()', () => { foo: '[SyntheticEvent]', }); }); + + test('known classes like `VueViewModel`', () => { + const obj = { + foo: { + _isVue: true, + }, + }; + expect(normalize(obj)).toEqual({ + foo: '[VueViewModel]', + }); + }); }); describe('can limit object to depth', () => { @@ -618,6 +629,24 @@ describe('normalize()', () => { }); }); + test('normalizes value on every iteration of decycle and takes care of things like `VueViewModel`', () => { + const obj = { + foo: { + _isVue: true, + }, + baz: NaN, + qux: function qux(): void { + /* no-empty */ + }, + }; + const result = normalize(obj); + expect(result).toEqual({ + foo: '[VueViewModel]', + baz: '[NaN]', + qux: '[Function: qux]', + }); + }); + describe('skips normalizing objects marked with a non-enumerable property __sentry_skip_normalization__', () => { test('by leaving non-serializable values intact', () => { const someFun = () => undefined;