Skip to content

Commit 1687319

Browse files
committed
Add node-core-light e2e test app
1 parent d6463d2 commit 1687319

File tree

10 files changed

+236
-0
lines changed

10 files changed

+236
-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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "node-core-light-express-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+
"@sentry/node-core": "latest || *",
16+
"@types/express": "^4.17.21",
17+
"@types/node": "^22.0.0",
18+
"express": "^4.21.2",
19+
"typescript": "~5.0.0"
20+
},
21+
"devDependencies": {
22+
"@playwright/test": "~1.56.0",
23+
"@sentry-internal/test-utils": "link:../../../test-utils",
24+
"@sentry/core": "latest || *"
25+
},
26+
"volta": {
27+
"node": "22.18.0"
28+
},
29+
"sentryTest": {
30+
"variants": [
31+
{
32+
"label": "node 22 (light mode, requires Node 22+ for diagnostics_channel)"
33+
}
34+
]
35+
}
36+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: 'pnpm start',
5+
port: 3030,
6+
});
7+
8+
export default config;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import * as Sentry from '@sentry/node-core/light';
2+
import express from 'express';
3+
4+
// IMPORTANT: Initialize Sentry BEFORE creating the Express app
5+
// This is required for automatic request isolation to work
6+
Sentry.init({
7+
dsn: process.env.E2E_TEST_DSN,
8+
debug: true,
9+
tracesSampleRate: 1.0,
10+
tunnel: 'http://localhost:3031/', // Use event proxy for testing
11+
});
12+
13+
// Create Express app AFTER Sentry.init()
14+
const app = express();
15+
const port = 3030;
16+
17+
app.get('/test-error', (_req, res) => {
18+
Sentry.setTag('test', 'error');
19+
Sentry.captureException(new Error('Test error from light mode'));
20+
res.status(500).json({ error: 'Error captured' });
21+
});
22+
23+
app.get('/test-isolation/:userId', async (req, res) => {
24+
const userId = req.params.userId;
25+
26+
const isolationScope = Sentry.getIsolationScope();
27+
const currentScope = Sentry.getCurrentScope();
28+
29+
Sentry.setUser({ id: userId });
30+
Sentry.setTag('user_id', userId);
31+
32+
currentScope.setTag('processing_user', userId);
33+
currentScope.setContext('api_context', {
34+
userId,
35+
timestamp: Date.now(),
36+
});
37+
38+
// Simulate async work with variance so we run into cases where
39+
// the next request comes in before the async work is complete
40+
// to showcase proper request isolation
41+
await new Promise(resolve => setTimeout(resolve, Math.random() * 500 + 100));
42+
43+
// Verify isolation after async operations
44+
const finalIsolationData = isolationScope.getScopeData();
45+
const finalCurrentData = currentScope.getScopeData();
46+
47+
const isIsolated =
48+
finalIsolationData.user?.id === userId &&
49+
finalIsolationData.tags?.user_id === userId &&
50+
finalCurrentData.contexts?.api_context?.userId === userId;
51+
52+
res.json({
53+
userId,
54+
isIsolated,
55+
scope: {
56+
userId: finalIsolationData.user?.id,
57+
userIdTag: finalIsolationData.tags?.user_id,
58+
currentUserId: finalCurrentData.contexts?.api_context?.userId,
59+
},
60+
});
61+
});
62+
63+
app.get('/test-isolation-error/:userId', (req, res) => {
64+
const userId = req.params.userId;
65+
Sentry.setTag('user_id', userId);
66+
Sentry.setUser({ id: userId });
67+
68+
Sentry.captureException(new Error(`Error for user ${userId}`));
69+
res.json({ userId, captured: true });
70+
});
71+
72+
app.get('/health', (_req, res) => {
73+
res.json({ status: 'ok' });
74+
});
75+
76+
app.listen(port, () => {
77+
console.log(`Example app listening on port ${port}`);
78+
});
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-express',
6+
});
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 { waitForError } from '@sentry-internal/test-utils';
3+
4+
test('should capture errors', async ({ request }) => {
5+
const errorEventPromise = waitForError('node-core-light-express', event => {
6+
return event?.exception?.values?.[0]?.value === 'Test error from light mode';
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 mode');
15+
expect(errorEvent.tags?.test).toBe('error');
16+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
// Make 3 concurrent requests with different user IDs
6+
const [response1, response2, response3] = await Promise.all([
7+
request.get('/test-isolation/user-1'),
8+
request.get('/test-isolation/user-2'),
9+
request.get('/test-isolation/user-3'),
10+
]);
11+
12+
const data1 = await response1.json();
13+
const data2 = await response2.json();
14+
const data3 = await response3.json();
15+
16+
// Each response should be properly isolated
17+
expect(data1.isIsolated).toBe(true);
18+
expect(data1.userId).toBe('user-1');
19+
expect(data1.scope.userId).toBe('user-1');
20+
expect(data1.scope.userIdTag).toBe('user-1');
21+
expect(data1.scope.currentUserId).toBe('user-1');
22+
23+
expect(data2.isIsolated).toBe(true);
24+
expect(data2.userId).toBe('user-2');
25+
expect(data2.scope.userId).toBe('user-2');
26+
expect(data2.scope.userIdTag).toBe('user-2');
27+
expect(data2.scope.currentUserId).toBe('user-2');
28+
29+
expect(data3.isIsolated).toBe(true);
30+
expect(data3.userId).toBe('user-3');
31+
expect(data3.scope.userId).toBe('user-3');
32+
expect(data3.scope.userIdTag).toBe('user-3');
33+
expect(data3.scope.currentUserId).toBe('user-3');
34+
});
35+
36+
test('should isolate errors across concurrent requests', async ({ request }) => {
37+
const errorPromises = [
38+
waitForError('node-core-light-express', event => {
39+
return event?.exception?.values?.[0]?.value === 'Error for user user-1';
40+
}),
41+
waitForError('node-core-light-express', event => {
42+
return event?.exception?.values?.[0]?.value === 'Error for user user-2';
43+
}),
44+
waitForError('node-core-light-express', event => {
45+
return event?.exception?.values?.[0]?.value === 'Error for user user-3';
46+
}),
47+
];
48+
49+
// Make 3 concurrent requests that trigger errors
50+
await Promise.all([
51+
request.get('/test-isolation-error/user-1'),
52+
request.get('/test-isolation-error/user-2'),
53+
request.get('/test-isolation-error/user-3'),
54+
]);
55+
56+
const [error1, error2, error3] = await Promise.all(errorPromises);
57+
58+
// Each error should have the correct user data
59+
expect(error1?.user?.id).toBe('user-1');
60+
expect(error1?.tags?.user_id).toBe('user-1');
61+
62+
expect(error2?.user?.id).toBe('user-2');
63+
expect(error2?.tags?.user_id).toBe('user-2');
64+
65+
expect(error3?.user?.id).toBe('user-3');
66+
expect(error3?.tags?.user_id).toBe('user-3');
67+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "Node16",
5+
"moduleResolution": "Node16",
6+
"lib": ["ES2022"],
7+
"outDir": "./dist",
8+
"rootDir": "./src",
9+
"strict": true,
10+
"esModuleInterop": true,
11+
"skipLibCheck": true,
12+
"forceConsistentCasingInFileNames": true,
13+
"resolveJsonModule": true,
14+
"types": ["node"]
15+
},
16+
"include": ["src/**/*"],
17+
"exclude": ["node_modules", "dist"]
18+
}

packages/node-core/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ app.get('/error', (req, res) => {
222222
```
223223

224224
**Caveats:**
225+
225226
- Manual isolation prevents scope data leakage between requests
226227
- However, **distributed tracing will not work correctly** - incoming `sentry-trace` and `baggage` headers won't be automatically extracted and propagated
227228
- For full distributed tracing support, use Node.js 22+ or the full `@sentry/node` SDK with OpenTelemetry

0 commit comments

Comments
 (0)