Skip to content
Draft
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
@@ -0,0 +1,4 @@
node_modules
dist
.env
pnpm-lock.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://127.0.0.1:4873
@sentry-internal:registry=http://127.0.0.1:4873
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "node-core-light-express-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/app.js",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/node-core": "latest || *",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"express": "^4.21.2",
"typescript": "~5.0.0"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *"
},
"volta": {
"node": "22.18.0"
},
"sentryTest": {
"variants": [
{
"label": "node 22 (light mode, requires Node 22+ for diagnostics_channel)"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getPlaywrightConfig } from '@sentry-internal/test-utils';

const config = getPlaywrightConfig({
startCommand: 'pnpm start',
port: 3030,
});

export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as Sentry from '@sentry/node-core/light';
import express from 'express';

// IMPORTANT: Initialize Sentry BEFORE creating the Express app
// This is required for automatic request isolation to work
Sentry.init({
dsn: process.env.E2E_TEST_DSN,
debug: true,
tracesSampleRate: 1.0,
tunnel: 'http://localhost:3031/', // Use event proxy for testing
});

// Create Express app AFTER Sentry.init()
const app = express();
const port = 3030;

app.get('/test-error', (_req, res) => {
Sentry.setTag('test', 'error');
Sentry.captureException(new Error('Test error from light mode'));
res.status(500).json({ error: 'Error captured' });
});

app.get('/test-isolation/:userId', async (req, res) => {
const userId = req.params.userId;

const isolationScope = Sentry.getIsolationScope();
const currentScope = Sentry.getCurrentScope();

Sentry.setUser({ id: userId });
Sentry.setTag('user_id', userId);

currentScope.setTag('processing_user', userId);
currentScope.setContext('api_context', {
userId,
timestamp: Date.now(),
});

// Simulate async work with variance so we run into cases where
// the next request comes in before the async work is complete
// to showcase proper request isolation
await new Promise(resolve => setTimeout(resolve, Math.random() * 500 + 100));

// Verify isolation after async operations
const finalIsolationData = isolationScope.getScopeData();
const finalCurrentData = currentScope.getScopeData();

const isIsolated =
finalIsolationData.user?.id === userId &&
finalIsolationData.tags?.user_id === userId &&
finalCurrentData.contexts?.api_context?.userId === userId;

res.json({
userId,
isIsolated,
scope: {
userId: finalIsolationData.user?.id,
userIdTag: finalIsolationData.tags?.user_id,
currentUserId: finalCurrentData.contexts?.api_context?.userId,
},
});
});

app.get('/test-isolation-error/:userId', (req, res) => {
const userId = req.params.userId;
Sentry.setTag('user_id', userId);
Sentry.setUser({ id: userId });

Sentry.captureException(new Error(`Error for user ${userId}`));
res.json({ userId, captured: true });
});

app.get('/health', (_req, res) => {
res.json({ status: 'ok' });
});

app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'node-core-light-express',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

test('should capture errors', async ({ request }) => {
const errorEventPromise = waitForError('node-core-light-express', event => {
return event?.exception?.values?.[0]?.value === 'Test error from light mode';
});

const response = await request.get('/test-error');
expect(response.status()).toBe(500);

const errorEvent = await errorEventPromise;
expect(errorEvent).toBeDefined();
expect(errorEvent.exception?.values?.[0]?.value).toBe('Test error from light mode');
expect(errorEvent.tags?.test).toBe('error');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

test('should isolate scope data across concurrent requests', async ({ request }) => {
// Make 3 concurrent requests with different user IDs
const [response1, response2, response3] = await Promise.all([
request.get('/test-isolation/user-1'),
request.get('/test-isolation/user-2'),
request.get('/test-isolation/user-3'),
]);

const data1 = await response1.json();
const data2 = await response2.json();
const data3 = await response3.json();

// Each response should be properly isolated
expect(data1.isIsolated).toBe(true);
expect(data1.userId).toBe('user-1');
expect(data1.scope.userId).toBe('user-1');
expect(data1.scope.userIdTag).toBe('user-1');
expect(data1.scope.currentUserId).toBe('user-1');

expect(data2.isIsolated).toBe(true);
expect(data2.userId).toBe('user-2');
expect(data2.scope.userId).toBe('user-2');
expect(data2.scope.userIdTag).toBe('user-2');
expect(data2.scope.currentUserId).toBe('user-2');

expect(data3.isIsolated).toBe(true);
expect(data3.userId).toBe('user-3');
expect(data3.scope.userId).toBe('user-3');
expect(data3.scope.userIdTag).toBe('user-3');
expect(data3.scope.currentUserId).toBe('user-3');
});

test('should isolate errors across concurrent requests', async ({ request }) => {
const errorPromises = [
waitForError('node-core-light-express', event => {
return event?.exception?.values?.[0]?.value === 'Error for user user-1';
}),
waitForError('node-core-light-express', event => {
return event?.exception?.values?.[0]?.value === 'Error for user user-2';
}),
waitForError('node-core-light-express', event => {
return event?.exception?.values?.[0]?.value === 'Error for user user-3';
}),
];

// Make 3 concurrent requests that trigger errors
await Promise.all([
request.get('/test-isolation-error/user-1'),
request.get('/test-isolation-error/user-2'),
request.get('/test-isolation-error/user-3'),
]);

const [error1, error2, error3] = await Promise.all(errorPromises);

// Each error should have the correct user data
expect(error1?.user?.id).toBe('user-1');
expect(error1?.tags?.user_id).toBe('user-1');

expect(error2?.user?.id).toBe('user-2');
expect(error2?.tags?.user_id).toBe('user-2');

expect(error3?.user?.id).toBe('user-3');
expect(error3?.tags?.user_id).toBe('user-3');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
111 changes: 111 additions & 0 deletions packages/node-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,117 @@ If it is not possible for you to pass the `--import` flag to the Node.js binary,
NODE_OPTIONS="--import ./instrument.mjs" npm run start
```

## Errors-only Lightweight Mode

If you only need error monitoring without performance tracing, you can use the lightweight mode which doesn't require OpenTelemetry dependencies. This mode is ideal for:

- Applications that only need error tracking
- Reducing bundle size and runtime overhead
- Environments where OpenTelemetry isn't needed

### Installation (Light Mode)

```bash
npm install @sentry/node-core

# Or yarn
yarn add @sentry/node-core
```

### Usage (Light Mode)

Import from `@sentry/node-core/light` instead of `@sentry/node-core`:

```js
// ESM
import * as Sentry from '@sentry/node-core/light';

// CJS
const Sentry = require('@sentry/node-core/light');

// Initialize Sentry BEFORE creating your HTTP server
Sentry.init({
dsn: '__DSN__',
// ...
});

// Then create your server (Express, Fastify, etc.)
const app = express();
```

**Important:** Initialize Sentry **before** creating your HTTP server to enable automatic request isolation.

### Features in Light Mode

**Included:**

- Error tracking and reporting
- Automatic request isolation (Node.js 22+)
- Breadcrumbs
- Context and user data
- Local variables capture
- Distributed tracing (via `sentry-trace` and `baggage` headers)

**Not included:**

- Performance monitoring (no spans/transactions)

### Automatic Request Isolation

Light mode includes automatic request isolation for HTTP servers (requires Node.js 22+). This ensures that context (tags, user data, breadcrumbs) set during a request doesn't leak to other concurrent requests.

No manual middleware or `--import` flag is required - just initialize Sentry before creating your server:

```js
import * as Sentry from '@sentry/node-core/light';
import express from 'express';

// Initialize FIRST
Sentry.init({ dsn: '__DSN__' });

// Then create server
const app = express();

app.get('/error', (req, res) => {
// This data is automatically isolated per request
Sentry.setTag('userId', req.params.id);
Sentry.captureException(new Error('Something went wrong'));
res.status(500).send('Error');
});
```

### Manual Request Isolation (Node.js < 22)

If you're using Node.js versions below 22, automatic request isolation is not available. You'll need to manually wrap your request handlers with `withIsolationScope`:

```js
import * as Sentry from '@sentry/node-core/light';
import express from 'express';

Sentry.init({ dsn: '__DSN__' });

const app = express();

// Add middleware to manually isolate requests
app.use((req, res, next) => {
Sentry.withIsolationScope(() => {
next();
});
});

app.get('/error', (req, res) => {
Sentry.setTag('userId', req.params.id);
Sentry.captureException(new Error('Something went wrong'));
res.status(500).send('Error');
});
```

**Caveats:**

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

## Links

- [Official SDK Docs](https://docs.sentry.io/quickstart/)
Loading
Loading