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
@@ -1,7 +1,6 @@
const Sentry = require('@sentry/node');

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.E2E_TEST_DSN,
includeLocalVariables: true,
Expand Down Expand Up @@ -31,7 +30,7 @@ const init = async () => {
method: 'GET',
path: '/test-param/{param}',
handler: function (request, h) {
Sentry.setTag(`param-${request.params.param}`, 'yes');
Sentry.setAttribute(`param-${request.params.param}`, 'yes');

return { paramWas: request.params.param };
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';

test('Sends thrown error to Sentry', async ({ baseURL }) => {
const errorEventPromise = waitForError('node-hapi', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'This is an error';
});

const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => {
return transactionEvent?.transaction === 'GET /test-failure';
});
const segmentEventPromise = waitForStreamedSpan(
'node-hapi',
segment => segment.is_segment && segment.name === 'GET /test-failure',
);

await fetch(`${baseURL}/test-failure`);

const errorEvent = await errorEventPromise;
const transactionEvent = await transactionEventPromise;
const segmentEvent = await segmentEventPromise;

expect(transactionEvent.transaction).toBe('GET /test-failure');
expect(transactionEvent.contexts?.trace).toMatchObject({
expect(segmentEvent.name).toBe('GET /test-failure');
expect(segmentEvent).toMatchObject({
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
});
Expand All @@ -43,8 +44,8 @@ test('Sends thrown error to Sentry', async ({ baseURL }) => {
span_id: expect.stringMatching(/[a-f0-9]{16}/),
});

expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id);
expect(errorEvent.contexts?.trace?.span_id).toBe(transactionEvent.contexts?.trace?.span_id);
expect(errorEvent.contexts?.trace?.trace_id).toBe(segmentEvent?.trace_id);
expect(errorEvent.contexts?.trace?.span_id).toBe(segmentEvent?.span_id);
});

test('sends error with parameterized transaction name', async ({ baseURL }) => {
Expand All @@ -71,26 +72,28 @@ test('Does not send errors to Sentry if boom throws in "onPreResponse" after JS
return false; // expects to return a boolean (but not relevant here)
});

const transactionEventPromise4xx = waitForTransaction('node-hapi', transactionEvent => {
return transactionEvent?.transaction === 'GET /test-failure-boom-4xx';
});
const segmentEventPromise4xx = waitForStreamedSpan(
'node-hapi',
segment => segment.is_segment && segment.name === 'GET /test-failure-boom-4xx',
);

const transactionEventPromise5xx = waitForTransaction('node-hapi', transactionEvent => {
return transactionEvent?.transaction === 'GET /test-failure-boom-5xx';
});
const segmentEventPromise5xx = waitForStreamedSpan(
'node-hapi',
segment => segment.is_segment && segment.name === 'GET /test-failure-boom-5xx',
);

const response4xx = await fetch(`${baseURL}/test-failure-boom-4xx`);
const response5xx = await fetch(`${baseURL}/test-failure-boom-5xx`);

expect(response4xx.status).toBe(400);
expect(response5xx.status).toBe(504);

const transactionEvent4xx = await transactionEventPromise4xx;
const transactionEvent5xx = await transactionEventPromise5xx;
const segmentEvent4xx = await segmentEventPromise4xx;
const segmentEvent5xx = await segmentEventPromise5xx;

expect(errorEventOccurred).toBe(false);
expect(transactionEvent4xx.transaction).toBe('GET /test-failure-boom-4xx');
expect(transactionEvent5xx.transaction).toBe('GET /test-failure-boom-5xx');
expect(segmentEvent4xx.name).toBe('GET /test-failure-boom-4xx');
expect(segmentEvent5xx.name).toBe('GET /test-failure-boom-5xx');
});

test('Does not send error to Sentry if error response is overwritten with 2xx in "onPreResponse"', async ({
Expand All @@ -105,17 +108,18 @@ test('Does not send error to Sentry if error response is overwritten with 2xx in
return false; // expects to return a boolean (but not relevant here)
});

const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => {
return transactionEvent?.transaction === 'GET /test-failure-2xx-override-onPreResponse';
});
const segmentEventPromise = waitForStreamedSpan(
'node-hapi',
segment => segment.is_segment && segment.name === 'GET /test-failure-2xx-override-onPreResponse',
);

const response = await fetch(`${baseURL}/test-failure-2xx-override-onPreResponse`);

const transactionEvent = await transactionEventPromise;
const segmentEvent = await segmentEventPromise;

expect(response.status).toBe(200);
expect(errorEventOccurred).toBe(false);
expect(transactionEvent.transaction).toBe('GET /test-failure-2xx-override-onPreResponse');
expect(segmentEvent.name).toBe('GET /test-failure-2xx-override-onPreResponse');
});

test('Only sends onPreResponse error to Sentry if JS error is thrown in route handler AND onPreResponse', async ({
Expand All @@ -137,18 +141,19 @@ test('Only sends onPreResponse error to Sentry if JS error is thrown in route ha
return false; // expects to return a boolean (but not relevant here)
});

const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => {
return transactionEvent?.transaction === 'GET /test-failure-JS-error-onPreResponse';
});
const segmentEventPromise = waitForStreamedSpan(
'node-hapi',
segment => segment.is_segment && segment.name === 'GET /test-failure-JS-error-onPreResponse',
);

const response = await fetch(`${baseURL}/test-failure-JS-error-onPreResponse`);

expect(response.status).toBe(500);

const errorEvent = await errorEventPromise;
const transactionEvent = await transactionEventPromise;
const segmentEvent = await segmentEventPromise;

expect(routeHandlerErrorOccurred).toBe(false);
expect(transactionEvent.transaction).toBe('GET /test-failure-JS-error-onPreResponse');
expect(segmentEvent.name).toBe('GET /test-failure-JS-error-onPreResponse');
expect(errorEvent.transaction).toEqual('GET /test-failure-JS-error-onPreResponse');
});
149 changes: 149 additions & 0 deletions dev-packages/e2e-tests/test-applications/node-hapi/tests/spans.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { expect, test } from '@playwright/test';
import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';

test('Sends successful span', async ({ baseURL }) => {
const pageloadSegmentEventPromise = collectStreamedSpansUntilSegment(
'node-hapi',
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
);

await fetch(`${baseURL}/test-success`);

const segmentEventSpans = await pageloadSegmentEventPromise;
const segmentEvent = segmentEventSpans.find(
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
)!;

expect(segmentEvent).toEqual(
expect.objectContaining({
span_id: expect.stringMatching(/[a-f0-9]{16}/),
status: 'ok',
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
attributes: expect.objectContaining({
'sentry.segment.name.source': { value: 'route', type: 'string' },
'sentry.origin': { value: 'auto.http.http_server', type: 'string' },
'sentry.op': { value: 'http.server', type: 'string' },
'sentry.sample_rate': { value: 1, type: 'integer' },
'sentry.kind': { value: 'server', type: 'string' },
'http.response.status_code': { value: 200, type: 'integer' },
'url.full': { value: 'http://localhost:3030/test-success', type: 'string' },
'url.path': { value: '/test-success', type: 'string' },
'server.address': { value: 'localhost', type: 'string' },
'http.request.method': { value: 'GET', type: 'string' },
'url.scheme': { value: 'http', type: 'string' },
'user_agent.original': { value: 'node', type: 'string' },
'client.address': { value: '::1', type: 'string' },
'client.port': { value: expect.any(Number), type: 'integer' },
'network.transport': { value: 'tcp', type: 'string' },
'network.local.address': { value: expect.any(String), type: 'string' },
'network.local.port': { value: expect.any(Number), type: 'integer' },
'network.peer.address': { value: expect.any(String), type: 'string' },
'network.peer.port': { value: expect.any(Number), type: 'integer' },
'network.protocol.name': { value: 'http', type: 'string' },
'network.protocol.version': { value: '1.1', type: 'string' },
'server.port': { value: 3030, type: 'integer' },
'http.response.status_text': { value: 'OK', type: 'string' },
'http.route': { value: '/test-success', type: 'string' },
'http.request.header.accept': { value: '*/*', type: 'string' },
'http.request.header.accept_encoding': { value: 'gzip, deflate', type: 'string' },
'http.request.header.accept_language': { value: '*', type: 'string' },
'http.request.header.connection': { value: 'keep-alive', type: 'string' },
'http.request.header.host': { value: expect.any(String), type: 'string' },
'http.request.header.sec_fetch_mode': { value: 'cors', type: 'string' },
'http.request.header.user_agent': { value: 'node', type: 'string' },
}),
}),
);

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

const spans = segmentEventSpans.filter(
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
);

spans.forEach(span => {
expect(Object.keys(span.attributes).some(key => key.startsWith('http.request.header.'))).toBe(false);
});

expect(spans).toEqual([
expect.objectContaining({
name: '/test-success',
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
status: 'ok',
end_timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
attributes: expect.objectContaining({
'hapi.type': { value: 'router', type: 'string' },
'http.request.method': { value: 'GET', type: 'string' },
'http.route': { value: '/test-success', type: 'string' },
'sentry.op': { value: 'router', type: 'string' },
'sentry.origin': { value: 'auto.http.hapi', type: 'string' },
}),
}),
expect.objectContaining({
name: 'ext - onPreResponse',
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
status: 'ok',
end_timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
attributes: expect.objectContaining({
'hapi.type': { value: 'server.ext', type: 'string' },
'sentry.op': { value: 'middleware', type: 'string' },
'sentry.origin': { value: 'auto.http.hapi', type: 'string' },
'server.ext.type': { value: 'onPreResponse', type: 'string' },
}),
}),
]);
});

test('Sends parameterized spans to Sentry', async ({ baseURL }) => {
const pageloadSegmentEventPromise = waitForStreamedSpan(
'node-hapi',
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-param/{param}',
);

await fetch(`${baseURL}/test-param/123`);

const segmentEvent = await pageloadSegmentEventPromise;

expect(getSpanOp(segmentEvent)).toBe('http.server');
expect(segmentEvent.attributes?.['http.route']?.value).toBe('/test-param/{param}');
expect(segmentEvent.name).toBe('GET /test-param/{param}');
});

test('Isolates requests', async ({ baseURL }) => {
const segment1Promise = waitForStreamedSpan(
'node-hapi',
segment =>
segment.is_segment &&
getSpanOp(segment) === 'http.server' &&
segment.attributes?.['url.path']?.value === '/test-param/888',
);
const segment2Promise = waitForStreamedSpan(
'node-hapi',
segment =>
segment.is_segment &&
getSpanOp(segment) === 'http.server' &&
segment.attributes?.['url.path']?.value === '/test-param/999',
);

await Promise.all([fetch(`${baseURL}/test-param/888`), fetch(`${baseURL}/test-param/999`)]);

const segment1 = await segment1Promise;
const segment2 = await segment2Promise;

expect(segment1.attributes['param-888']?.value).toBe('yes');
expect(segment1.attributes['param-999']).toBeUndefined();
expect(segment2.attributes['param-999']?.value).toBe('yes');
expect(segment2.attributes['param-888']).toBeUndefined();
});
Loading
Loading