Skip to content

Commit 33f4e09

Browse files
committed
Add E2E test app for node-core-light-otlp
1 parent 4431f59 commit 33f4e09

File tree

11 files changed

+308
-0
lines changed

11 files changed

+308
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
.env
4+
pnpm-lock.yaml
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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "node-core-light-otlp-app",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "tsc",
8+
"start": "node dist/app.js",
9+
"test": "playwright test",
10+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
11+
"test:build": "pnpm install && pnpm build",
12+
"test:assert": "pnpm test"
13+
},
14+
"dependencies": {
15+
"@opentelemetry/api": "^1.9.0",
16+
"@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
17+
"@opentelemetry/sdk-trace-base": "^2.5.1",
18+
"@opentelemetry/sdk-trace-node": "^2.5.1",
19+
"@sentry/node-core": "latest || *",
20+
"@types/express": "^4.17.21",
21+
"@types/node": "^22.0.0",
22+
"express": "^4.21.2",
23+
"typescript": "~5.0.0"
24+
},
25+
"devDependencies": {
26+
"@playwright/test": "~1.56.0",
27+
"@sentry-internal/test-utils": "link:../../../test-utils",
28+
"@sentry/core": "latest || *"
29+
},
30+
"volta": {
31+
"node": "22.18.0"
32+
},
33+
"sentryTest": {
34+
"variants": [
35+
{
36+
"label": "node 22 (light mode + OTLP integration)"
37+
}
38+
]
39+
}
40+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig(
4+
{
5+
startCommand: 'pnpm start',
6+
},
7+
{
8+
webServer: [
9+
{
10+
command: 'node ./start-event-proxy.mjs',
11+
port: 3031,
12+
stdout: 'pipe',
13+
stderr: 'pipe',
14+
},
15+
{
16+
command: 'node ./start-otel-proxy.mjs',
17+
port: 3032,
18+
stdout: 'pipe',
19+
stderr: 'pipe',
20+
},
21+
{
22+
command: 'pnpm start',
23+
port: 3030,
24+
stdout: 'pipe',
25+
stderr: 'pipe',
26+
env: {
27+
PORT: '3030',
28+
},
29+
},
30+
],
31+
},
32+
);
33+
34+
export default config;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { trace } from '@opentelemetry/api';
2+
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
3+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
4+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
5+
import * as Sentry from '@sentry/node-core/light';
6+
import { otlpIntegration } from '@sentry/node-core/light/otlp';
7+
import express from 'express';
8+
9+
const provider = new NodeTracerProvider({
10+
spanProcessors: [
11+
// The user's own exporter (sends to test proxy for verification)
12+
new BatchSpanProcessor(
13+
new OTLPTraceExporter({
14+
url: 'http://localhost:3032/',
15+
}),
16+
),
17+
],
18+
});
19+
20+
provider.register();
21+
22+
Sentry.init({
23+
dsn: process.env.E2E_TEST_DSN,
24+
debug: true,
25+
tracesSampleRate: 1.0,
26+
tunnel: 'http://localhost:3031/', // Use event proxy for testing
27+
integrations: [otlpIntegration()],
28+
});
29+
30+
const app = express();
31+
const port = 3030;
32+
const tracer = trace.getTracer('test-app');
33+
34+
app.get('/test-error', (_req, res) => {
35+
Sentry.setTag('test', 'error');
36+
Sentry.captureException(new Error('Test error from light+otel'));
37+
res.status(500).json({ error: 'Error captured' });
38+
});
39+
40+
app.get('/test-otel-span', (_req, res) => {
41+
tracer.startActiveSpan('test-span', span => {
42+
Sentry.captureException(new Error('Error inside OTel span'));
43+
span.end();
44+
});
45+
46+
res.json({ ok: true });
47+
});
48+
49+
app.get('/test-isolation/:userId', async (req, res) => {
50+
const userId = req.params.userId;
51+
52+
// The light httpIntegration provides request isolation via diagnostics_channel.
53+
// This should still work alongside the OTLP integration.
54+
Sentry.setUser({ id: userId });
55+
Sentry.setTag('user_id', userId);
56+
57+
// Simulate async work
58+
await new Promise(resolve => setTimeout(resolve, Math.random() * 200 + 50));
59+
60+
const isolationScope = Sentry.getIsolationScope();
61+
const scopeData = isolationScope.getScopeData();
62+
63+
const isIsolated = scopeData.user?.id === userId && scopeData.tags?.user_id === userId;
64+
65+
res.json({
66+
userId,
67+
isIsolated,
68+
scope: {
69+
userId: scopeData.user?.id,
70+
userIdTag: scopeData.tags?.user_id,
71+
},
72+
});
73+
});
74+
75+
app.get('/test-isolation-error/:userId', (req, res) => {
76+
const userId = req.params.userId;
77+
Sentry.setTag('user_id', userId);
78+
Sentry.setUser({ id: userId });
79+
80+
Sentry.captureException(new Error(`Error for user ${userId}`));
81+
res.json({ userId, captured: true });
82+
});
83+
84+
app.get('/health', (_req, res) => {
85+
res.json({ status: 'ok' });
86+
});
87+
88+
app.listen(port, () => {
89+
console.log(`Example app listening on port ${port}`);
90+
});
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-light-otlp',
6+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startProxyServer } from '@sentry-internal/test-utils';
2+
3+
startProxyServer({
4+
port: 3032,
5+
proxyServerName: 'node-core-light-otlp-otel',
6+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
test('should capture errors with correct tags', async ({ request }) => {
5+
const errorEventPromise = waitForError('node-core-light-otlp', event => {
6+
return event?.exception?.values?.[0]?.value === 'Test error from light+otel';
7+
});
8+
9+
const response = await request.get('/test-error');
10+
expect(response.status()).toBe(500);
11+
12+
const errorEvent = await errorEventPromise;
13+
expect(errorEvent).toBeDefined();
14+
expect(errorEvent.exception?.values?.[0]?.value).toBe('Test error from light+otel');
15+
expect(errorEvent.tags?.test).toBe('error');
16+
});
17+
18+
test('should link error events to the active OTel trace context', async ({ request }) => {
19+
const errorEventPromise = waitForError('node-core-light-otlp', event => {
20+
return event?.exception?.values?.[0]?.value === 'Error inside OTel span';
21+
});
22+
23+
await request.get('/test-otel-span');
24+
25+
const errorEvent = await errorEventPromise;
26+
expect(errorEvent).toBeDefined();
27+
28+
// The error event should have trace context from the OTel span
29+
expect(errorEvent.contexts?.trace).toBeDefined();
30+
expect(errorEvent.contexts?.trace?.trace_id).toMatch(/[a-f0-9]{32}/);
31+
expect(errorEvent.contexts?.trace?.span_id).toMatch(/[a-f0-9]{16}/);
32+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForPlainRequest } from '@sentry-internal/test-utils';
3+
4+
test('User OTel exporter still receives spans', async ({ request }) => {
5+
// The user's own OTel exporter sends spans to port 3032 (our test proxy).
6+
// Verify that OTel span export still works alongside the Sentry OTLP integration.
7+
const otelPromise = waitForPlainRequest('node-core-light-otlp-otel', data => {
8+
const json = JSON.parse(data) as { resourceSpans: unknown[] };
9+
return json.resourceSpans.length > 0;
10+
});
11+
12+
await request.get('/test-otel-span');
13+
14+
const otelData = await otelPromise;
15+
expect(otelData).toBeDefined();
16+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
test('should isolate scope data across concurrent requests', async ({ request }) => {
5+
const [response1, response2, response3] = await Promise.all([
6+
request.get('/test-isolation/user-1'),
7+
request.get('/test-isolation/user-2'),
8+
request.get('/test-isolation/user-3'),
9+
]);
10+
11+
const data1 = await response1.json();
12+
const data2 = await response2.json();
13+
const data3 = await response3.json();
14+
15+
expect(data1.isIsolated).toBe(true);
16+
expect(data1.userId).toBe('user-1');
17+
expect(data1.scope.userId).toBe('user-1');
18+
expect(data1.scope.userIdTag).toBe('user-1');
19+
20+
expect(data2.isIsolated).toBe(true);
21+
expect(data2.userId).toBe('user-2');
22+
expect(data2.scope.userId).toBe('user-2');
23+
expect(data2.scope.userIdTag).toBe('user-2');
24+
25+
expect(data3.isIsolated).toBe(true);
26+
expect(data3.userId).toBe('user-3');
27+
expect(data3.scope.userId).toBe('user-3');
28+
expect(data3.scope.userIdTag).toBe('user-3');
29+
});
30+
31+
test('should isolate errors across concurrent requests', async ({ request }) => {
32+
const errorPromises = [
33+
waitForError('node-core-light-otlp', event => {
34+
return event?.exception?.values?.[0]?.value === 'Error for user user-1';
35+
}),
36+
waitForError('node-core-light-otlp', event => {
37+
return event?.exception?.values?.[0]?.value === 'Error for user user-2';
38+
}),
39+
waitForError('node-core-light-otlp', event => {
40+
return event?.exception?.values?.[0]?.value === 'Error for user user-3';
41+
}),
42+
];
43+
44+
await Promise.all([
45+
request.get('/test-isolation-error/user-1'),
46+
request.get('/test-isolation-error/user-2'),
47+
request.get('/test-isolation-error/user-3'),
48+
]);
49+
50+
const [error1, error2, error3] = await Promise.all(errorPromises);
51+
52+
expect(error1?.user?.id).toBe('user-1');
53+
expect(error1?.tags?.user_id).toBe('user-1');
54+
55+
expect(error2?.user?.id).toBe('user-2');
56+
expect(error2?.tags?.user_id).toBe('user-2');
57+
58+
expect(error3?.user?.id).toBe('user-3');
59+
expect(error3?.tags?.user_id).toBe('user-3');
60+
});

0 commit comments

Comments
 (0)