Skip to content

Commit b2d38bb

Browse files
authored
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 the channel integrations and httpIntegration use them. Other runtimes get the original node:diagnostics_channel functions. graphql-tracing-channel, which the GC broke, now runs on Bun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a18ed9a commit b2d38bb

40 files changed

Lines changed: 191 additions & 43 deletions

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

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

148-
// Bun garbage-collects a diagnostics channel that no code references, and its subscribers with it.
149-
// `graphql` 17 publishes its own tracing channels, so the integration only subscribes to them, and
150-
// no spans arrive. See https://github.com/oven-sh/bun/issues/43086
151-
const CHANNEL_GARBAGE_COLLECTED = ['suites/tracing/graphql-tracing-channel/**'];
152-
153148
export const NODE_SUITES_EXCLUDE = [
154149
'**/node_modules/**',
155150
...NODE_ONLY,
@@ -158,5 +153,4 @@ export const NODE_SUITES_EXCLUDE = [
158153
...NO_OUTGOING_HTTP_INSTRUMENTATION,
159154
...NO_AUTO_INSTRUMENTATION,
160155
...NOT_TRIAGED,
161-
...CHANNEL_GARBAGE_COLLECTED,
162156
];
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/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/server-utils/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type { InstrumentationConfig } from './orchestrion/apmTypes';
1313
// `orchestrion/bundler/moduleInjectedTransform.ts`); it is a plain runtime
1414
// helper with no orchestrion build-time dependency.
1515
export { orchestrionModuleInjected } from './utils/moduleInjected';
16+
export { subscribe as subscribeDiagnosticsChannel } from './utils/diagnosticsChannel';
1617
export { eveConversationHook, eveIntegration } from './eve';
1718
export { getInstrumentedModuleNames } from './orchestrion/config';
1819
export {

‎packages/server-utils/src/integrations/amqplib.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable max-lines */
2-
import * as diagnosticsChannel from 'node:diagnostics_channel';
2+
import * as diagnosticsChannel from '../utils/diagnosticsChannel';
33
import type { IntegrationFn, Span, SpanAttributes } from '@sentry/core';
44
import {
55
continueTrace,

‎packages/server-utils/src/integrations/anthropic.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GEN_AI_REQUEST_MODEL } from '@sentry/conventions/attributes';
2-
import * as diagnosticsChannel from 'node:diagnostics_channel';
2+
import * as diagnosticsChannel from '../utils/diagnosticsChannel';
33
import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core';
44
import {
55
_INTERNAL_shouldSkipAiProviderWrapping,

‎packages/server-utils/src/integrations/aws-sdk/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as diagnosticsChannel from 'node:diagnostics_channel';
1+
import * as diagnosticsChannel from '../../utils/diagnosticsChannel';
22
import type { IntegrationFn, Span } from '@sentry/core';
33
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core';
44
import {

‎packages/server-utils/src/integrations/dataloader.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as diagnosticsChannel from 'node:diagnostics_channel';
1+
import * as diagnosticsChannel from '../utils/diagnosticsChannel';
22
import {
33
CACHE_KEY,
44
CACHE_OPERATION,

‎packages/server-utils/src/integrations/express/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as diagnosticsChannel from 'node:diagnostics_channel';
1+
import * as diagnosticsChannel from '../../utils/diagnosticsChannel';
22
import type { IntegrationFn } from '@sentry/core';
33
import { defineIntegration } from '@sentry/core';
44
import { expressModuleNames } from '../../orchestrion/config/express';

0 commit comments

Comments
 (0)