Conversation
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.
|
1687319 to
773cb0a
Compare
5f3ebcb to
f5bdfc9
Compare
size-limit report 📦
|
748eb5f to
fdb1403
Compare
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", |
There was a problem hiding this comment.
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?
sentry-javascript/packages/node-core/README.md
Lines 23 to 30 in 356776c
fdb1403 to
da1124d
Compare
|
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. |
| serverName, | ||
| }; | ||
|
|
||
| applySdkMetadata(clientOptions, 'node'); |
There was a problem hiding this comment.
Should we mark this as node-light here?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Good idea, I'll add a test app.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Yep, went with a common exports file in efc79f8.
| maxRequestBodySize?: 'none' | 'small' | 'medium' | 'always'; | ||
| } | ||
|
|
||
| const _httpServerIntegration = ((options: HttpServerIntegrationOptions = {}) => { |
There was a problem hiding this comment.
l: Should we call this something else like HttpServerLightIntegration? Might be confusing to have this flying around a couple of times
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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)'); |
There was a problem hiding this comment.
Maybe call it "(node-light)" -> People could think about a theme when first reading "light mode" and might be confused.
There was a problem hiding this comment.
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 }); |
There was a problem hiding this comment.
M: Setting the IP address should be guarded with sendDefaultPii. I think this is not done here.
There was a problem hiding this comment.
Great catch! should be h: really 😅
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Q: Is this something that still needs to be fixed?
There was a problem hiding this comment.
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.
947923f to
a0cdf5f
Compare
| import { expect, test } from '@playwright/test'; | ||
| import { waitForError } from '@sentry-internal/test-utils'; | ||
|
|
||
| test('should isolate scope data across concurrent requests', async ({ request }) => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
| this._flushOutcomes(); | ||
| }; | ||
|
|
||
| this._clientReportInterval = setInterval(() => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
safeUnref is a @sentry/core internal, I didn't want to export it as public api for this though...
| // eslint-disable-next-line deprecation/deprecation | ||
| export { anrIntegration, disableAnrDetectionForCallback } from '../integrations/anr'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Ah, yea I like that idea. No reason to ship something new that'll be immediately taken away.
There was a problem hiding this comment.
worst-case, if anyone requests it we can still add it 😅
packages/node-core/src/light/sdk.ts
Outdated
| // eslint-disable-next-line deprecation/deprecation | ||
| inboundFiltersIntegration(), |
There was a problem hiding this comment.
l: we could make the switch here already (see other comment)
| const scope = getCurrentScope(); | ||
| scope.update(options.initialScope); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
packages/node-core/README.md
Outdated
| NODE_OPTIONS="--import ./instrument.mjs" npm run start | ||
| ``` | ||
|
|
||
| ## Errors-only Lightweight Mode |
There was a problem hiding this comment.
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" ?
There was a problem hiding this comment.
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!
packages/node-core/README.md
Outdated
|
|
||
| > **⚠️ 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: |
There was a problem hiding this comment.
l: I would at least scratch "performance", maybe even replace "performance tracing" with:
| 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
| 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?
There was a problem hiding this comment.
Right, something like that. I'm curious about no spans since manual span creation is still intended.
| ); | ||
| } | ||
|
|
||
| applySdkMetadata(options, 'node-light', ['node-core']); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
It's possible to instantiate a new client without going through init.
Co-authored-by: Charly Gomez <[email protected]>
@sentry/node-core)
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.
2dd6c29 to
b1ef70d
Compare
There was a problem hiding this comment.
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 }), | ||
| }); |
There was a problem hiding this comment.
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)
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.
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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)
| isolationScope.setSDKProcessingMetadata({ | ||
| normalizedRequest, | ||
| ...(client.getOptions().sendDefaultPii && { ipAddress }), | ||
| }); |
There was a problem hiding this comment.
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.


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.withIsolationScopeis necessary to isolate requests.Closes #19157 (added automatically)