Skip to content

Commit d6463d2

Browse files
committed
feat(node-core): Add node-core/light
1 parent d1b86bd commit d6463d2

File tree

10 files changed

+1015
-124
lines changed

10 files changed

+1015
-124
lines changed

packages/node-core/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,116 @@ If it is not possible for you to pass the `--import` flag to the Node.js binary,
116116
NODE_OPTIONS="--import ./instrument.mjs" npm run start
117117
```
118118

119+
## Errors-only Lightweight Mode
120+
121+
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:
122+
123+
- Applications that only need error tracking
124+
- Reducing bundle size and runtime overhead
125+
- Environments where OpenTelemetry isn't needed
126+
127+
### Installation (Light Mode)
128+
129+
```bash
130+
npm install @sentry/node-core
131+
132+
# Or yarn
133+
yarn add @sentry/node-core
134+
```
135+
136+
### Usage (Light Mode)
137+
138+
Import from `@sentry/node-core/light` instead of `@sentry/node-core`:
139+
140+
```js
141+
// ESM
142+
import * as Sentry from '@sentry/node-core/light';
143+
144+
// CJS
145+
const Sentry = require('@sentry/node-core/light');
146+
147+
// Initialize Sentry BEFORE creating your HTTP server
148+
Sentry.init({
149+
dsn: '__DSN__',
150+
// ...
151+
});
152+
153+
// Then create your server (Express, Fastify, etc.)
154+
const app = express();
155+
```
156+
157+
**Important:** Initialize Sentry **before** creating your HTTP server to enable automatic request isolation.
158+
159+
### Features in Light Mode
160+
161+
**Included:**
162+
163+
- Error tracking and reporting
164+
- Automatic request isolation (Node.js 22+)
165+
- Breadcrumbs
166+
- Context and user data
167+
- Local variables capture
168+
- Distributed tracing (via `sentry-trace` and `baggage` headers)
169+
170+
**Not included:**
171+
172+
- Performance monitoring (no spans/transactions)
173+
174+
### Automatic Request Isolation
175+
176+
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.
177+
178+
No manual middleware or `--import` flag is required - just initialize Sentry before creating your server:
179+
180+
```js
181+
import * as Sentry from '@sentry/node-core/light';
182+
import express from 'express';
183+
184+
// Initialize FIRST
185+
Sentry.init({ dsn: '__DSN__' });
186+
187+
// Then create server
188+
const app = express();
189+
190+
app.get('/error', (req, res) => {
191+
// This data is automatically isolated per request
192+
Sentry.setTag('userId', req.params.id);
193+
Sentry.captureException(new Error('Something went wrong'));
194+
res.status(500).send('Error');
195+
});
196+
```
197+
198+
### Manual Request Isolation (Node.js < 22)
199+
200+
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`:
201+
202+
```js
203+
import * as Sentry from '@sentry/node-core/light';
204+
import express from 'express';
205+
206+
Sentry.init({ dsn: '__DSN__' });
207+
208+
const app = express();
209+
210+
// Add middleware to manually isolate requests
211+
app.use((req, res, next) => {
212+
Sentry.withIsolationScope(() => {
213+
next();
214+
});
215+
});
216+
217+
app.get('/error', (req, res) => {
218+
Sentry.setTag('userId', req.params.id);
219+
Sentry.captureException(new Error('Something went wrong'));
220+
res.status(500).send('Error');
221+
});
222+
```
223+
224+
**Caveats:**
225+
- Manual isolation prevents scope data leakage between requests
226+
- However, **distributed tracing will not work correctly** - incoming `sentry-trace` and `baggage` headers won't be automatically extracted and propagated
227+
- For full distributed tracing support, use Node.js 22+ or the full `@sentry/node` SDK with OpenTelemetry
228+
119229
## Links
120230

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

packages/node-core/package.json

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@
2727
"default": "./build/cjs/index.js"
2828
}
2929
},
30+
"./light": {
31+
"import": {
32+
"types": "./build/types/light/index.d.ts",
33+
"default": "./build/esm/light/index.js"
34+
},
35+
"require": {
36+
"types": "./build/types/light/index.d.ts",
37+
"default": "./build/cjs/light/index.js"
38+
}
39+
},
3040
"./import": {
3141
"import": {
3242
"default": "./build/import-hook.mjs"
@@ -63,12 +73,38 @@
6373
"@opentelemetry/instrumentation": ">=0.57.1 <1",
6474
"@opentelemetry/resources": "^1.30.1 || ^2.1.0 || ^2.2.0",
6575
"@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0 || ^2.2.0",
66-
"@opentelemetry/semantic-conventions": "^1.37.0"
76+
"@opentelemetry/semantic-conventions": "^1.37.0",
77+
"@sentry/opentelemetry": "10.30.0"
78+
},
79+
"peerDependenciesMeta": {
80+
"@opentelemetry/api": {
81+
"optional": true
82+
},
83+
"@opentelemetry/context-async-hooks": {
84+
"optional": true
85+
},
86+
"@opentelemetry/core": {
87+
"optional": true
88+
},
89+
"@opentelemetry/instrumentation": {
90+
"optional": true
91+
},
92+
"@opentelemetry/resources": {
93+
"optional": true
94+
},
95+
"@opentelemetry/sdk-trace-base": {
96+
"optional": true
97+
},
98+
"@opentelemetry/semantic-conventions": {
99+
"optional": true
100+
},
101+
"@sentry/opentelemetry": {
102+
"optional": true
103+
}
67104
},
68105
"dependencies": {
69106
"@apm-js-collab/tracing-hooks": "^0.3.1",
70107
"@sentry/core": "10.30.0",
71-
"@sentry/opentelemetry": "10.30.0",
72108
"import-in-the-middle": "^2"
73109
},
74110
"devDependencies": {
@@ -80,6 +116,7 @@
80116
"@opentelemetry/resources": "^2.2.0",
81117
"@opentelemetry/sdk-trace-base": "^2.2.0",
82118
"@opentelemetry/semantic-conventions": "^1.37.0",
119+
"@sentry/opentelemetry": "10.30.0",
83120
"@types/node": "^18.19.1"
84121
},
85122
"scripts": {

packages/node-core/rollup.npm.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default [
1919
localVariablesWorkerConfig,
2020
...makeNPMConfigVariants(
2121
makeBaseNPMConfig({
22-
entrypoints: ['src/index.ts', 'src/init.ts'],
22+
entrypoints: ['src/index.ts', 'src/init.ts', 'src/light/index.ts'],
2323
packageSpecificConfig: {
2424
output: {
2525
// set exports to 'named' or 'auto' so that rollup doesn't warn

packages/node-core/src/integrations/http/httpServerIntegration.ts

Lines changed: 6 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from '@sentry/core';
1919
import { DEBUG_BUILD } from '../../debug-build';
2020
import type { NodeClient } from '../../sdk/client';
21-
import { MAX_BODY_BYTE_LENGTH } from './constants';
21+
import { patchRequestToCaptureBody } from '../../utils/captureRequestBody';
2222

2323
type ServerEmit = typeof Server.prototype.emit;
2424

@@ -128,6 +128,10 @@ const _httpServerIntegration = ((options: HttpServerIntegrationOptions = {}) =>
128128
/**
129129
* This integration handles request isolation, trace continuation and other core Sentry functionality around incoming http requests
130130
* handled via the node `http` module.
131+
*
132+
* This version uses OpenTelemetry for context propagation and span management.
133+
*
134+
* @see {@link ../../light/integrations/httpServerIntegration.ts} for the lightweight version without OpenTelemetry
131135
*/
132136
export const httpServerIntegration = _httpServerIntegration as (
133137
options?: HttpServerIntegrationOptions,
@@ -189,7 +193,7 @@ function instrumentServer(
189193

190194
const url = request.url || '/';
191195
if (maxRequestBodySize !== 'none' && !ignoreRequestBody?.(url, request)) {
192-
patchRequestToCaptureBody(request, isolationScope, maxRequestBodySize);
196+
patchRequestToCaptureBody(request, isolationScope, maxRequestBodySize, INTEGRATION_NAME);
193197
}
194198

195199
// Update the isolation scope, isolate this request
@@ -315,122 +319,3 @@ export function recordRequestSession(
315319
}
316320
});
317321
}
318-
319-
/**
320-
* This method patches the request object to capture the body.
321-
* Instead of actually consuming the streamed body ourselves, which has potential side effects,
322-
* we monkey patch `req.on('data')` to intercept the body chunks.
323-
* This way, we only read the body if the user also consumes the body, ensuring we do not change any behavior in unexpected ways.
324-
*/
325-
function patchRequestToCaptureBody(
326-
req: IncomingMessage,
327-
isolationScope: Scope,
328-
maxIncomingRequestBodySize: 'small' | 'medium' | 'always',
329-
): void {
330-
let bodyByteLength = 0;
331-
const chunks: Buffer[] = [];
332-
333-
DEBUG_BUILD && debug.log(INTEGRATION_NAME, 'Patching request.on');
334-
335-
/**
336-
* We need to keep track of the original callbacks, in order to be able to remove listeners again.
337-
* Since `off` depends on having the exact same function reference passed in, we need to be able to map
338-
* original listeners to our wrapped ones.
339-
*/
340-
const callbackMap = new WeakMap();
341-
342-
const maxBodySize =
343-
maxIncomingRequestBodySize === 'small'
344-
? 1_000
345-
: maxIncomingRequestBodySize === 'medium'
346-
? 10_000
347-
: MAX_BODY_BYTE_LENGTH;
348-
349-
try {
350-
// eslint-disable-next-line @typescript-eslint/unbound-method
351-
req.on = new Proxy(req.on, {
352-
apply: (target, thisArg, args: Parameters<typeof req.on>) => {
353-
const [event, listener, ...restArgs] = args;
354-
355-
if (event === 'data') {
356-
DEBUG_BUILD &&
357-
debug.log(INTEGRATION_NAME, `Handling request.on("data") with maximum body size of ${maxBodySize}b`);
358-
359-
const callback = new Proxy(listener, {
360-
apply: (target, thisArg, args: Parameters<typeof listener>) => {
361-
try {
362-
const chunk = args[0] as Buffer | string;
363-
const bufferifiedChunk = Buffer.from(chunk);
364-
365-
if (bodyByteLength < maxBodySize) {
366-
chunks.push(bufferifiedChunk);
367-
bodyByteLength += bufferifiedChunk.byteLength;
368-
} else if (DEBUG_BUILD) {
369-
debug.log(
370-
INTEGRATION_NAME,
371-
`Dropping request body chunk because maximum body length of ${maxBodySize}b is exceeded.`,
372-
);
373-
}
374-
} catch (err) {
375-
DEBUG_BUILD && debug.error(INTEGRATION_NAME, 'Encountered error while storing body chunk.');
376-
}
377-
378-
return Reflect.apply(target, thisArg, args);
379-
},
380-
});
381-
382-
callbackMap.set(listener, callback);
383-
384-
return Reflect.apply(target, thisArg, [event, callback, ...restArgs]);
385-
}
386-
387-
return Reflect.apply(target, thisArg, args);
388-
},
389-
});
390-
391-
// Ensure we also remove callbacks correctly
392-
// eslint-disable-next-line @typescript-eslint/unbound-method
393-
req.off = new Proxy(req.off, {
394-
apply: (target, thisArg, args: Parameters<typeof req.off>) => {
395-
const [, listener] = args;
396-
397-
const callback = callbackMap.get(listener);
398-
if (callback) {
399-
callbackMap.delete(listener);
400-
401-
const modifiedArgs = args.slice();
402-
modifiedArgs[1] = callback;
403-
return Reflect.apply(target, thisArg, modifiedArgs);
404-
}
405-
406-
return Reflect.apply(target, thisArg, args);
407-
},
408-
});
409-
410-
req.on('end', () => {
411-
try {
412-
const body = Buffer.concat(chunks).toString('utf-8');
413-
if (body) {
414-
// Using Buffer.byteLength here, because the body may contain characters that are not 1 byte long
415-
const bodyByteLength = Buffer.byteLength(body, 'utf-8');
416-
const truncatedBody =
417-
bodyByteLength > maxBodySize
418-
? `${Buffer.from(body)
419-
.subarray(0, maxBodySize - 3)
420-
.toString('utf-8')}...`
421-
: body;
422-
423-
isolationScope.setSDKProcessingMetadata({ normalizedRequest: { data: truncatedBody } });
424-
}
425-
} catch (error) {
426-
if (DEBUG_BUILD) {
427-
debug.error(INTEGRATION_NAME, 'Error building captured request body', error);
428-
}
429-
}
430-
});
431-
} catch (error) {
432-
if (DEBUG_BUILD) {
433-
debug.error(INTEGRATION_NAME, 'Error patching request to capture body', error);
434-
}
435-
}
436-
}

0 commit comments

Comments
 (0)