diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.client.test.ts index 5935f1ef1c3c..f8af579792eb 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/tracing.client.test.ts @@ -46,10 +46,9 @@ test('sends a navigation root span with a parameterized URL', async ({ page }) = }); test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => { - // Nuxt 5 disables the Options API by default (nuxt/nuxt#35791), which turns `app.mixin()` into a - // no-op, and that mixin is where the SDK creates every UI span. Flips to passing once component - // tracking works without it. - test.fail(true, 'Vue tracing is registered through app.mixin(), which needs the Options API'); + // Nuxt 5 disables the Options API by default (nuxt/nuxt#35791), and component spans only exist + // through `app.mixin()`, which that flag turns into a no-op. `vue: { optionsApi: true }` re-enables it. + test.fail(true, 'Component tracking (`trackComponents`) needs the Options API'); const spansPromise = collectStreamedSpansUntilSegment( 'nuxt-5', @@ -77,10 +76,6 @@ test('sends component tracking spans when `trackComponents` is enabled', async ( }); test('sends an application render span and a root component span on pageload', async ({ page }) => { - // Same root cause as above: no Options API, no `app.mixin()`, no UI spans. Flips to passing once - // the root spans stop depending on the mixin. - test.fail(true, 'Vue tracing is registered through app.mixin(), which needs the Options API'); - const spansPromise = collectStreamedSpansUntilSegment( 'nuxt-5', span => span.name === '/client-error' && getSpanOp(span) === 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts index c81a662c61e2..030d75dffb23 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts @@ -1,4 +1,5 @@ import { createRouter, createWebHistory } from 'vue-router'; +import DelayedView from '../views/DelayedView.vue'; import HomeView from '../views/HomeView.vue'; const router = createRouter({ @@ -8,6 +9,11 @@ const router = createRouter({ path: '/', component: HomeView, }, + { + // Loaded eagerly so the only async step on this route is the view's delayed child component. + path: '/delayed', + component: DelayedView, + }, { path: '/about', name: 'AboutView', diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/DelayedView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/DelayedView.vue new file mode 100644 index 000000000000..c5dad2ae5e76 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/DelayedView.vue @@ -0,0 +1,26 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts index ac49083727e8..d0df920e9fd0 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts @@ -4,6 +4,9 @@ import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-in // Set by the `assert-command` of the `vue-3 (no Options API)` variant const OPTIONS_API_DISABLED = process.env.VUE_OPTIONS_API === 'false'; +// Must stay in sync with `ASYNC_CHILD_DELAY_MS` in `src/views/DelayedView.vue`. +const ASYNC_CHILD_DELAY_S = 0.3; + test('sends a pageload span with a parameterized URL', async ({ page }) => { const pageloadSpanPromise = waitForStreamedSpan('vue-3', span => { return span.is_segment && getSpanOp(span) === 'pageload'; @@ -106,27 +109,25 @@ test('sends a pageload span with a route name as span name if available', async }); }); -// The root component is always tracked, even when the route's view is missing from `trackComponents`. -// The root itself mounts synchronously on both routes (`app.mount()` does not wait for the router). -// What differs on `/components` is that its view arrives through a dynamic `import()`, so the -// async-loaded components must join the same pageload while `Application Render` is still open. +// The root component is always tracked, and the `app.mount()` wrap records the root spans when +// the Options API is disabled, so both variants expect them. The tracked component spans on +// `/components` still need the Options API, so the disabled variant expects the root spans only. [ { route: '/', routeDescription: 'a route with a synchronously mounted component', // `HomeView` is missing from `trackComponents`, so the root spans are the only UI spans. - expectedUiSpanNames: ['Application Render', 'Vue '], + expectedUiSpanNames: ['Application Render', 'Vue '].sort(), }, { route: '/components', routeDescription: 'a route with an async component', - expectedUiSpanNames: ['Application Render', 'Vue ', 'Vue ', 'Vue '], + expectedUiSpanNames: OPTIONS_API_DISABLED + ? ['Application Render', 'Vue '].sort() + : ['Application Render', 'Vue ', 'Vue ', 'Vue '].sort(), }, ].forEach(({ route, routeDescription, expectedUiSpanNames }) => { test(`sends an application render span and a root component span on ${routeDescription}`, async ({ page }) => { - // Vue compiles `app.mixin()` down to a no-op when the Options API is disabled, so the SDK creates no UI spans at all. - test.fail(OPTIONS_API_DISABLED, 'Vue tracing is registered through app.mixin(), which needs the Options API'); - const spansPromise = collectStreamedSpans('vue-3', spans => { return ( spans.some( @@ -163,9 +164,38 @@ test('sends a pageload span with a route name as span name if available', async }); }); +// True on both variants: the mixin arms one debounce timer per component (`tracing.ts`), so a +// late child never clears the root's earlier timer and the span ends at the root's mount. The +// `app.mount()` wrap only observes the root, so it matches. +test('ends the application render span before a delayed async component mounts', async ({ page }) => { + const spansPromise = collectStreamedSpans('vue-3', spans => + spans.some( + span => span.is_segment && getSpanOp(span) === 'pageload' && span.attributes['url.path']?.value === '/delayed', + ), + ); + + await page.goto('/delayed'); + // Proves the child really mounted after its delay; the duration assertion relies on it. + await expect(page.locator('#delayed-child')).toBeVisible(); + + const spans = await spansPromise; + const uiSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.ui.vue'); + + // Neither `DelayedView` nor its child is in `trackComponents`, so both variants expect the same set. + expect(uiSpans.map(span => span.name).sort()).toEqual(['Application Render', 'Vue ']); + + const applicationRenderSpan = uiSpans.find(span => span.name === 'Application Render'); + expect(applicationRenderSpan?.start_timestamp).toEqual(expect.any(Number)); + expect(applicationRenderSpan?.end_timestamp).toEqual(expect.any(Number)); + + const duration = (applicationRenderSpan?.end_timestamp ?? 0) - (applicationRenderSpan?.start_timestamp ?? 0); + expect(duration).toBeLessThan(ASYNC_CHILD_DELAY_S); +}); + test('sends a lifecycle span for the root and for each tracked component only', async ({ page }) => { - // Vue compiles `app.mixin()` down to a no-op when the Options API is disabled, so the SDK creates no UI spans at all. - test.fail(OPTIONS_API_DISABLED, 'Vue tracing is registered through app.mixin(), which needs the Options API'); + // The root spans survive through the `app.mount()` wrap, but the tracked component spans asserted + // below still come from `app.mixin()`, which is a no-op when the Options API is disabled. + test.fail(OPTIONS_API_DISABLED, 'Component tracking (`trackComponents`) needs the Options API'); const expectedUiSpanNames = ['Application Render', 'Vue ', 'Vue ', 'Vue ']; diff --git a/packages/vue/src/constants.ts b/packages/vue/src/constants.ts index 50aa82f77885..fc62410bd823 100644 --- a/packages/vue/src/constants.ts +++ b/packages/vue/src/constants.ts @@ -1,3 +1,6 @@ import type { Operation } from './types'; export const DEFAULT_HOOKS: Operation[] = ['activate', 'mount']; + +/** How long the root render span waits for further render activity before it ends. */ +export const DEFAULT_ROOT_SPAN_TIMEOUT = 2000; diff --git a/packages/vue/src/integration.ts b/packages/vue/src/integration.ts index 856fdcf56d9b..8bdac3d5db44 100644 --- a/packages/vue/src/integration.ts +++ b/packages/vue/src/integration.ts @@ -1,9 +1,10 @@ import { consoleSandbox, defineIntegration, GLOBAL_OBJ, hasSpansEnabled } from '@sentry/core'; -import { DEFAULT_HOOKS } from './constants'; +import { DEFAULT_HOOKS, DEFAULT_ROOT_SPAN_TIMEOUT } from './constants'; import { DEBUG_BUILD } from './debug-build'; import { attachErrorHandler } from './errorhandler'; +import { instrumentAppMountWithoutMixin } from './rootInstrumentation'; import { createTracingMixins } from './tracing'; -import type { Options, Vue, VueOptions } from './types'; +import type { Options, TracingOptions, Vue, VueOptions } from './types'; const globalWithVue = GLOBAL_OBJ as typeof GLOBAL_OBJ & { Vue: Vue }; @@ -13,7 +14,7 @@ const DEFAULT_CONFIG: VueOptions = { attachErrorHandler: true, tracingOptions: { hooks: DEFAULT_HOOKS, - timeout: 2000, + timeout: DEFAULT_ROOT_SPAN_TIMEOUT, trackComponents: false, }, }; @@ -76,21 +77,35 @@ const vueInit = (app: Vue, options: Options): void => { if (hasSpansEnabled(options)) { const mixins = createTracingMixins(options.tracingOptions); app.mixin(mixins); - warnIfMixinWasDropped(app, mixins); + if (!mixinWasApplied(app, mixins)) { + instrumentAppMountWithoutMixin(app, mixins); + warnAboutLostComponentTracking(app, options.tracingOptions); + } } }; /** - * `app.mixin()` is a no-op when Options API is disabled (default in Nuxt 5). - * Without mixins (Options API) users lose every UI span (render, mount, etc.) - + * Reads back whether Vue accepted the mixin, because `app.mixin()` fails silently when the Options + * API is disabled (the Nuxt 5 default). A Vue 2 constructor has no `_context` and no Options API + * flag, so the mixin always applies there. + * * See: https://github.com/vuejs/core/blob/v3.5.41/packages/runtime-core/src/apiCreateApp.ts */ -function warnIfMixinWasDropped(app: Vue, mixin: unknown): void { - // Vue 2 has no `_context` and no Options API flag, so there is nothing to check. +function mixinWasApplied(app: Vue, mixin: unknown): boolean { const mixins = (app as Vue & { _context?: { mixins?: unknown[] } })._context?.mixins; + return !mixins || mixins.includes(mixin); +} + +/** + * Warns only when the dropped mixin loses component tracking the user opted into. The default + * spans still work through the `app.mount()` wrap, so a default config stays silent. + */ +function warnAboutLostComponentTracking(app: Vue, tracingOptions: Partial | undefined): void { + const trackComponents = tracingOptions?.trackComponents; + const losesComponentSpans = + trackComponents === true || (Array.isArray(trackComponents) && trackComponents.length > 0); - if (!mixins || mixins.includes(mixin)) { + if (!losesComponentSpans) { return; } @@ -103,7 +118,7 @@ function warnIfMixinWasDropped(app: Vue, mixin: unknown): void { consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( - `[@sentry/vue]: The Vue Options API is disabled (\`__VUE_OPTIONS_API__: false\`), so Sentry cannot record UI spans. You lose \`Application Render\` and the component mount, update and unmount spans. Errors, pageload spans and navigation spans still work. ${fix}`, + `[@sentry/vue]: The Vue Options API is disabled (\`__VUE_OPTIONS_API__: false\`). Sentry still records the \`Application Render\` and root component mount spans, but component tracking (\`trackComponents\`) needs the Options API. ${fix}`, ); }); } diff --git a/packages/vue/src/rootInstrumentation.ts b/packages/vue/src/rootInstrumentation.ts new file mode 100644 index 000000000000..4f0e30dd2b2c --- /dev/null +++ b/packages/vue/src/rootInstrumentation.ts @@ -0,0 +1,51 @@ +import type { Mixins, VueSentry } from './tracing'; +import type { Vue } from './types'; + +const instrumentedApps = new WeakSet(); + +/** + * The mixin hooks only check `$root === this` to detect the root component, so a self-referential + * stand-in works in place of the real instance, which does not exist yet at wrap time. + */ +function createRootViewModel(): VueSentry { + const vm: { $root?: unknown; $props: Record } = { $props: {} }; + vm.$root = vm; + return vm as unknown as VueSentry; +} + +/** + * Records the `Application Render` and root component mount spans by wrapping `app.mount()`, for + * builds where the Options API is compiled out and `app.mixin()` is a silent no-op (Nuxt 5 default). + * + * Vue runs all `mounted` hooks before `mount()` returns, so the wrap covers the same window as the + * mixin's root hooks. Late mounts extend neither path; the mixin's debounce timers are per component. + */ +export function instrumentAppMountWithoutMixin(app: Vue, mixins: Mixins): void { + // A second wrap would duplicate the root spans (e.g. user and Nuxt SDK both add the integration). + if (instrumentedApps.has(app)) { + return; + } + + const appWithMount = app as Vue & { mount?: (...args: unknown[]) => unknown }; + const originalMount = appWithMount.mount; + // Guards odd app-like objects; Vue 2 constructors lack `mount` but never get here (their `app.mixin()` works). + if (typeof originalMount !== 'function') { + return; + } + + const vm = createRootViewModel(); + instrumentedApps.add(app); + + // `createTracingMixins` always merges `DEFAULT_HOOKS`, so the `mount` pair exists. + const mountHooks = mixins as Partial void>>; + + appWithMount.mount = function (...args: unknown[]): unknown { + mountHooks.beforeMount?.call(vm); + try { + return originalMount.apply(this, args); + } finally { + // Also runs when mounting throws, so the started root component span always ends. + mountHooks.mounted?.call(vm); + } + }; +} diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 71b610a2e0ee..3939e4c174fc 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -3,7 +3,7 @@ import type { Span } from '@sentry/core'; import { debug, timestampInSeconds, uniq } from '@sentry/core'; import { SENTRY_OP } from '@sentry/conventions/attributes'; import { UI_MOUNT, UI_RENDER, UI_UNMOUNT, UI_UPDATE } from '@sentry/conventions/op'; -import { DEFAULT_HOOKS } from './constants'; +import { DEFAULT_HOOKS, DEFAULT_ROOT_SPAN_TIMEOUT } from './constants'; import { DEBUG_BUILD } from './debug-build'; import type { Hook, Operation, TracingOptions, ViewModel, Vue } from './types'; import { formatComponentName } from './vendor/components'; @@ -18,9 +18,9 @@ const VUE_OPERATION_TO_SPAN_OP: Record = { destroy: UI_UNMOUNT, }; -type Mixins = Parameters[0]; +export type Mixins = Parameters[0]; -interface VueSentry extends ViewModel { +export interface VueSentry extends ViewModel { readonly $root: VueSentry; $_sentryComponentSpans?: { [key: string]: Span | undefined; @@ -73,7 +73,7 @@ export const createTracingMixins = (options: Partial = {}): Mixi const mixins: Mixins = {}; - const rootComponentSpanFinalTimeout = options.timeout || 2000; + const rootComponentSpanFinalTimeout = options.timeout || DEFAULT_ROOT_SPAN_TIMEOUT; for (const operation of hooks) { // Retrieve corresponding hooks from Vue lifecycle. diff --git a/packages/vue/test/integration/mixinRegistration.test.ts b/packages/vue/test/integration/mixinRegistration.test.ts index bef3c07891db..d89d30f74fbb 100644 --- a/packages/vue/test/integration/mixinRegistration.test.ts +++ b/packages/vue/test/integration/mixinRegistration.test.ts @@ -5,8 +5,8 @@ import { spanToJSON } from '@sentry/core'; import type { MockInstance } from 'vitest'; import { afterEach, beforeEach, describe, expect, it as baseIt, vi } from 'vitest'; -import type { App, Ref } from 'vue'; -import { createApp, h, nextTick, ref } from 'vue'; +import type { App, Component, Ref } from 'vue'; +import { createApp, defineAsyncComponent, h, nextTick, ref } from 'vue'; import * as Sentry from '../../src'; import type { Options, TracingOptions } from '../../src/types'; @@ -43,6 +43,19 @@ function createReactiveTestApp(): { app: App; message: Ref } { return { app, message }; } +/** An app whose only child mounts when the returned `resolveChild` is called. */ +function createAppWithDeferredChild(): { app: App; resolveChild: () => void } { + let resolve: (component: Component) => void = () => {}; + const asyncChild = defineAsyncComponent( + () => + new Promise(resolveLoader => { + resolve = resolveLoader; + }), + ); + const app = createApp({ name: 'RootComponent', render: () => h('div', [h(asyncChild)]) }); + return { app, resolveChild: () => resolve({ render: () => h('p', 'child') }) }; +} + /** Reads the mixins Vue accepted. `app.mixin()` is a silent no-op without the Options API. */ function getRegisteredMixins(app: App): unknown[] { return (app as unknown as { _context: { mixins: unknown[] } })._context.mixins; @@ -183,8 +196,7 @@ describe('tracing mixin span creation', () => { }); // The mixin always tracks the root component: `isRootComponent || …` short-circuits before the - // `trackComponents` filter runs. The next four tests record what that means for each hook, so a - // mixin replacement can prove which parts it keeps. + // `trackComponents` filter runs. The following tests record what that means for each hook. it('tracks the root component for update hooks without trackComponents', async ({ uiSpans, initSentry }) => { const { app, message } = createReactiveTestApp(); @@ -206,9 +218,8 @@ describe('tracing mixin span creation', () => { ]); }); - // `beforeCreate` fires very early in `app.mount()`, but the mixin creates the root render span - // first, in the same handler. So the `create` span has a parent and is emitted, as `ui.mount`, - // which is the op the `create` operation maps to. + // The mixin creates the root render span in the same `beforeCreate` handler, so the `create` + // span has a parent even this early in `app.mount()`. The `create` operation maps to `ui.mount`. it('tracks the root component for create hooks without trackComponents', ({ app, uiSpans, initSentry }) => { initSentry({ tracing: { hooks: ['create'] } }); @@ -248,6 +259,21 @@ describe('tracing mixin span creation', () => { ]); }); + // `maybeEndRootComponentSpan` arms one debounce timer per component, so a late child never + // clears the root's earlier timer, and the root's timer ends the span first. The twin test in + // the disabled describe below proves the `app.mount()` wrap matches. + it('ends the root render span before a deferred child mounts', ({ uiSpans, initSentry }) => { + const { app } = createAppWithDeferredChild(); + initSentry({ sdk: { app } }); + + mountUnderActiveSpan(app); + + expect(uiSpans).toEqual([ + { name: 'Vue ', op: UI_MOUNT_SPAN_OP }, + { name: 'Application Render', op: UI_RENDER_SPAN_OP }, + ]); + }); + // Vue 3 compiles `app.mixin()` down to a no-op returning the app when the `__VUE_OPTIONS_API__` // build flag is `false`. Nuxt 5 sets that flag by default (nuxt/nuxt#35791), so this stub matches // what those users run. The real build is covered by the `vue-3 (no Options API)` e2e variant. @@ -256,9 +282,7 @@ describe('tracing mixin span creation', () => { app.mixin = () => app; } - // Drop `.fails` once tracing no longer depends on `app.mixin()`. Vitest then reports this as a - // failure, which is the signal to delete the modifier. - it.fails('creates the same UI spans as with the Options API enabled', ({ app, uiSpans, initSentry }) => { + it('creates the same UI spans as with the Options API enabled', ({ app, uiSpans, initSentry }) => { disableOptionsApi(app); initSentry(); @@ -279,6 +303,48 @@ describe('tracing mixin span creation', () => { expect(container.innerHTML).toBe('

child

'); }); + // Users rely on `const instance = app.mount(container)`; the wrap must not swallow it. + it('returns the root instance from the wrapped mount', ({ app, initSentry }) => { + disableOptionsApi(app); + initSentry(); + const container = document.createElement('div'); + + const rootInstance = app.mount(container); + + expect(rootInstance.$el).toBe(container.firstElementChild); + }); + + // Matches the mixin-path twin above: the mixin never waited for late children either. + it('ends the root render span before a deferred child mounts', ({ uiSpans, initSentry }) => { + const { app } = createAppWithDeferredChild(); + disableOptionsApi(app); + initSentry({ sdk: { app } }); + + mountUnderActiveSpan(app); + + expect(uiSpans).toEqual([ + { name: 'Vue ', op: UI_MOUNT_SPAN_OP }, + { name: 'Application Render', op: UI_RENDER_SPAN_OP }, + ]); + }); + + it('records no further spans when a child mounts after the root span ended', async ({ uiSpans, initSentry }) => { + const { app, resolveChild } = createAppWithDeferredChild(); + disableOptionsApi(app); + initSentry({ sdk: { app } }); + const container = mountUnderActiveSpan(app); + + resolveChild(); + // Async component resolution hops through several real microtasks before the re-render + // flush, so poll until the child rendered; `vi.waitFor` advances the fake timers itself. + await vi.waitFor(() => expect(container.innerHTML).toBe('

child

')); + vi.advanceTimersByTime(ROOT_SPAN_TIMEOUT_MS + 1); + expect(uiSpans).toEqual([ + { name: 'Vue ', op: UI_MOUNT_SPAN_OP }, + { name: 'Application Render', op: UI_RENDER_SPAN_OP }, + ]); + }); + it('attaches the Vue error handler', ({ app, initSentry }) => { disableOptionsApi(app); @@ -302,18 +368,49 @@ describe('Options API detection guard', () => { consoleWarn.mockRestore(); }); - it('warns when the app dropped the tracing mixin', ({ app, initSentry }) => { + // The default spans survive without the mixin (see the fallback tests above), so warning about a + // default config would be noise on every Nuxt 5 app. + it('does not warn with default options when the app dropped the tracing mixin', ({ app, initSentry }) => { app.mixin = () => app; initSentry(); + expect(consoleWarn).not.toHaveBeenCalledWith(OPTIONS_API_WARNING); + }); + + it('warns when trackComponents is enabled and the app dropped the tracing mixin', ({ app, initSentry }) => { + app.mixin = () => app; + + initSentry({ tracing: { trackComponents: true } }); + expect(consoleWarn).toHaveBeenCalledWith(OPTIONS_API_WARNING); }); + it('warns when a tracked component list is configured and the app dropped the tracing mixin', ({ + app, + initSentry, + }) => { + app.mixin = () => app; + + initSentry({ tracing: { trackComponents: ['ChildComponent'] } }); + + expect(consoleWarn).toHaveBeenCalledWith(OPTIONS_API_WARNING); + }); + + // A custom `hooks` config also degrades without the mixin, but the fallback still covers + // `mount` for the root, so only `trackComponents` is worth a warning. + it('does not warn when only hooks are configured', ({ app, initSentry }) => { + app.mixin = () => app; + + initSentry({ tracing: { hooks: ['update'] } }); + + expect(consoleWarn).not.toHaveBeenCalledWith(OPTIONS_API_WARNING); + }); + it('points a plain Vue app at its bundler config', ({ app, initSentry }) => { app.mixin = () => app; - initSentry(); + initSentry({ tracing: { trackComponents: true } }); expect(consoleWarn).toHaveBeenCalledWith(expect.stringContaining('`define` config of your bundler')); expect(consoleWarn).not.toHaveBeenCalledWith(expect.stringContaining('nuxt.config.ts')); @@ -324,13 +421,13 @@ describe('Options API detection guard', () => { app.mixin = () => app; Object.defineProperty(app, '$nuxt', { get: () => ({}) }); - initSentry(); + initSentry({ tracing: { trackComponents: true } }); expect(consoleWarn).toHaveBeenCalledWith(expect.stringContaining('`vue: { optionsApi: true }`')); }); it('does not warn when the app accepted the tracing mixin', ({ initSentry }) => { - initSentry(); + initSentry({ tracing: { trackComponents: true } }); expect(consoleWarn).not.toHaveBeenCalledWith(OPTIONS_API_WARNING); }); @@ -338,7 +435,7 @@ describe('Options API detection guard', () => { it('does not warn when tracing is disabled, because no mixin is registered', ({ app, initSentry }) => { app.mixin = () => app; - initSentry({ sdk: { tracesSampleRate: undefined } }); + initSentry({ tracing: { trackComponents: true }, sdk: { tracesSampleRate: undefined } }); expect(consoleWarn).not.toHaveBeenCalledWith(OPTIONS_API_WARNING); }); @@ -347,7 +444,7 @@ describe('Options API detection guard', () => { it('does not warn for a Vue 2 constructor', ({ initSentry }) => { const vue2Constructor = { config: {}, mixin: () => {} }; - initSentry({ sdk: { app: undefined, Vue: vue2Constructor } }); + initSentry({ tracing: { trackComponents: true }, sdk: { app: undefined, Vue: vue2Constructor } }); expect(consoleWarn).not.toHaveBeenCalledWith(OPTIONS_API_WARNING); });