Skip to content

Commit 1990465

Browse files
committed
Add E2E test app for node-core-light-otlp
1 parent 70d58ad commit 1990465

11 files changed

Lines changed: 308 additions & 0 deletions

File tree

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+
constconfig=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+
exportdefaultconfig;
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*asSentryfrom'@sentry/node-core/light';
6+
import{otlpIntegration}from'@sentry/node-core/light/otlp';
7+
importexpressfrom'express';
8+
9+
constprovider=newNodeTracerProvider({
10+
spanProcessors: [
11+
// The user's own exporter (sends to test proxy for verification)
12+
newBatchSpanProcessor(
13+
newOTLPTraceExporter({
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+
constapp=express();
31+
constport=3030;
32+
consttracer=trace.getTracer('test-app');
33+
34+
app.get('/test-error',(_req,res)=>{
35+
Sentry.setTag('test','error');
36+
Sentry.captureException(newError('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(newError('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+
constuserId=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+
awaitnewPromise(resolve=>setTimeout(resolve,Math.random()*200+50));
59+
60+
constisolationScope=Sentry.getIsolationScope();
61+
constscopeData=isolationScope.getScopeData();
62+
63+
constisIsolated=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+
constuserId=req.params.userId;
77+
Sentry.setTag('user_id',userId);
78+
Sentry.setUser({id: userId});
79+
80+
Sentry.captureException(newError(`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+
consterrorEventPromise=waitForError('node-core-light-otlp',event=>{
6+
returnevent?.exception?.values?.[0]?.value==='Test error from light+otel';
7+
});
8+
9+
constresponse=awaitrequest.get('/test-error');
10+
expect(response.status()).toBe(500);
11+
12+
consterrorEvent=awaiterrorEventPromise;
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+
consterrorEventPromise=waitForError('node-core-light-otlp',event=>{
20+
returnevent?.exception?.values?.[0]?.value==='Error inside OTel span';
21+
});
22+
23+
awaitrequest.get('/test-otel-span');
24+
25+
consterrorEvent=awaiterrorEventPromise;
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+
constotelPromise=waitForPlainRequest('node-core-light-otlp-otel',data=>{
8+
constjson=JSON.parse(data)as{resourceSpans: unknown[]};
9+
returnjson.resourceSpans.length>0;
10+
});
11+
12+
awaitrequest.get('/test-otel-span');
13+
14+
constotelData=awaitotelPromise;
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]=awaitPromise.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+
constdata1=awaitresponse1.json();
12+
constdata2=awaitresponse2.json();
13+
constdata3=awaitresponse3.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+
consterrorPromises=[
33+
waitForError('node-core-light-otlp',event=>{
34+
returnevent?.exception?.values?.[0]?.value==='Error for user user-1';
35+
}),
36+
waitForError('node-core-light-otlp',event=>{
37+
returnevent?.exception?.values?.[0]?.value==='Error for user user-2';
38+
}),
39+
waitForError('node-core-light-otlp',event=>{
40+
returnevent?.exception?.values?.[0]?.value==='Error for user user-3';
41+
}),
42+
];
43+
44+
awaitPromise.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]=awaitPromise.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)