Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(node-core): Add node-core/light#18502

Open
andreiborza wants to merge 18 commits intodevelopfrom
ab/node-core-light
Open

feat(node-core): Add node-core/light#18502
andreiborza wants to merge 18 commits intodevelopfrom
ab/node-core-light

Conversation

@andreiborza
Copy link
Member

@andreiborza andreiborza commented Dec 15, 2025

This PR adds a lightweight version of the node-core SDK that doesn't include nor requires OpenTelemetry dependencies. It provides a basic error-tracking SDK with support for request isolation via AsyncLocalStorage and basic tracing abilities
via our Sentry.startSpan* apis.

Request isolation requires Node 22.12.0+. On lower Node versions, manual wrapping via Sentry.withIsolationScope is necessary to isolate requests.

Closes #19157 (added automatically)

@github-actions
Copy link
Contributor

github-actions bot commented Dec 15, 2025

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,839 - 9,178 -4%
GET With Sentry 1,723 19% 1,725 -0%
GET With Sentry (error only) 6,139 69% 6,156 -0%
POST Baseline 1,218 - 1,217 +0%
POST With Sentry 595 49% 609 -2%
POST With Sentry (error only) 1,067 88% 1,068 -0%
MYSQL Baseline 3,314 - 3,347 -1%
MYSQL With Sentry 433 13% 485 -11%
MYSQL With Sentry (error only) 2,676 81% 2,735 -2%

View base workflow run

@andreiborza andreiborza force-pushed the ab/node-core-light branch 5 times, most recently from 1687319 to 773cb0a Compare December 18, 2025 14:47
@andreiborza andreiborza force-pushed the ab/node-core-light branch 2 times, most recently from 5f3ebcb to f5bdfc9 Compare January 27, 2026 11:26
@github-actions
Copy link
Contributor

github-actions bot commented Jan 28, 2026

size-limit report 📦

Path Size % Change Change
@sentry/browser 25.54 kB - -
@sentry/browser - with treeshaking flags 24 kB - -
@sentry/browser (incl. Tracing) 42.37 kB - -
@sentry/browser (incl. Tracing, Profiling) 47.02 kB - -
@sentry/browser (incl. Tracing, Replay) 81.01 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 70.61 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 85.71 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 97.89 kB - -
@sentry/browser (incl. Feedback) 42.26 kB - -
@sentry/browser (incl. sendFeedback) 30.22 kB - -
@sentry/browser (incl. FeedbackAsync) 35.23 kB - -
@sentry/browser (incl. Metrics) 26.65 kB - -
@sentry/browser (incl. Logs) 26.79 kB - -
@sentry/browser (incl. Metrics & Logs) 27.47 kB - -
@sentry/react 27.25 kB - -
@sentry/react (incl. Tracing) 44.63 kB - -
@sentry/vue 29.98 kB - -
@sentry/vue (incl. Tracing) 44.21 kB - -
@sentry/svelte 25.55 kB - -
CDN Bundle 28.08 kB - -
CDN Bundle (incl. Tracing) 43.15 kB - -
CDN Bundle (incl. Logs, Metrics) 28.92 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 43.99 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 67.86 kB - -
CDN Bundle (incl. Tracing, Replay) 79.91 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 80.77 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 85.33 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 86.23 kB - -
CDN Bundle - uncompressed 82.12 kB - -
CDN Bundle (incl. Tracing) - uncompressed 127.83 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 84.95 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 130.66 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 208.33 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 244.43 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 247.25 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 257.23 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 260.04 kB - -
@sentry/nextjs (client) 46.97 kB - -
@sentry/sveltekit (client) 42.77 kB - -
@sentry/node-core 52.19 kB +0.02% +6 B 🔺
@sentry/node 166.31 kB +0.01% +11 B 🔺
@sentry/node - without tracing 93.97 kB +0.01% +9 B 🔺
@sentry/aws-serverless 109.49 kB +0.02% +11 B 🔺

View base workflow run

@andreiborza andreiborza force-pushed the ab/node-core-light branch 3 times, most recently from 748eb5f to fdb1403 Compare January 30, 2026 15:05
@github-actions
Copy link
Contributor

github-actions bot commented Jan 30, 2026

Codecov Results 📊


Generated by Codecov Action

"dependencies": {
"@apm-js-collab/tracing-hooks": "^0.3.1",
"@sentry/core": "10.38.0",
"@sentry/opentelemetry": "10.38.0",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, this is a breaking change. That being said, the only place where we document node-core is in the README and the README already advised users to install @sentry/opentelemetry so... maaaybe this is ok?

## Installation
```bash
npm install @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/context-async-hooks @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions
# Or yarn
yarn add @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/context-async-hooks @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions
```

@andreiborza andreiborza marked this pull request as ready for review February 4, 2026 11:53
@andreiborza
Copy link
Member Author

To reviewers: Sorry for the big PR, I tried to split this up via commits. The first one adds all the core logic, the rest are testing related.

Copy link
Member

@chargome chargome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally LGTM!

serverName,
};

applySdkMetadata(clientOptions, 'node');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we mark this as node-light here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, yea we can. We didn't want to do that for node-core because every node SDK is based on it but I think it makes sense for node-light.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 01a4339

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be hard to miss adding exports here. Should we do something similar like dev-packages/e2e-tests/test-applications/node-exports-test-app? Or maybe add this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I'll add a test app.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternatively, is there a way to have a common exports file so that we end up with discepancies in the first place? Just a thought, feel free to disregard

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, went with a common exports file in efc79f8.

maxRequestBodySize?: 'none' | 'small' | 'medium' | 'always';
}

const _httpServerIntegration = ((options: HttpServerIntegrationOptions = {}) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Should we call this something else like HttpServerLightIntegration? Might be confusing to have this flying around a couple of times

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I think this would be more confusing from a user's perspective. The light SDK has only one http server integration, calling it something else would only really benefit us maintainers. I think this is fine as is tbh.

* in light mode (without OpenTelemetry).
*
* This is a lightweight alternative to the OpenTelemetry-based httpServerIntegration.
* It uses Node's native AsyncLocalStorage for scope isolation and Sentry's continueTrace for propagation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small docs thing to be more specific:
"for trace propagation"

return target.apply(thisArg, args);
}

DEBUG_BUILD && debug.log(INTEGRATION_NAME, 'Handling incoming request (light mode)');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe call it "(node-light)" -> People could think about a theme when first reading "light mode" and might be confused.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this completely in f1084ce because there's no other mode when you use the light SDK and it's potentially confusing to users.

}

// Update the isolation scope, isolate this request
isolationScope.setSDKProcessingMetadata({ normalizedRequest, ipAddress });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M: Setting the IP address should be guarded with sendDefaultPii. I think this is not done here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! should be h: really 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ed91667 and added tests around this. Technically this will never end on envelopes because SDKprocessingMetaData is always wiped off of envelopes but it's better to guard against it here anyway.

I will send a separate PR for the same issue in node-core.

}

function withIsolationScope<T>(callback: (isolationScope: Scope) => T): T {
// FIX: Clone current scope as well to prevent leakage between concurrent requests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Is this something that still needs to be fixed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already in here, I left the comment to indicate that this was previously not done but I think I'll just remove it. Now that I read it again it feels out of place.

import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

test('should isolate scope data across concurrent requests', async ({ request }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: can we still test againt the traceId in event.contexts.trace? IIUC Node-light doesn't start any spans but it should still recycle trace ids for errors. Otherwise the product will show related errors that have nothing to do with each other.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Added in 48c9f19.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: another test scenario I think we should cover: What happens when a service instrumented with node-code/light would receive incoming sentry-trace and baggage headers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, added in 48c9f19.

this._flushOutcomes();
};

this._clientReportInterval = setInterval(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super-l: We could use safeUnref here instead of manually calling .unref(). Won't make a difference in Node though, so feel free to ignore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safeUnref is a @sentry/core internal, I didn't want to export it as public api for this though...

Comment on lines 17 to 18
// eslint-disable-next-line deprecation/deprecation
export { anrIntegration, disableAnrDetectionForCallback } from '../integrations/anr';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: WDYT about not even exporting the deprecated exports? Theoretically this isn't breaking and we don't have to do it later this way. But no strong feelings either way.

Copy link
Member Author

@andreiborza andreiborza Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yea I like that idea. No reason to ship something new that'll be immediately taken away.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worst-case, if anyone requests it we can still add it 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea. Removed in eae82cf.

Comment on lines 47 to 48
// eslint-disable-next-line deprecation/deprecation
inboundFiltersIntegration(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: we could make the switch here already (see other comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, switched in eae82cf.

Comment on lines +113 to +112
const scope = getCurrentScope();
scope.update(options.initialScope);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l/side-question: Should we write the initial scope onto the global scope? This is probably copied from the main node-core init so it's probably something to discuss outside of this PR. Just curious on your thoughts.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right, this should ideally be put onto the global scope. That being said, the final outcome is currently the same because of the cloning I added to withIsolationScope.

I'd say let's defer this, maybe we can bike-shed about it on Thursday.

NODE_OPTIONS="--import ./instrument.mjs" npm run start
```

## Errors-only Lightweight Mode
Copy link
Member

@Lms24 Lms24 Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: I feel like marketing node-light as "Errors-only" is underselling it a bit. Logs and metrics also work in this package. Assuming I didn't miss something, all three telemetry items would still be trace-connected so users can still make use of it. It even handles Tracing without Performance (god this name didn't age well).

I also struggle with a great name alternative but what about something like

  • Span-less Lightweight Mode
  • Passive Tracing Mode
  • Just plain old "Lightweight Mode" ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it's definitely not an errors-only SDK and calling it that will probably be misleading and confusing for users and at worst will have people not use any other features even if they would have otherwise.

I also struggled with naming this... I think I'll go with "Lightweight Mode" and rely on the descriptions to explain what that means.

Thanks for the input on this!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 9433879.


> **⚠️ Experimental**: The `@sentry/node-core/light` subpath export is experimental and may receive breaking changes in minor or patch releases.

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: I would at least scratch "performance", maybe even replace "performance tracing" with:

Suggested change
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:
If you only need error monitoring without spans, you can use the lightweight mode which doesn't require OpenTelemetry dependencies. This mode is ideal for:

or maybe something like

Suggested change
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:
If you only need error monitoring, logs and metrics but no spans, you can use the lightweight mode which doesn't require OpenTelemetry dependencies. This mode is ideal for:

wdyt?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, something like that. I'm curious about no spans since manual span creation is still intended.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right ... I hate naming things 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 9433879.

);
}

applySdkMetadata(options, 'node-light', ['node-core']);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant applySdkMetadata call in light SDK init

Low Severity

applySdkMetadata(options, 'node-light', ['node-core']) is called in both _init() and the LightNodeClient constructor with identical arguments. Since applySdkMetadata has an if (!sdk.name) guard, the constructor call is always a no-op when invoked through _init(). The call in _init() is the redundant one since the constructor already handles the fallback for direct construction.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible to instantiate a new client without going through init.

andreiborza and others added 16 commits February 6, 2026 18:39
Co-authored-by: Charly Gomez <[email protected]>
Add test verifying incoming sentry-trace and baggage headers are
correctly continued via continueTrace in light mode. Also add trace ID
isolation assertions to the concurrent error test.
Drop anrIntegration, disableAnrDetectionForCallback, and
inboundFiltersIntegration from the light entry point since this is a
new entry point with no existing users. Switch getDefaultIntegrations
to use eventFiltersIntegration directly.
Light mode supports logs, metrics, and distributed tracing — not just
error tracking. Rename section from "Errors-only" to "Lightweight Mode"
and update feature list accordingly.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

isolationScope.setSDKProcessingMetadata({
normalizedRequest,
...(client.getOptions().sendDefaultPii && { ipAddress }),
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ipAddress conditionally stored differs from full version behavior

Low Severity

The light httpServerIntegration conditionally stores ipAddress in sdkProcessingMetadata based on sendDefaultPii, while the full version always stores it unconditionally. The requestDataIntegration has its own independent include.ip option (defaulting to sendDefaultPii) that controls whether to add IP to events. When a user explicitly sets include: { ip: true } in requestDataIntegration while sendDefaultPii is false, the full version provides socket.remoteAddress as a fallback, but the light version loses this fallback since ipAddress was never stored. This violates separation of concerns — the HTTP integration pre-empts a decision that belongs to requestDataIntegration.

Additional Locations (1)

Fix in Cursor Fix in Web

Avoids drift between the two entry points by keeping shared exports
in a single file. Entry-point-specific and deprecated exports remain
in their respective index files.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.


// Check that some expected integrations are present
const integrationNames = integrations.map(i => i.name);
expect(integrationNames).toContain('InboundFilters');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test asserts wrong integration name for light SDK

Medium Severity

The test asserts that the default integrations contain 'InboundFilters', but the light SDK's getDefaultIntegrations uses eventFiltersIntegration() which has the name 'EventFilters'. The deprecated inboundFiltersIntegration uses the name 'InboundFilters', but the light SDK intentionally uses the non-deprecated version. This assertion will always fail.

Additional Locations (1)

Fix in Cursor Fix in Web

isolationScope.setSDKProcessingMetadata({
normalizedRequest,
...(client.getOptions().sendDefaultPii && { ipAddress }),
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Light mode double-gates ipAddress with sendDefaultPii check

Low Severity

The light httpServerIntegration conditionally stores ipAddress in sdkProcessingMetadata only when sendDefaultPii is truthy, while the OTel version always stores it unconditionally. The requestDataIntegration already has its own sendDefaultPii gating via include.ip ?? client.getOptions().sendDefaultPii. This double-gating means a user who explicitly configures requestDataIntegration({ include: { ip: true } }) without setting sendDefaultPii won't get socket-level IP addresses in the light mode, unlike the full mode.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(node-core): Add node-core/light

4 participants