Skip to content

Commit 984e443

Browse files
JPeer264claude
andcommitted
fix(bun): Keep diagnostics channel subscriptions alive
Bun garbage-collects a diagnostics channel that no code references, together with its subscribers, so a subscription made at init() could stop receiving messages after the next GC. Node and Deno keep a subscribed channel alive. On Bun, the channel wrappers in @sentry/server-utils now keep a reference to every channel they return, and all channel subscriptions in server-utils and @sentry/node use them, including Prisma and pino. Other runtimes get the original node:diagnostics_channel functions. A no-restricted-imports lint rule in server-utils, node and bun rejects value imports of node:diagnostics_channel outside the wrappers, so new integrations use them too. graphql-tracing-channel, which the GC broke, now runs on Bun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 378d934 commit 984e443

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)