Skip to content

Commit e417276

Browse files
JPeer264claude
andauthored
fix(bun): Keep diagnostics channel subscriptions alive (#24632)
Bun garbage-collects a diagnostics channel that no code references, and its subscribers with it. Node and Deno keep subscribed channels alive. The channel integrations kept no reference, so on Bun they could silently stop producing spans after the first GC. They now create their channels through wrappers in `@sentry/server-utils` that keep one. `@sentry/bun` uses these integrations, and also `httpIntegration` from `@sentry/node`, whose channel subscriptions now go through the same wrappers. `diagnosticsChannelGc.test.ts` reproduces the GC on Bun. Running the shared Node integration suites on Bun found this. Also an oxlint rule has been added to prevent any issues in the future regarding this Part of #23889. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 69a6783 commit e417276

49 files changed

Lines changed: 259 additions & 56 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎dev-packages/bun-integration-tests/node-suites/excludes.ts‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,6 @@ const NOT_TRIAGED = [
149149
'suites/tracing/tracer-start-active-span-error/test.ts',
150150
];
151151

152-
// Bun garbage-collects a diagnostics channel that no code references, and its subscribers with it.
153-
// `graphql` 17 publishes its own tracing channels, so the integration only subscribes to them, and
154-
// no spans arrive. See https://github.com/oven-sh/bun/issues/43086
155-
const CHANNEL_GARBAGE_COLLECTED = ['suites/tracing/graphql-tracing-channel/**'];
156-
157152
// The scenario configures `nativeNodeFetchIntegration`, which `@sentry/bun` does not export.
158153
const NO_NATIVE_NODE_FETCH_INTEGRATION = [
159154
'suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts',
@@ -209,7 +204,6 @@ export const NODE_SUITES_EXCLUDE = [
209204
...NO_OUTGOING_HTTP_INSTRUMENTATION,
210205
...NO_AUTO_INSTRUMENTATION,
211206
...NOT_TRIAGED,
212-
...CHANNEL_GARBAGE_COLLECTED,
213207
];
214208

215209
// Excluded only in the `node-suites` project, which runs the suites with `@sentry/node`.

‎packages/bun/.oxlintrc.json‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,24 @@
1414
{
1515
"files": ["**/src/**"],
1616
"rules": {
17-
"sdk/no-class-field-initializers": "off"
17+
"sdk/no-class-field-initializers": "off",
18+
"no-restricted-imports": [
19+
"error",
20+
{
21+
"paths": [
22+
{
23+
"name": "node:diagnostics_channel",
24+
"allowTypeImports": true,
25+
"message": "Bun garbage-collects a channel that no code references, together with its subscribers. Use `subscribeDiagnosticsChannel` or `diagnosticsTracingChannel` from `@sentry/server-utils` instead. See https://github.com/oven-sh/bun/issues/43086"
26+
},
27+
{
28+
"name": "diagnostics_channel",
29+
"allowTypeImports": true,
30+
"message": "Bun garbage-collects a channel that no code references, together with its subscribers. Use `subscribeDiagnosticsChannel` or `diagnosticsTracingChannel` from `@sentry/server-utils` instead. See https://github.com/oven-sh/bun/issues/43086"
31+
}
32+
]
33+
}
34+
]
1835
}
1936
}
2037
]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { channel } from 'node:diagnostics_channel';
2+
import { describe, expect, test } from 'bun:test';
3+
import { init } from '../../src';
4+
5+
const EXPRESS_HANDLE_START = 'tracing:orchestrion:express:handle:start';
6+
7+
function nextTask(): Promise<void> {
8+
return new Promise(resolve => setTimeout(resolve, 0));
9+
}
10+
11+
describe('channel-based integrations', () => {
12+
// Bun garbage-collects a diagnostics channel that no code references, together with its
13+
// subscribers. A GC in the same task does not collect it, so the check runs in a later task.
14+
// See https://github.com/oven-sh/bun/issues/43086
15+
test('stay subscribed after a garbage collection', async () => {
16+
init({ dsn: 'https://username@domain/123', tracesSampleRate: 1 });
17+
await nextTask();
18+
19+
expect(channel(EXPRESS_HANDLE_START).hasSubscribers).toBe(true);
20+
21+
Bun.gc(true);
22+
await nextTask();
23+
24+
expect(channel(EXPRESS_HANDLE_START).hasSubscribers).toBe(true);
25+
});
26+
});

‎packages/node/.oxlintrc.json‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,24 @@
1717
{
1818
"files": ["**/src/**"],
1919
"rules": {
20-
"sdk/no-class-field-initializers": "off"
20+
"sdk/no-class-field-initializers": "off",
21+
"no-restricted-imports": [
22+
"error",
23+
{
24+
"paths": [
25+
{
26+
"name": "node:diagnostics_channel",
27+
"allowTypeImports": true,
28+
"message": "Bun garbage-collects a channel that no code references, together with its subscribers. Use `subscribeDiagnosticsChannel` or `diagnosticsTracingChannel` from `@sentry/server-utils` instead. See https://github.com/oven-sh/bun/issues/43086"
29+
},
30+
{
31+
"name": "diagnostics_channel",
32+
"allowTypeImports": true,
33+
"message": "Bun garbage-collects a channel that no code references, together with its subscribers. Use `subscribeDiagnosticsChannel` or `diagnosticsTracingChannel` from `@sentry/server-utils` instead. See https://github.com/oven-sh/bun/issues/43086"
34+
}
35+
]
36+
}
37+
]
2138
}
2239
},
2340
{

‎packages/node/src/integrations/childProcess.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ChildProcess } from 'node:child_process';
2-
import * as diagnosticsChannel from 'node:diagnostics_channel';
32
import { addBreadcrumb, defineIntegration, isObjectLike } from '@sentry/core';
3+
import { subscribeDiagnosticsChannel } from '@sentry/server-utils';
44

55
interface Options {
66
/**
@@ -21,7 +21,7 @@ export const childProcessIntegration = defineIntegration((options: Options = {})
2121
return {
2222
name: INTEGRATION_NAME,
2323
setup() {
24-
diagnosticsChannel.channel('child_process').subscribe((event: unknown) => {
24+
subscribeDiagnosticsChannel('child_process', (event: unknown) => {
2525
if (isObjectLike(event) && 'process' in event) {
2626
captureChildProcessEvents(event.process as ChildProcess, options);
2727
}

‎packages/node/src/integrations/http/SentryHttpInstrumentation.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { subscribe } from 'node:diagnostics_channel';
21
import { context, trace } from '@opentelemetry/api';
32
import type { ClientRequest, IncomingMessage } from 'node:http';
43
import type { Span } from '@sentry/core';
@@ -10,6 +9,7 @@ import {
109
HTTP_ON_CLIENT_REQUEST,
1110
patchHttpModuleClient,
1211
} from '@sentry/core/server';
12+
import { subscribeDiagnosticsChannel } from '@sentry/server-utils';
1313
import { NODE_VERSION } from '../../nodeVersion';
1414
import { errorMonitor } from 'node:events';
1515
import * as http from 'node:http';
@@ -125,7 +125,7 @@ export function instrumentHttpOutgoingRequests(
125125

126126
function instrumentHttpOutgoingRequestsViaChannel(options: HttpInstrumentationOptions): void {
127127
const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequestCreated } = getHttpClientSubscriptions(options);
128-
subscribe(HTTP_ON_CLIENT_REQUEST, onHttpClientRequestCreated);
128+
subscribeDiagnosticsChannel(HTTP_ON_CLIENT_REQUEST, onHttpClientRequestCreated);
129129
}
130130

131131
/**

‎packages/node/src/integrations/http/httpServerIntegration.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { subscribe } from 'node:diagnostics_channel';
21
import type { RequestOptions } from 'node:http';
32
import { context, createContextKey, propagation } from '@opentelemetry/api';
43
import type { Integration, IntegrationFn } from '@sentry/core';
54
import { addNonEnumerableProperty, debug, getClient } from '@sentry/core';
65
import type { HttpIncomingMessage, HttpServerResponse } from '@sentry/core/server';
76
import { getHttpServerSubscriptions, HTTP_ON_SERVER_REQUEST, recordRequestSession } from '@sentry/core/server';
7+
import { subscribeDiagnosticsChannel } from '@sentry/server-utils';
88
import type { RequestEventData } from '@sentry/core';
99
import { DEBUG_BUILD } from '../../debug-build';
1010

@@ -143,7 +143,7 @@ const _httpServerIntegration = ((options: HttpServerIntegrationOptions = {}) =>
143143
name: INTEGRATION_NAME,
144144
setupOnce() {
145145
const { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequestStart } = getHttpServerSubscriptions(_options);
146-
subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequestStart);
146+
subscribeDiagnosticsChannel(HTTP_ON_SERVER_REQUEST, onHttpServerRequestStart);
147147
},
148148
afterAllSetup(client) {
149149
if (DEBUG_BUILD && client.getIntegrationByName('Http')) {

‎packages/node/src/integrations/node-fetch/undici-instrumentation.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
/* eslint-disable max-lines */
1818

19-
import * as diagch from 'diagnostics_channel';
2019
import { URL } from 'url';
2120

2221
import type { Span, SpanAttributes } from '@sentry/core';
@@ -42,6 +41,7 @@ import {
4241
filterCollectedUrlQuery,
4342
httpHeadersToSpanAttributes,
4443
} from '@sentry/core';
44+
import { subscribeDiagnosticsChannel } from '@sentry/server-utils';
4545
import { addFetchRequestBreadcrumb, addTracePropagationHeadersToFetchRequest } from '../../utils/outgoingFetchRequest';
4646
import {
4747
HTTP_REQUEST_METHOD,
@@ -118,7 +118,7 @@ function subscribeToChannel(
118118
diagnosticChannel: string,
119119
onMessage: (message: unknown, name: string | symbol) => void,
120120
): void {
121-
diagch.subscribe?.(diagnosticChannel, onMessage);
121+
subscribeDiagnosticsChannel(diagnosticChannel, onMessage);
122122
}
123123

124124
function parseRequestHeaders(request: UndiciRequest): Map<string, string | string[]> {

‎packages/node/src/integrations/pino.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as diagnosticsChannel from 'node:diagnostics_channel';
21
import type { IntegrationFn, LogSeverityLevel } from '@sentry/core';
32
import {
43
_INTERNAL_captureLog,
@@ -10,6 +9,7 @@ import {
109
severityLevelFromString,
1110
withScope,
1211
} from '@sentry/core';
12+
import { diagnosticsTracingChannel } from '@sentry/server-utils';
1313

1414
const SENTRY_TRACK_SYMBOL = Symbol('sentry-track-pino-logger');
1515

@@ -125,7 +125,7 @@ const _pinoIntegration = defineIntegration((userOptions: DeepPartial<PinoOptions
125125
return {
126126
name: 'Pino',
127127
setup: () => {
128-
const integratedChannel = diagnosticsChannel.tracingChannel('pino_asJson');
128+
const integratedChannel = diagnosticsTracingChannel('pino_asJson');
129129

130130
function onPinoStart(self: Pino, args: PinoHookArgs, result: PinoResult): void {
131131
if (!shouldTrackLogger(self)) {

‎packages/node/src/integrations/workerThreads.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Worker } from 'node:worker_threads';
2-
import * as diagnosticsChannel from 'node:diagnostics_channel';
32
import { captureException, defineIntegration, isObjectLike } from '@sentry/core';
3+
import { subscribeDiagnosticsChannel } from '@sentry/server-utils';
44

55
const INTEGRATION_NAME = 'WorkerThreads' as const;
66

@@ -12,7 +12,7 @@ export const workerThreadsIntegration = defineIntegration(() => {
1212
return {
1313
name: INTEGRATION_NAME,
1414
setup() {
15-
diagnosticsChannel.channel('worker_threads').subscribe((event: unknown) => {
15+
subscribeDiagnosticsChannel('worker_threads', (event: unknown) => {
1616
if (isObjectLike(event) && 'worker' in event) {
1717
captureWorkerThreadEvents(event.worker as Worker);
1818
}

0 commit comments

Comments
 (0)