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
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ declare module '@tanstack/vue-router' {
const app = createApp(RouterProvider, { router });

Sentry.init({
traceLifecycle: 'static',
app,
dsn: import.meta.env.PUBLIC_E2E_TEST_DSN,
debug: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,253 +1,204 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';

test('sends pageload transaction with web vitals measurements', async ({ page }) => {
const transactionPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
test('sends a pageload span with a parameterized URL', async ({ page }) => {
const pageloadSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return span.is_segment && getSpanOp(span) === 'pageload';
});

await page.goto(`/`);
await page.goto(`/posts/456`);

const transaction = await transactionPromise;

expect(transaction).toMatchObject({
contexts: {
trace: {
op: 'pageload',
origin: 'auto.pageload.vue.tanstack_router',
data: {
'sentry.origin': 'auto.pageload.vue.tanstack_router',
'sentry.segment.name.source': 'route',
'sentry.op': 'pageload',
'url.template': '/',
'url.path': '/',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/),
},
},
const pageloadSpan = await pageloadSpanPromise;

expect(pageloadSpan).toMatchObject({
name: '/posts/$postId',
is_segment: true,
attributes: {
'sentry.segment.name.source': { type: 'string', value: 'route' },
'sentry.origin': { type: 'string', value: 'auto.pageload.vue.tanstack_router' },
'sentry.op': { type: 'string', value: 'pageload' },
'url.path.parameter.postId': { type: 'string', value: '456' },
'url.template': { type: 'string', value: '/posts/$postId' },
'url.path': { type: 'string', value: '/posts/456' },
'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/456$/) },
},
transaction: '/',
transaction_info: {
source: 'route',
},
measurements: expect.objectContaining({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Web vitals measurements live on transaction events, not streamed spans, so those asserts are dropped rather than rewritten

Partially true. Web vitals are no longer collected on segment spans. But they are sent as separate spans. Can you check if we have other tests for Vue that directly assert on web vitals? If not, maybe we can assert on the web vital spans here. Or in a separate test in this app. Happy to leave it up to you.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Checked! no other Vue app asserts web vitals, only the test this PR deleted.
Added them back in 737e7e4

ttfb: expect.objectContaining({
value: expect.any(Number),
unit: 'millisecond',
}),
lcp: expect.objectContaining({
value: expect.any(Number),
unit: 'millisecond',
}),
fp: expect.objectContaining({
value: expect.any(Number),
unit: 'millisecond',
}),
fcp: expect.objectContaining({
value: expect.any(Number),
unit: 'millisecond',
}),
}),
});
});

test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
const transactionPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
test('sends a pageload span for the root route with web vital attributes and a standalone LCP span', async ({
page,
}) => {
const pageloadSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return span.is_segment && getSpanOp(span) === 'pageload';
});

await page.goto(`/posts/456`);
const lcpSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return getSpanOp(span) === 'ui.webvital.lcp';
});

const rootSpan = await transactionPromise;

expect(rootSpan).toMatchObject({
contexts: {
trace: {
data: {
'sentry.segment.name.source': 'route',
'sentry.origin': 'auto.pageload.vue.tanstack_router',
'sentry.op': 'pageload',
'url.path.parameter.postId': '456',
'url.template': '/posts/$postId',
'url.path': '/posts/456',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/456$/),
},
op: 'pageload',
origin: 'auto.pageload.vue.tanstack_router',
},
},
transaction: '/posts/$postId',
transaction_info: {
source: 'route',
await page.goto(`/`);

const pageloadSpan = await pageloadSpanPromise;

// LCP is only reported once the page is hidden or a navigation happens
await page.evaluate(() => {
Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true });
document.dispatchEvent(new Event('visibilitychange'));
});

const lcpSpan = await lcpSpanPromise;

const webVitalNumber = { value: expect.any(Number), type: expect.stringMatching(/^(integer|double)$/) };

expect(pageloadSpan).toMatchObject({
name: '/',
is_segment: true,
attributes: {
'sentry.op': { type: 'string', value: 'pageload' },
'sentry.origin': { type: 'string', value: 'auto.pageload.vue.tanstack_router' },
'sentry.segment.name.source': { type: 'string', value: 'route' },
'url.template': { type: 'string', value: '/' },
'url.path': { type: 'string', value: '/' },
'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/) },
'browser.web_vital.ttfb.value': webVitalNumber,
'browser.web_vital.fp.value': webVitalNumber,
'browser.web_vital.fcp.value': webVitalNumber,
},
});

expect(lcpSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'ui.webvital.lcp' },
'sentry.origin': { type: 'string', value: 'auto.http.browser.lcp' },
'sentry.pageload.span_id': { type: 'string', value: pageloadSpan.span_id },
'browser.web_vital.lcp.value': webVitalNumber,
});
});

test('sends a navigation transaction with a parameterized URL', async ({ page }) => {
const pageloadTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
test('sends a navigation span with a parameterized URL', async ({ page }) => {
const pageloadSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return span.is_segment && getSpanOp(span) === 'pageload';
});

const navigationTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
return (
!!transactionEvent?.transaction &&
transactionEvent.transaction === '/posts/$postId' &&
transactionEvent.contexts?.trace?.op === 'navigation'
);
const navigationSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return span.is_segment && getSpanOp(span) === 'navigation' && span.name === '/posts/$postId';
});

await page.goto(`/`);
await pageloadTxnPromise;
await pageloadSpanPromise;

await page.waitForTimeout(5000);

await page.locator('#nav-link').click();

const navigationTxn = await navigationTxnPromise;

expect(navigationTxn).toMatchObject({
contexts: {
trace: {
data: {
'sentry.segment.name.source': 'route',
'sentry.origin': 'auto.navigation.vue.tanstack_router',
'sentry.op': 'navigation',
'url.path.parameter.postId': '2',
'url.template': '/posts/$postId',
'url.path': '/posts/2',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/),
},
op: 'navigation',
origin: 'auto.navigation.vue.tanstack_router',
},
},
transaction: '/posts/$postId',
transaction_info: {
source: 'route',
const navigationSpan = await navigationSpanPromise;

expect(navigationSpan).toMatchObject({
name: '/posts/$postId',
is_segment: true,
attributes: {
'sentry.segment.name.source': { type: 'string', value: 'route' },
'sentry.origin': { type: 'string', value: 'auto.navigation.vue.tanstack_router' },
'sentry.op': { type: 'string', value: 'navigation' },
'url.path.parameter.postId': { type: 'string', value: '2' },
'url.template': { type: 'string', value: '/posts/$postId' },
'url.path': { type: 'string', value: '/posts/2' },
'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/) },
},
});
});

test('sends a pageload transaction named after the resolved route when a redirect is thrown on initial load', async ({
test('sends a pageload span named after the resolved route when a redirect is thrown on initial load', async ({
page,
}) => {
const pageloadTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/posts/$postId';
const pageloadSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return span.is_segment && getSpanOp(span) === 'pageload' && span.name === '/posts/$postId';
});

await page.goto(`/redirect`);

const pageloadTxn = await pageloadTxnPromise;

expect(pageloadTxn).toMatchObject({
contexts: {
trace: {
data: {
'sentry.segment.name.source': 'route',
'sentry.origin': 'auto.pageload.vue.tanstack_router',
'sentry.op': 'pageload',
'url.path.parameter.postId': '1',
'url.template': '/posts/$postId',
'url.path': '/posts/1',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/),
},
op: 'pageload',
origin: 'auto.pageload.vue.tanstack_router',
},
},
transaction: '/posts/$postId',
transaction_info: {
source: 'route',
const pageloadSpan = await pageloadSpanPromise;

expect(pageloadSpan).toMatchObject({
name: '/posts/$postId',
is_segment: true,
attributes: {
'sentry.segment.name.source': { type: 'string', value: 'route' },
'sentry.origin': { type: 'string', value: 'auto.pageload.vue.tanstack_router' },
'sentry.op': { type: 'string', value: 'pageload' },
'url.path.parameter.postId': { type: 'string', value: '1' },
'url.template': { type: 'string', value: '/posts/$postId' },
'url.path': { type: 'string', value: '/posts/1' },
'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/) },
},
});
});

test('sends a navigation transaction when a redirect is thrown in beforeLoad', async ({ page }) => {
const pageloadTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
test('sends a navigation span when a redirect is thrown in beforeLoad', async ({ page }) => {
const pageloadSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return span.is_segment && getSpanOp(span) === 'pageload';
});

const navigationTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
return (
!!transactionEvent?.transaction &&
transactionEvent.transaction === '/posts/$postId' &&
transactionEvent.contexts?.trace?.op === 'navigation'
);
const navigationSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return span.is_segment && getSpanOp(span) === 'navigation' && span.name === '/posts/$postId';
});

await page.goto(`/`);
await pageloadTxnPromise;
await pageloadSpanPromise;

await page.locator('#redirect-link').click();

const navigationTxn = await navigationTxnPromise;

expect(navigationTxn).toMatchObject({
contexts: {
trace: {
data: {
'sentry.segment.name.source': 'route',
'sentry.origin': 'auto.navigation.vue.tanstack_router',
'sentry.op': 'navigation',
'url.path.parameter.postId': '1',
'url.template': '/posts/$postId',
'url.path': '/posts/1',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/),
},
op: 'navigation',
origin: 'auto.navigation.vue.tanstack_router',
},
},
transaction: '/posts/$postId',
transaction_info: {
source: 'route',
const navigationSpan = await navigationSpanPromise;

expect(navigationSpan).toMatchObject({
name: '/posts/$postId',
is_segment: true,
attributes: {
'sentry.segment.name.source': { type: 'string', value: 'route' },
'sentry.origin': { type: 'string', value: 'auto.navigation.vue.tanstack_router' },
'sentry.op': { type: 'string', value: 'navigation' },
'url.path.parameter.postId': { type: 'string', value: '1' },
'url.template': { type: 'string', value: '/posts/$postId' },
'url.path': { type: 'string', value: '/posts/1' },
'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/) },
},
});
});

test('sends a navigation transaction for a normal navigation that happens after a redirect', async ({ page }) => {
const pageloadTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
test('sends a navigation span for a normal navigation that happens after a redirect', async ({ page }) => {
const pageloadSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return span.is_segment && getSpanOp(span) === 'pageload';
});

await page.goto(`/`);
await pageloadTxnPromise;
await pageloadSpanPromise;

const redirectTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/posts/$postId';
const redirectSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return span.is_segment && getSpanOp(span) === 'navigation' && span.name === '/posts/$postId';
});
await page.locator('#redirect-link').click();
await redirectTxnPromise;
await redirectSpanPromise;

const navigationTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
const navigationSpanPromise = waitForStreamedSpan('vue-tanstack-router', span => {
return (
transactionEvent.contexts?.trace?.op === 'navigation' &&
transactionEvent.contexts?.trace?.data?.['url.path.parameter.postId'] === '2'
span.is_segment && getSpanOp(span) === 'navigation' && span.attributes['url.path.parameter.postId']?.value === '2'
);
});

await page.locator('#nav-link').click();

const navigationTxn = await navigationTxnPromise;

expect(navigationTxn).toMatchObject({
contexts: {
trace: {
data: {
'sentry.segment.name.source': 'route',
'sentry.origin': 'auto.navigation.vue.tanstack_router',
'sentry.op': 'navigation',
'url.path.parameter.postId': '2',
'url.template': '/posts/$postId',
'url.path': '/posts/2',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/),
},
op: 'navigation',
origin: 'auto.navigation.vue.tanstack_router',
},
},
transaction: '/posts/$postId',
transaction_info: {
source: 'route',
const navigationSpan = await navigationSpanPromise;

expect(navigationSpan).toMatchObject({
name: '/posts/$postId',
is_segment: true,
attributes: {
'sentry.segment.name.source': { type: 'string', value: 'route' },
'sentry.origin': { type: 'string', value: 'auto.navigation.vue.tanstack_router' },
'sentry.op': { type: 'string', value: 'navigation' },
'url.path.parameter.postId': { type: 'string', value: '2' },
'url.template': { type: 'string', value: '/posts/$postId' },
'url.path': { type: 'string', value: '/posts/2' },
'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/) },
},
});
});
Loading