Skip to content

Commit 9e95ca5

Browse files
committed
Add node-core otel v2 e2e test app
1 parent f316d0f commit 9e95ca5

File tree

10 files changed

+291
-0
lines changed

10 files changed

+291
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "node-core-express-otel-v2-app",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "tsc",
7+
"start": "node dist/app.js",
8+
"test": "playwright test",
9+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
10+
"test:build": "pnpm install && pnpm build",
11+
"test:assert": "pnpm test"
12+
},
13+
"dependencies": {
14+
"@sentry/node-core": "latest || *",
15+
"@sentry/opentelemetry": "latest || *",
16+
"@opentelemetry/api": "^1.9.0",
17+
"@opentelemetry/context-async-hooks": "^2.0.0",
18+
"@opentelemetry/core": "^2.0.0",
19+
"@opentelemetry/instrumentation": "^0.200.0",
20+
"@opentelemetry/instrumentation-http": "^0.200.0",
21+
"@opentelemetry/resources": "^2.0.0",
22+
"@opentelemetry/sdk-trace-node": "^2.0.0",
23+
"@opentelemetry/semantic-conventions": "^1.30.0",
24+
"@types/express": "^4.17.21",
25+
"@types/node": "^18.19.1",
26+
"express": "^4.21.2",
27+
"typescript": "~5.0.0"
28+
},
29+
"devDependencies": {
30+
"@playwright/test": "~1.50.0",
31+
"@sentry-internal/test-utils": "link:../../../test-utils"
32+
},
33+
"resolutions": {
34+
"@types/qs": "6.9.17"
35+
},
36+
"volta": {
37+
"extends": "../../package.json"
38+
}
39+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Import this first!
2+
import './instrument';
3+
4+
// Now import other modules
5+
import * as Sentry from '@sentry/node-core';
6+
import express from 'express';
7+
8+
const app = express();
9+
const port = 3030;
10+
11+
app.get('/test-transaction', function (req, res) {
12+
Sentry.withActiveSpan(null, async () => {
13+
Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => {
14+
Sentry.startSpan({ name: 'test-span' }, () => undefined);
15+
});
16+
17+
await Sentry.flush();
18+
19+
res.send({
20+
transactionIds: global.transactionIds || [],
21+
});
22+
});
23+
});
24+
25+
app.get('/test-exception/:id', function (req, _res) {
26+
try {
27+
throw new Error(`This is an exception with id ${req.params.id}`);
28+
} catch (e) {
29+
Sentry.captureException(e);
30+
throw e;
31+
}
32+
});
33+
34+
app.get('/test-local-variables-caught', function (req, res) {
35+
const randomVariableToRecord = Math.random();
36+
37+
let exceptionId: string;
38+
try {
39+
throw new Error('Local Variable Error');
40+
} catch (e) {
41+
exceptionId = Sentry.captureException(e);
42+
}
43+
44+
res.send({ exceptionId, randomVariableToRecord });
45+
});
46+
47+
// @ts-ignore
48+
app.use(function onError(err, req, res, next) {
49+
// The error id is attached to `res.sentry` to be returned
50+
// and optionally displayed to the user for support.
51+
res.statusCode = 500;
52+
res.end(res.sentry + '\n');
53+
});
54+
55+
app.listen(port, () => {
56+
console.log(`Example app listening on port ${port}`);
57+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
2+
import * as Sentry from '@sentry/node-core';
3+
import { SentrySpanProcessor, SentryPropagator, SentrySampler } from '@sentry/opentelemetry';
4+
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
5+
6+
declare global {
7+
namespace globalThis {
8+
var transactionIds: string[];
9+
}
10+
}
11+
12+
const sentryClient = Sentry.init({
13+
environment: 'qa', // dynamic sampling bias to keep transactions
14+
dsn: process.env.E2E_TEST_DSN,
15+
includeLocalVariables: true,
16+
debug: !!process.env.DEBUG,
17+
tunnel: `http://localhost:3031/`, // proxy server
18+
tracesSampleRate: 1,
19+
openTelemetryInstrumentations: [new HttpInstrumentation()],
20+
});
21+
22+
const provider = new NodeTracerProvider({
23+
sampler: sentryClient ? new SentrySampler(sentryClient) : undefined,
24+
spanProcessors: [new SentrySpanProcessor()],
25+
});
26+
27+
provider.register({
28+
propagator: new SentryPropagator(),
29+
contextManager: new Sentry.SentryContextManager(),
30+
});
31+
32+
Sentry.validateOpenTelemetrySetup();
33+
34+
Sentry.addEventProcessor(event => {
35+
global.transactionIds = global.transactionIds || [];
36+
37+
if (event.type === 'transaction') {
38+
const eventId = event.event_id;
39+
40+
if (eventId) {
41+
global.transactionIds.push(eventId);
42+
}
43+
}
44+
45+
return event;
46+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'node-core-express-otel-v2',
6+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
test('Sends correct error event', async ({ baseURL }) => {
5+
const errorEventPromise = waitForError('node-core-express-otel-v2', event => {
6+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
7+
});
8+
9+
await fetch(`${baseURL}/test-exception/123`);
10+
11+
const errorEvent = await errorEventPromise;
12+
13+
expect(errorEvent.exception?.values).toHaveLength(1);
14+
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
15+
16+
expect(errorEvent.request).toEqual({
17+
method: 'GET',
18+
cookies: {},
19+
headers: expect.any(Object),
20+
url: 'http://localhost:3030/test-exception/123',
21+
});
22+
23+
expect(errorEvent.transaction).toEqual('GET /test-exception/123');
24+
25+
expect(errorEvent.contexts?.trace).toEqual({
26+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
27+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
28+
});
29+
});
30+
31+
test('Should record caught exceptions with local variable', async ({ baseURL }) => {
32+
const errorEventPromise = waitForError('node-core-express-otel-v2', event => {
33+
return event.transaction === 'GET /test-local-variables-caught';
34+
});
35+
36+
await fetch(`${baseURL}/test-local-variables-caught`);
37+
38+
const errorEvent = await errorEventPromise;
39+
40+
const frames = errorEvent.exception?.values?.[0].stacktrace?.frames;
41+
expect(frames?.[frames.length - 1].vars?.randomVariableToRecord).toBeDefined();
42+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('Sends an API route transaction', async ({ baseURL }) => {
5+
const pageloadTransactionEventPromise = waitForTransaction('node-core-express-otel-v2', transactionEvent => {
6+
return (
7+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
8+
transactionEvent?.transaction === 'GET /test-transaction'
9+
);
10+
});
11+
12+
await fetch(`${baseURL}/test-transaction`);
13+
14+
const transactionEvent = await pageloadTransactionEventPromise;
15+
16+
expect(transactionEvent.contexts?.trace).toEqual({
17+
data: {
18+
'sentry.source': 'url',
19+
'sentry.origin': 'manual',
20+
'sentry.op': 'http.server',
21+
'sentry.sample_rate': 1,
22+
url: 'http://localhost:3030/test-transaction',
23+
'otel.kind': 'SERVER',
24+
'http.response.status_code': 200,
25+
'http.url': 'http://localhost:3030/test-transaction',
26+
'http.host': 'localhost:3030',
27+
'net.host.name': 'localhost',
28+
'http.method': 'GET',
29+
'http.scheme': 'http',
30+
'http.target': '/test-transaction',
31+
'http.user_agent': 'node',
32+
'http.flavor': '1.1',
33+
'net.transport': 'ip_tcp',
34+
'net.host.ip': expect.any(String),
35+
'net.host.port': expect.any(Number),
36+
'net.peer.ip': expect.any(String),
37+
'net.peer.port': expect.any(Number),
38+
'http.status_code': 200,
39+
'http.status_text': 'OK',
40+
},
41+
op: 'http.server',
42+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
43+
status: 'ok',
44+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
45+
origin: 'manual',
46+
});
47+
48+
expect(transactionEvent.contexts?.response).toEqual({
49+
status_code: 200,
50+
});
51+
52+
expect(transactionEvent).toEqual(
53+
expect.objectContaining({
54+
transaction: 'GET /test-transaction',
55+
type: 'transaction',
56+
transaction_info: {
57+
source: 'url',
58+
},
59+
}),
60+
);
61+
});
62+
63+
test('Sends an API route transaction for an errored route', async ({ baseURL }) => {
64+
const transactionEventPromise = waitForTransaction('node-core-express-otel-v2', transactionEvent => {
65+
return (
66+
transactionEvent.contexts?.trace?.op === 'http.server' &&
67+
transactionEvent.transaction === 'GET /test-exception/777' &&
68+
transactionEvent.request?.url === 'http://localhost:3030/test-exception/777'
69+
);
70+
});
71+
72+
await fetch(`${baseURL}/test-exception/777`);
73+
74+
const transactionEvent = await transactionEventPromise;
75+
76+
expect(transactionEvent.contexts?.trace?.op).toEqual('http.server');
77+
expect(transactionEvent.transaction).toEqual('GET /test-exception/777');
78+
expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error');
79+
expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500);
80+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"types": ["node"],
4+
"esModuleInterop": true,
5+
"lib": ["es2020"],
6+
"strict": true,
7+
"outDir": "dist",
8+
"skipLibCheck": true
9+
},
10+
"include": ["src/**/*.ts"]
11+
}

0 commit comments

Comments
 (0)