Skip to content
Open
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 @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/nuxt';
import { useRuntimeConfig } from '#imports';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: useRuntimeConfig().public.sentry.dsn,
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/nuxt';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0, // Capture 100% of the transactions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';

test.describe('server-side errors', async () => {
test('captures api fetch error (fetched on click)', async ({ page }) => {
const transactionEventPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => {
return transactionEvent?.transaction === 'GET /api/server-error';
// The exact-match API route is not parametrized, so the segment keeps a method-only name and
// has to be selected via its `url.path` attribute.
const serverSpanPromise = waitForStreamedSpan('nuxt-3-top-level-import', span => {
return (
span.is_segment &&
getSpanOp(span) === 'http.server' &&
span.attributes['url.path']?.value === '/api/server-error'
);
});

const errorPromise = waitForError('nuxt-3-top-level-import', async errorEvent => {
Expand All @@ -14,7 +20,7 @@ test.describe('server-side errors', async () => {
await page.goto(`/fetch-server-error`);
await page.getByText('Fetch Server API Error', { exact: true }).click();

const transactionEvent = await transactionEventPromise;
const serverSpan = await serverSpanPromise;
const error = await errorPromise;

expect(error.transaction).toEqual('GET /api/server-error');
Expand All @@ -40,15 +46,19 @@ test.describe('server-side errors', async () => {
exception_id: 0,
});

// Streamed spans carry no scope tags, so isolation is asserted on the error event only
expect(serverSpan.name).toBe('GET');
expect(error.tags?.['my-isolated-tag']).toBe(true);
expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
});

test('isolates requests', async ({ page }) => {
const transactionEventPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => {
return transactionEvent?.transaction === 'GET /api/server-error';
const serverSpanPromise = waitForStreamedSpan('nuxt-3-top-level-import', span => {
return (
span.is_segment &&
getSpanOp(span) === 'http.server' &&
span.attributes['url.path']?.value === '/api/server-error'
);
});

const errorPromise = waitForError('nuxt-3-top-level-import', async errorEvent => {
Expand All @@ -58,13 +68,12 @@ test.describe('server-side errors', async () => {
await page.goto(`/fetch-server-error`);
await page.getByText('Fetch Server API Error', { exact: true }).click();

const transactionEvent = await transactionEventPromise;
await serverSpanPromise;
const error = await errorPromise;

// Streamed spans carry no scope tags, so isolation is asserted on the error event only
expect(error.tags?.['my-isolated-tag']).toBe(true);
expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
});

test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,49 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import type { Span } from '@sentry/nuxt';
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';

test('sends a pageload root span with a parameterized URL', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => {
return transactionEvent.transaction === '/test-param/:param()';
const pageloadSpanPromise = waitForStreamedSpan('nuxt-3-top-level-import', span => {
return getSpanOp(span) === 'pageload' && span.is_segment;
});

await page.goto(`/test-param/1234`);

const rootSpan = await transactionPromise;

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

expect(pageloadSpan.name).toBe('/test-param/:param()');
expect(pageloadSpan.status).toBe('ok');
expect(pageloadSpan.attributes).toMatchObject({
'sentry.segment.name.source': { type: 'string', value: 'route' },
'sentry.origin': { type: 'string', value: 'auto.pageload.vue' },
'sentry.op': { type: 'string', value: 'pageload' },
'params.param': { type: 'string', value: '1234' },
'url.template': { type: 'string', value: '/test-param/:param()' },
'url.path': { type: 'string', value: '/test-param/1234' },
'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/) },
});
});

test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => {
return transactionEvent.transaction === '/client-error';
});
const spansPromise = collectStreamedSpans('nuxt-3-top-level-import', spans =>
spans.some(span => span.name === '/client-error' && span.is_segment && getSpanOp(span) === 'pageload'),
);

await page.goto(`/client-error`);

const rootSpan = await transactionPromise;
const errorButtonSpan = rootSpan.spans.find((span: Span) => span.description === 'Vue <ErrorButton>');
const spans = await spansPromise;
const errorButtonSpan = spans.find(span => span.name === 'Vue <ErrorButton>');

const expected = {
data: { 'sentry.origin': 'auto.ui.vue', 'sentry.op': 'ui.mount' },
description: 'Vue <ErrorButton>',
op: 'ui.mount',
expect(errorButtonSpan).toMatchObject({
name: 'Vue <ErrorButton>',
is_segment: false,
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
origin: 'auto.ui.vue',
};

expect(errorButtonSpan).toMatchObject(expected);
start_timestamp: expect.any(Number),
end_timestamp: expect.any(Number),
attributes: expect.objectContaining({
'sentry.op': { type: 'string', value: 'ui.mount' },
'sentry.origin': { type: 'string', value: 'auto.ui.vue' },
}),
});
});
Original file line number Diff line number Diff line change
@@ -1,45 +1,39 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt';
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';

test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-top-level-import', transactionEvent => {
return transactionEvent.transaction.includes('GET /test-param/');
test('sends a server root span on pageload', async ({ page }) => {
const serverSpanPromise = waitForStreamedSpan('nuxt-3-top-level-import', span => {
return span.is_segment && span.name.includes('GET /test-param/');
});

await page.goto('/test-param/1234');

const transaction = await transactionPromise;
const serverSpan = await serverSpanPromise;

expect(transaction.contexts.trace).toEqual(
expect.objectContaining({
data: expect.objectContaining({
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.http_server',
}),
}),
);
expect(getSpanOp(serverSpan)).toBe('http.server');
expect(serverSpan.attributes['sentry.origin']?.value).toBe('auto.http.http_server');
});

test('does not send transactions for build asset folder "_nuxt"', async ({ page }) => {
test('does not send spans for build asset folder "_nuxt"', async ({ page }) => {
let buildAssetFolderOccurred = false;

waitForTransaction('nuxt-3-top-level-import', transactionEvent => {
if (transactionEvent.transaction?.match(/^GET \/_nuxt\//)) {
waitForStreamedSpan('nuxt-3-top-level-import', span => {
if (span.is_segment && /^GET \/_nuxt\//.test(span.name)) {
buildAssetFolderOccurred = true;
}
return false; // expects to return a boolean (but not relevant here)
});

const transactionEventPromise = waitForTransaction('nuxt-3-top-level-import', transactionEvent => {
return transactionEvent.transaction.includes('GET /test-param/');
const serverSpanPromise = waitForStreamedSpan('nuxt-3-top-level-import', span => {
return span.is_segment && span.name.includes('GET /test-param/');
});

await page.goto('/test-param/1234');

const transactionEvent = await transactionEventPromise;
const serverSpan = await serverSpanPromise;

expect(buildAssetFolderOccurred).toBe(false);

expect(transactionEvent.transaction).toBe('GET /test-param/:param()');
expect(serverSpan.name).toBe('GET /test-param/:param()');
expect(serverSpan.attributes['sentry.segment.name.source']?.value).toBe('route');
});
Loading
Loading