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 @@ -19,7 +19,7 @@
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *",
"nitro": "^3.0.260429-beta",
"nitro": "^3.0.260522-beta",
"rolldown": "latest",
"vite": "latest"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,16 @@ test('Sends an error event to Sentry', async ({ request }) => {

const errorEvent = await errorEventPromise;

// Nitro wraps thrown errors in an HTTPError with .cause, producing a chained exception
expect(errorEvent.exception?.values).toHaveLength(2);
expect(errorEvent.exception?.values).toHaveLength(1);

// The innermost exception (values[0]) is the original thrown error
expect(errorEvent.exception?.values?.[0]?.type).toBe('Error');
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is a test error');
expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
expect.objectContaining({
handled: false,
type: 'auto.function.nitro.captureErrorHook',
type: 'auto.http.nitro.onTraceError',
}),
);

// The outermost exception (values[1]) is the HTTPError wrapper
expect(errorEvent.exception?.values?.[1]?.type).toBe('HTTPError');
expect(errorEvent.exception?.values?.[1]?.value).toBe('This is a test error');
});

test('Does not send 404 errors to Sentry', async ({ request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ test('Span nesting: h3 middleware spans are children of the srvx request span',
expect(srvxSpan).toBeDefined();

// All h3 middleware spans should be children of the srvx span
const h3Spans = event.spans?.filter(span => span.origin === 'auto.http.nitro.h3');
expect(h3Spans?.length).toBeGreaterThanOrEqual(1);
const h3MiddlewareSpans = event.spans?.filter(
span => span.origin === 'auto.http.nitro.h3' && span.op === 'middleware.nitro',
);
expect(h3MiddlewareSpans?.length).toBeGreaterThanOrEqual(1);

for (const span of h3Spans ?? []) {
for (const span of h3MiddlewareSpans ?? []) {
expect(span.parent_span_id).toBe(srvxSpan!.span_id);
}
});

test('Span nesting: manual startSpan calls inside route handler are children of the srvx request span', async ({
request,
}) => {
test('Span nesting: h3 route handler span is a child of the srvx request span', async ({ request }) => {
const transactionEventPromise = waitForTransaction('nitro-3', event => {
return event?.transaction === 'GET /api/test-nesting';
});
Expand All @@ -52,23 +52,38 @@ test('Span nesting: manual startSpan calls inside route handler are children of

const event = await transactionEventPromise;

// Find the srvx request span — this is the parent of all h3 and manual spans
const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server');
expect(srvxSpan).toBeDefined();
const srvxSpanId = srvxSpan!.span_id;

const h3HandlerSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server');
expect(h3HandlerSpan).toBeDefined();
expect(h3HandlerSpan!.parent_span_id).toBe(srvxSpan!.span_id);
});

test('Span nesting: manual startSpan calls inside route handler are children of the h3 route handler span', async ({
request,
}) => {
const transactionEventPromise = waitForTransaction('nitro-3', event => {
return event?.transaction === 'GET /api/test-nesting';
});

await request.get('/api/test-nesting');

const event = await transactionEventPromise;

// Find the h3 route handler span
const h3HandlerSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server');
expect(h3HandlerSpan).toBeDefined();

// Find the manually created db spans
const dbSelectSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.select');
const dbInsertSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.insert');
expect(dbSelectSpan).toBeDefined();
expect(dbInsertSpan).toBeDefined();

// FIXME: Once nitro's h3 tracing plugin emits a separate span for route handlers (type: "route"),
// the db spans should be children of the h3 route handler span, not the srvx span directly.
// Currently nitro bypasses h3's ~routes for file-based routing, so h3 only emits middleware spans.
// Both db spans should be children of the srvx request span
expect(dbSelectSpan!.parent_span_id).toBe(srvxSpanId);
expect(dbInsertSpan!.parent_span_id).toBe(srvxSpanId);
// Both db spans should be children of the h3 route handler span
expect(dbSelectSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);
expect(dbInsertSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);

// Both db spans should be siblings (same parent)
expect(dbSelectSpan!.parent_span_id).toBe(dbInsertSpan!.parent_span_id);
Expand All @@ -79,49 +94,6 @@ test('Span nesting: manual startSpan calls inside route handler are children of
expect(serializeSpan!.parent_span_id).toBe(dbInsertSpan!.span_id);
});

// FIXME: Nitro's file-based routing bypasses h3's ~routes, so h3's tracing plugin never wraps
// route handlers with type: "route". Once this is fixed upstream or we add our own wrapping,
// uncomment these tests to verify the h3 route handler span exists and is the parent of manual spans.
//
// test('Span nesting: h3 route handler span is a child of the srvx request span', async ({ request }) => {
// const transactionEventPromise = waitForTransaction('nitro-3', event => {
// return event?.transaction === 'GET /api/test-nesting';
// });
//
// await request.get('/api/test-nesting');
//
// const event = await transactionEventPromise;
//
// const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server');
// expect(srvxSpan).toBeDefined();
//
// const h3HandlerSpan = event.spans?.find(
// span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server',
// );
// expect(h3HandlerSpan).toBeDefined();
// expect(h3HandlerSpan!.parent_span_id).toBe(srvxSpan!.span_id);
// });
//
// test('Span nesting: manual startSpan calls are children of the h3 route handler span', async ({ request }) => {
// const transactionEventPromise = waitForTransaction('nitro-3', event => {
// return event?.transaction === 'GET /api/test-nesting';
// });
//
// await request.get('/api/test-nesting');
//
// const event = await transactionEventPromise;
//
// const h3HandlerSpan = event.spans?.find(
// span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server',
// );
// expect(h3HandlerSpan).toBeDefined();
//
// const dbSelectSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.select');
// const dbInsertSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.insert');
// expect(dbSelectSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);
// expect(dbInsertSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);
// });

test('Span nesting: middleware spans start before manual spans in the span tree', async ({ request }) => {
const transactionEventPromise = waitForTransaction('nitro-3', event => {
return event?.transaction === 'GET /api/test-nesting';
Expand Down
Loading