Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions dev-packages/e2e-tests/test-applications/vue-3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"test:assert": "pnpm test:print-version && playwright test",
"test:build-canary": "pnpm install && pnpm test:install-canary && pnpm build",
"test:build-latest": "pnpm install && pnpm add vue@latest && pnpm build",
"test:build-no-options-api": "pnpm install && VUE_OPTIONS_API=false pnpm build",
"test:assert-no-options-api": "VUE_OPTIONS_API=false pnpm test:assert",
"test:install-canary": "pnpm add vue@$(git ls-remote --tags --sort='v:refname' https://github.com/vuejs/core.git | tail -n1 | awk -F'/' '{print $NF}')",
"test:print-version": "node -p \"'Vue version: ' + require('vue/package.json').version\""
},
Expand Down Expand Up @@ -52,6 +54,11 @@
{
"build-command": "pnpm test:build-canary",
"label": "vue-3 (canary)"
},
{
"build-command": "pnpm test:build-no-options-api",
"assert-command": "pnpm test:assert-no-options-api",
"label": "vue-3 (no Options API)"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

// Set by the `assert-command` of the `vue-3 (no Options API)` variant
const OPTIONS_API_DISABLED = process.env.VUE_OPTIONS_API === 'false';

test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
const transactionPromise = waitForTransaction('vue-3', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
Expand Down Expand Up @@ -135,7 +138,10 @@ test('sends a pageload transaction with a route name as transaction name if avai
});
});

test('sends a lifecycle span for each tracked components', async ({ page }) => {
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');

const transactionPromise = waitForTransaction('vue-3', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});
Expand All @@ -144,79 +150,61 @@ test('sends a lifecycle span for each tracked components', async ({ page }) => {

const rootSpan = await transactionPromise;

expect(rootSpan).toMatchObject({
contexts: {
trace: {
data: {
'sentry.source': 'route',
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'url.template': '/components',
'url.path': '/components',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/components$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
},
const uiSpans = (rootSpan.spans || []).filter(span => span.origin === 'auto.ui.vue');
const uiSpanDescriptions = uiSpans.map(span => span.description).sort();

expect(uiSpanDescriptions).toEqual([
'Application Render',
'Vue <ComponentMainView>',
'Vue <ComponentOneView>',
'Vue <Root>',
]);

// enabled by default
const applicationRenderSpan = uiSpans.find(span => span.description === 'Application Render');
expect(applicationRenderSpan).toMatchObject({
data: {
'sentry.op': 'ui.render',
'sentry.origin': 'auto.ui.vue',
},
spans: expect.arrayContaining([
// enabled by default
expect.objectContaining({
data: {
'sentry.op': 'ui.render',
'sentry.origin': 'auto.ui.vue',
},
description: 'Application Render',
op: 'ui.render',
origin: 'auto.ui.vue',
}),
// enabled by default
expect.objectContaining({
data: {
'sentry.op': 'ui.mount',
'sentry.origin': 'auto.ui.vue',
},
description: 'Vue <Root>',
op: 'ui.mount',
origin: 'auto.ui.vue',
}),
op: 'ui.render',
origin: 'auto.ui.vue',
});

// without `<>`
expect.objectContaining({
data: {
'sentry.op': 'ui.mount',
'sentry.origin': 'auto.ui.vue',
},
description: 'Vue <ComponentMainView>',
op: 'ui.mount',
origin: 'auto.ui.vue',
}),
// enabled by default
const rootComponentSpan = uiSpans.find(span => span.description === 'Vue <Root>');
expect(rootComponentSpan).toMatchObject({
data: {
'sentry.op': 'ui.mount',
'sentry.origin': 'auto.ui.vue',
},
op: 'ui.mount',
origin: 'auto.ui.vue',
});

// with `<>`
expect.objectContaining({
data: {
'sentry.op': 'ui.mount',
'sentry.origin': 'auto.ui.vue',
},
description: 'Vue <ComponentOneView>',
op: 'ui.mount',
origin: 'auto.ui.vue',
}),
// without `<>`
const componentMainViewSpan = uiSpans.find(span => span.description === 'Vue <ComponentMainView>');
expect(componentMainViewSpan).toMatchObject({
data: {
'sentry.op': 'ui.mount',
'sentry.origin': 'auto.ui.vue',
},
op: 'ui.mount',
origin: 'auto.ui.vue',
});

// not tracked
expect.not.objectContaining({
data: {
'sentry.op': 'ui.mount',
'sentry.origin': 'auto.ui.vue',
},
description: 'Vue <ComponentTwoView>',
op: 'ui.mount',
origin: 'auto.ui.vue',
}),
]),
transaction: '/components',
transaction_info: {
source: 'route',
// with `<>`
const componentOneViewSpan = uiSpans.find(span => span.description === 'Vue <ComponentOneView>');
expect(componentOneViewSpan).toMatchObject({
data: {
'sentry.op': 'ui.mount',
'sentry.origin': 'auto.ui.vue',
},
op: 'ui.mount',
origin: 'auto.ui.vue',
});

// `ComponentTwoView` renders on this route but is absent from `trackComponents`
// not tracked
expect(uiSpanDescriptions).not.toContain('Vue <ComponentTwoView>');
});
6 changes: 6 additions & 0 deletions dev-packages/e2e-tests/test-applications/vue-3/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import { defineConfig } from 'vite';

// Nuxt 5 disables the Options API by default (users can disable it too for smaller bundle size)
const optionsApi = process.env.VUE_OPTIONS_API === 'false' ? 'false' : 'true';

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx()],
define: {
Comment on lines 11 to +13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The __VUE_OPTIONS_API__ flag is set to the string 'false', which is truthy in JavaScript, preventing the Vue Options API from being disabled as intended.
Severity: MEDIUM

Suggested Fix

The optionsApi variable should be a boolean, not a string. Change const optionsApi = process.env.VUE_OPTIONS_API === 'false' ? 'false' : 'true'; to const optionsApi = process.env.VUE_OPTIONS_API !== 'false';. This ensures a proper boolean value is passed to Vite's define configuration.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: dev-packages/e2e-tests/test-applications/vue-3/vite.config.ts#L11-L13

Potential issue: In `vite.config.ts`, the `__VUE_OPTIONS_API__` flag is set to the
string `'false'` instead of the boolean `false`. Vite's `define` feature injects this as
a raw string literal. In JavaScript, the non-empty string `'false'` is truthy, so the
conditional check `if (__VUE_OPTIONS_API__)` in Vue's source evaluates to true. This
prevents the Options API from being disabled when intended. As a result, an E2E test
that is expected to fail (`test.fail`) when the Options API is disabled will instead
pass, masking the configuration bug.

Did we get this right? 👍 / 👎 to inform future reviews.

__VUE_OPTIONS_API__: optionsApi,
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
Expand Down
Loading
Loading