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 @@ -3,7 +3,6 @@ import { Elysia } from 'elysia';
import { node } from '@elysiajs/node';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.E2E_TEST_DSN,
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';

test('Captures an error thrown in a route handler', async ({ baseURL, request }) => {
const errorEventPromise = waitForError('elysia-node', event => {
Expand Down Expand Up @@ -47,13 +47,14 @@ test('Error event includes request metadata', async ({ baseURL, request }) => {
});

test('Does not capture errors for 4xx responses', async ({ baseURL, request }) => {
const transactionPromise = waitForTransaction('elysia-node', transactionEvent => {
return transactionEvent?.transaction === 'GET /test-4xx';
});
const segmentPromise = waitForStreamedSpan(
'elysia-node',
segment => segment.is_segment && segment.name === 'GET /test-4xx',
);

const response = await request.get(`${baseURL}/test-4xx`);
// Wait for the transaction to ensure the request was processed
await transactionPromise;
// Wait for the segment to ensure the request was processed
await segmentPromise;

expect(response.status()).toBe(400);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto';
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils';

test('Includes sentry-trace and baggage in response headers', async ({ baseURL }) => {
const response = await fetch(`${baseURL}/test-success`);
Expand All @@ -21,27 +21,23 @@ test('Includes sentry-trace and baggage in response headers', async ({ baseURL }
test.fixme('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const id = randomUUID();

const inboundTransactionPromise = waitForTransaction('elysia-node', transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /test-inbound-headers/:id'
);
const inboundSegmentPromise = waitForStreamedSpan('elysia-node', segmentEvent => {
if (!segmentEvent.is_segment) return false;
return getSpanOp(segmentEvent!) === 'http.server' && segmentEvent.name === 'GET /test-inbound-headers/:id';
});

const outboundTransactionPromise = waitForTransaction('elysia-node', transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /test-outgoing-fetch/:id'
);
const outboundSegmentPromise = waitForStreamedSpan('elysia-node', segmentEvent => {
if (!segmentEvent.is_segment) return false;
return getSpanOp(segmentEvent!) === 'http.server' && segmentEvent.name === 'GET /test-outgoing-fetch/:id';
});

const response = await fetch(`${baseURL}/test-outgoing-fetch/${id}`);
const data = await response.json();

const inboundTransaction = await inboundTransactionPromise;
const outboundTransaction = await outboundTransactionPromise;
const inboundSegment = await inboundSegmentPromise;
const outboundSegment = await outboundSegmentPromise;

const traceId = outboundTransaction?.contexts?.trace?.trace_id;
const traceId = outboundSegment?.trace_id;
expect(traceId).toEqual(expect.any(String));

// Verify sentry-trace header was propagated to the inbound request
Expand All @@ -61,22 +57,22 @@ test.fixme('Propagates trace for outgoing fetch requests', async ({ baseURL }) =
);

// Both transactions should share the same trace ID
expect(inboundTransaction.contexts?.trace?.trace_id).toBe(traceId);
expect(inboundSegment?.trace_id).toBe(traceId);
});

test.fixme('Propagates trace for outgoing fetch to external allowed URL', async ({ baseURL }) => {
const inboundTransactionPromise = waitForTransaction('elysia-node', transactionEvent => {
const inboundSegmentPromise = waitForStreamedSpan('elysia-node', segmentEvent => {
if (!segmentEvent.is_segment) return false;
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /test-outgoing-fetch-external-allowed'
getSpanOp(segmentEvent!) === 'http.server' && segmentEvent.name === 'GET /test-outgoing-fetch-external-allowed'
);
});

const response = await fetch(`${baseURL}/test-outgoing-fetch-external-allowed`);
const data = await response.json();

const inboundTransaction = await inboundTransactionPromise;
const traceId = inboundTransaction?.contexts?.trace?.trace_id;
const inboundSegment = await inboundSegmentPromise;
const traceId = inboundSegment?.trace_id;

expect(traceId).toEqual(expect.any(String));

Expand All @@ -95,17 +91,18 @@ test.fixme('Propagates trace for outgoing fetch to external allowed URL', async
});

test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => {
const inboundTransactionPromise = waitForTransaction('elysia-node', transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /test-outgoing-fetch-external-disallowed'
);
});
const inboundSegmentPromise = waitForStreamedSpan(
'elysia-node',
segment =>
segment.is_segment &&
getSpanOp(segment) === 'http.server' &&
segment.name === 'GET /test-outgoing-fetch-external-disallowed',
);

const response = await fetch(`${baseURL}/test-outgoing-fetch-external-disallowed`);
const data = await response.json();

await inboundTransactionPromise;
await inboundSegmentPromise;

expect(data.route).toBe('/external-disallowed');
expect(data.headers?.['sentry-trace']).toBeUndefined();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { expect, test } from '@playwright/test';
import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';

test('Sends a span for a successful route', async ({ baseURL, request }) => {
const segmentEventPromise = waitForStreamedSpan(
'elysia-node',
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
);

await request.get(`${baseURL}/test-success`);

const segmentEvent = await segmentEventPromise;

expect(segmentEvent).toEqual(
expect.objectContaining({
name: 'GET /test-success',
is_segment: true,
attributes: expect.objectContaining({ 'sentry.segment.name.source': { value: 'route', type: 'string' } }),
}),
);

expect(segmentEvent).toEqual(
expect.objectContaining({
status: 'ok',
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
attributes: expect.objectContaining({ 'sentry.op': { value: 'http.server', type: 'string' } }),
}),
);
});

test('Sends a span with parameterized route name', async ({ baseURL, request }) => {
const segmentEventPromise = waitForStreamedSpan(
'elysia-node',
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-param/:param',
);

await request.get(`${baseURL}/test-param/123`);

const segmentEvent = await segmentEventPromise;

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

test('Sends a span with multiple parameterized segments', async ({ baseURL, request }) => {
const segmentEventPromise = waitForStreamedSpan(
'elysia-node',
segment =>
segment.is_segment &&
getSpanOp(segment) === 'http.server' &&
segment.name === 'GET /test-multi-param/:param1/:param2',
);

await request.get(`${baseURL}/test-multi-param/foo/bar`);

const segmentEvent = await segmentEventPromise;

expect(segmentEvent.name).toBe('GET /test-multi-param/:param1/:param2');
expect(segmentEvent.attributes['sentry.segment.name.source']?.value).toBe('route');
});

test('Sends a span for an errored route', async ({ baseURL, request }) => {
const segmentEventPromise = waitForStreamedSpan(
'elysia-node',
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-exception/:id',
);

await request.get(`${baseURL}/test-exception/777`);

const segmentEvent = await segmentEventPromise;

expect(segmentEvent.name).toBe('GET /test-exception/:id');
expect(segmentEvent?.status).toBe('error');
});

test('Includes manually started spans with parent-child relationship', async ({ baseURL, request }) => {
const segmentEventPromise = collectStreamedSpansUntilSegment(
'elysia-node',
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-transaction',
);

await request.get(`${baseURL}/test-transaction`);

const segmentEventSpans = await segmentEventPromise;
const segmentEvent = segmentEventSpans.find(
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-transaction',
)!;
const spans = segmentEventSpans.filter(
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
);

const testSpan = spans.find(span => span.name === 'test-span');
const childSpan = spans.find(span => span.name === 'child-span');

expect(testSpan).toEqual(
expect.objectContaining({
name: 'test-span',
attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' } }),
}),
);

expect(childSpan).toEqual(
expect.objectContaining({
name: 'child-span',
parent_span_id: testSpan?.span_id,
attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' } }),
}),
);
});

test('Creates lifecycle spans for Elysia hooks', async ({ baseURL, request }) => {
const segmentEventPromise = collectStreamedSpansUntilSegment(
'elysia-node',
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
);

await request.get(`${baseURL}/test-success`);

const segmentEventSpans = await segmentEventPromise;
const segmentEvent = segmentEventSpans.find(
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
)!;
const spans = segmentEventSpans.filter(
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
);

// Elysia should produce lifecycle spans enriched with sentry attributes
const elysiaSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.http.elysia');
expect(elysiaSpans.length).toBeGreaterThan(0);

expect(spans.filter(span => span.name === '/test-success')).toEqual([
expect.objectContaining({
name: '/test-success',
attributes: expect.objectContaining({
'sentry.op': { value: 'handler', type: 'string' },
'sentry.origin': { value: 'auto.http.elysia', type: 'string' },
}),
}),
]);
Comment thread
msonnb marked this conversation as resolved.
});

test('Names middleware "anonymous" and handlers after their route', async ({ baseURL, request }) => {
const segmentEventPromise = collectStreamedSpansUntilSegment(
'elysia-node',
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /with-middleware/test',
);

await request.get(`${baseURL}/with-middleware/test`);

const segmentEventSpans = await segmentEventPromise;
const segmentEvent = segmentEventSpans.find(
segment =>
segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /with-middleware/test',
)!;
const spans = segmentEventSpans.filter(
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
);

const unknownSpans = spans.filter(span => span.name === '<unknown>');
expect(unknownSpans).toHaveLength(0);

const elysiaSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.http.elysia');
const anonymousMiddlewareSpans = elysiaSpans.filter(
span => getSpanOp(span) === 'middleware' && span.name === 'anonymous',
);
expect(anonymousMiddlewareSpans).toHaveLength(1);

const handlerSpans = elysiaSpans.filter(span => getSpanOp(span) === 'handler');
expect(handlerSpans.map(span => span.name)).toEqual(['/with-middleware/test']);
});

test('Creates lifecycle spans for route-specific middleware', async ({ baseURL, request }) => {
const segmentEventPromise = collectStreamedSpansUntilSegment(
'elysia-node',
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /with-middleware/test',
);

await request.get(`${baseURL}/with-middleware/test`);

const segmentEventSpans = await segmentEventPromise;
const segmentEvent = segmentEventSpans.find(
segment =>
segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /with-middleware/test',
)!;
const spans = segmentEventSpans.filter(
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
);

// BeforeHandle span should be present from the route-specific middleware
expect(spans.filter(span => span.name === 'BeforeHandle')).toEqual([
expect.objectContaining({
name: 'BeforeHandle',
attributes: expect.objectContaining({
'sentry.op': { value: 'middleware', type: 'string' },
'sentry.origin': { value: 'auto.http.elysia', type: 'string' },
}),
}),
]);
});

test('Captures request metadata for POST requests', async ({ baseURL, request }) => {
const segmentEventPromise = waitForStreamedSpan(
'elysia-node',
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'POST /test-post',
);

const response = await request.post(`${baseURL}/test-post`, {
data: { foo: 'bar', other: 1 },
headers: { 'Content-Type': 'application/json' },
});
const resBody = await response.json();

expect(resBody).toEqual({ status: 'ok', body: { foo: 'bar', other: 1 } });

const segmentEvent = await segmentEventPromise;

expect(segmentEvent.attributes['http.request.method']?.value).toEqual('POST');
expect(segmentEvent.attributes['url.full']?.value).toEqual(expect.stringContaining('/test-post'));
expect(segmentEvent.attributes['http.request.header.content_type']?.value).toEqual('application/json');
});
Loading
Loading