From 2882826c17109d7a93c0b8457173753c54cf2510 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Fri, 25 Sep 2026 17:48:25 +0200 Subject: [PATCH] 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 --- .../node-suites/excludes.ts | 6 -- .../integrations/diagnosticsChannelGc.test.ts | 26 +++++++ .../http/SentryHttpInstrumentation.ts | 4 +- .../http/httpServerIntegration.ts | 4 +- packages/server-utils/src/index.ts | 1 + .../server-utils/src/integrations/amqplib.ts | 2 +- .../src/integrations/anthropic.ts | 2 +- .../src/integrations/aws-sdk/index.ts | 2 +- .../src/integrations/dataloader.ts | 2 +- .../src/integrations/express/index.ts | 2 +- .../src/integrations/fastify/errors.ts | 2 +- .../integrations/fastify/instrumentation.ts | 2 +- .../integrations/firebase/instrumentation.ts | 2 +- .../src/integrations/generic-pool.ts | 2 +- .../src/integrations/google-genai.ts | 2 +- .../src/integrations/graphql/index.ts | 2 +- .../src/integrations/hapi/index.ts | 2 +- .../src/integrations/kafkajs/index.ts | 2 +- .../server-utils/src/integrations/knex.ts | 2 +- .../src/integrations/koa/index.ts | 2 +- .../src/integrations/langchain.ts | 2 +- .../src/integrations/langgraph.ts | 2 +- .../src/integrations/lru-memoizer.ts | 2 +- .../server-utils/src/integrations/mastra.ts | 2 +- .../server-utils/src/integrations/mistral.ts | 2 +- .../src/integrations/mongodb/index.ts | 2 +- .../src/integrations/mongoose/index.ts | 2 +- .../server-utils/src/integrations/mysql.ts | 2 +- .../src/integrations/mysql2/index.ts | 2 +- .../src/integrations/openai-compatible.ts | 2 +- .../server-utils/src/integrations/openai.ts | 2 +- .../src/integrations/postgres-js.ts | 2 +- .../server-utils/src/integrations/postgres.ts | 2 +- .../src/integrations/redis/index.ts | 2 +- .../redis/ioredis-channel-subscriber.ts | 2 +- .../server-utils/src/integrations/tedious.ts | 2 +- .../src/integrations/vercel-ai/index.ts | 2 +- .../src/orchestrion/instrumentation.ts | 2 +- .../src/utils/diagnosticsChannel.ts | 51 +++++++++++++ .../test/utils/diagnosticsChannel.test.ts | 76 +++++++++++++++++++ 40 files changed, 191 insertions(+), 43 deletions(-) create mode 100644 packages/bun/test/integrations/diagnosticsChannelGc.test.ts create mode 100644 packages/server-utils/src/utils/diagnosticsChannel.ts create mode 100644 packages/server-utils/test/utils/diagnosticsChannel.test.ts diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index fbd7668883ae..556f39ee7fea 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -145,11 +145,6 @@ const NOT_TRIAGED = [ 'suites/tracing/tracer-start-active-span-error/test.ts', ]; -// Bun garbage-collects a diagnostics channel that no code references, and its subscribers with it. -// `graphql` 17 publishes its own tracing channels, so the integration only subscribes to them, and -// no spans arrive. See https://github.com/oven-sh/bun/issues/43086 -const CHANNEL_GARBAGE_COLLECTED = ['suites/tracing/graphql-tracing-channel/**']; - export const NODE_SUITES_EXCLUDE = [ '**/node_modules/**', ...NODE_ONLY, @@ -158,5 +153,4 @@ export const NODE_SUITES_EXCLUDE = [ ...NO_OUTGOING_HTTP_INSTRUMENTATION, ...NO_AUTO_INSTRUMENTATION, ...NOT_TRIAGED, - ...CHANNEL_GARBAGE_COLLECTED, ]; diff --git a/packages/bun/test/integrations/diagnosticsChannelGc.test.ts b/packages/bun/test/integrations/diagnosticsChannelGc.test.ts new file mode 100644 index 000000000000..3034f4005493 --- /dev/null +++ b/packages/bun/test/integrations/diagnosticsChannelGc.test.ts @@ -0,0 +1,26 @@ +import { channel } from 'node:diagnostics_channel'; +import { describe, expect, test } from 'bun:test'; +import { init } from '../../src'; + +const EXPRESS_HANDLE_START = 'tracing:orchestrion:express:handle:start'; + +function nextTask(): Promise { + return new Promise(resolve => setTimeout(resolve, 0)); +} + +describe('channel-based integrations', () => { + // Bun garbage-collects a diagnostics channel that no code references, together with its + // subscribers. A GC in the same task does not collect it, so the check runs in a later task. + // See https://github.com/oven-sh/bun/issues/43086 + test('stay subscribed after a garbage collection', async () => { + init({ dsn: 'https://username@domain/123', tracesSampleRate: 1 }); + await nextTask(); + + expect(channel(EXPRESS_HANDLE_START).hasSubscribers).toBe(true); + + Bun.gc(true); + await nextTask(); + + expect(channel(EXPRESS_HANDLE_START).hasSubscribers).toBe(true); + }); +}); diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index a0d38f50043c..6f940d60093e 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -1,4 +1,3 @@ -import { subscribe } from 'node:diagnostics_channel'; import { context, trace } from '@opentelemetry/api'; import type { ClientRequest, IncomingMessage } from 'node:http'; import type { Span } from '@sentry/core'; @@ -10,6 +9,7 @@ import { HTTP_ON_CLIENT_REQUEST, patchHttpModuleClient, } from '@sentry/core/server'; +import { subscribeDiagnosticsChannel } from '@sentry/server-utils'; import { NODE_VERSION } from '../../nodeVersion'; import { errorMonitor } from 'node:events'; import * as http from 'node:http'; @@ -125,7 +125,7 @@ export function instrumentHttpOutgoingRequests( function instrumentHttpOutgoingRequestsViaChannel(options: HttpInstrumentationOptions): void { const { [HTTP_ON_CLIENT_REQUEST]: onHttpClientRequestCreated } = getHttpClientSubscriptions(options); - subscribe(HTTP_ON_CLIENT_REQUEST, onHttpClientRequestCreated); + subscribeDiagnosticsChannel(HTTP_ON_CLIENT_REQUEST, onHttpClientRequestCreated); } /** diff --git a/packages/node/src/integrations/http/httpServerIntegration.ts b/packages/node/src/integrations/http/httpServerIntegration.ts index 68de63aeecdb..6322cd0169bd 100644 --- a/packages/node/src/integrations/http/httpServerIntegration.ts +++ b/packages/node/src/integrations/http/httpServerIntegration.ts @@ -1,10 +1,10 @@ -import { subscribe } from 'node:diagnostics_channel'; import type { RequestOptions } from 'node:http'; import { context, createContextKey, propagation } from '@opentelemetry/api'; import type { Integration, IntegrationFn } from '@sentry/core'; import { addNonEnumerableProperty, debug, getClient } from '@sentry/core'; import type { HttpIncomingMessage, HttpServerResponse } from '@sentry/core/server'; import { getHttpServerSubscriptions, HTTP_ON_SERVER_REQUEST, recordRequestSession } from '@sentry/core/server'; +import { subscribeDiagnosticsChannel } from '@sentry/server-utils'; import type { RequestEventData } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; @@ -143,7 +143,7 @@ const _httpServerIntegration = ((options: HttpServerIntegrationOptions = {}) => name: INTEGRATION_NAME, setupOnce() { const { [HTTP_ON_SERVER_REQUEST]: onHttpServerRequestStart } = getHttpServerSubscriptions(_options); - subscribe(HTTP_ON_SERVER_REQUEST, onHttpServerRequestStart); + subscribeDiagnosticsChannel(HTTP_ON_SERVER_REQUEST, onHttpServerRequestStart); }, afterAllSetup(client) { if (DEBUG_BUILD && client.getIntegrationByName('Http')) { diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 54e82b0dae6f..4e6ef2196c2e 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -13,6 +13,7 @@ export type { InstrumentationConfig } from './orchestrion/apmTypes'; // `orchestrion/bundler/moduleInjectedTransform.ts`); it is a plain runtime // helper with no orchestrion build-time dependency. export { orchestrionModuleInjected } from './utils/moduleInjected'; +export { subscribe as subscribeDiagnosticsChannel } from './utils/diagnosticsChannel'; export { eveConversationHook, eveIntegration } from './eve'; export { getInstrumentedModuleNames } from './orchestrion/config'; export { diff --git a/packages/server-utils/src/integrations/amqplib.ts b/packages/server-utils/src/integrations/amqplib.ts index 864022d8d291..b67b546d8b2f 100644 --- a/packages/server-utils/src/integrations/amqplib.ts +++ b/packages/server-utils/src/integrations/amqplib.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributes } from '@sentry/core'; import { continueTrace, diff --git a/packages/server-utils/src/integrations/anthropic.ts b/packages/server-utils/src/integrations/anthropic.ts index 87bce3b9c199..c8638acc0a28 100644 --- a/packages/server-utils/src/integrations/anthropic.ts +++ b/packages/server-utils/src/integrations/anthropic.ts @@ -1,5 +1,5 @@ import { GEN_AI_REQUEST_MODEL } from '@sentry/conventions/attributes'; -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; import { _INTERNAL_shouldSkipAiProviderWrapping, diff --git a/packages/server-utils/src/integrations/aws-sdk/index.ts b/packages/server-utils/src/integrations/aws-sdk/index.ts index 1b0656551a10..eb966dcd56c6 100644 --- a/packages/server-utils/src/integrations/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/aws-sdk/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn, Span } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; import { diff --git a/packages/server-utils/src/integrations/dataloader.ts b/packages/server-utils/src/integrations/dataloader.ts index be42e44d3006..ac928f3ceeca 100644 --- a/packages/server-utils/src/integrations/dataloader.ts +++ b/packages/server-utils/src/integrations/dataloader.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import { CACHE_KEY, CACHE_OPERATION, diff --git a/packages/server-utils/src/integrations/express/index.ts b/packages/server-utils/src/integrations/express/index.ts index 52598014408d..c53ff8fcf6bb 100644 --- a/packages/server-utils/src/integrations/express/index.ts +++ b/packages/server-utils/src/integrations/express/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { expressModuleNames } from '../../orchestrion/config/express'; diff --git a/packages/server-utils/src/integrations/fastify/errors.ts b/packages/server-utils/src/integrations/fastify/errors.ts index 1dcc5d5bc9ad..3ef6ad596863 100644 --- a/packages/server-utils/src/integrations/fastify/errors.ts +++ b/packages/server-utils/src/integrations/fastify/errors.ts @@ -1,5 +1,5 @@ import type { FastifyIntegration, FastifyReply, FastifyRequest } from './types'; -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import { addNonEnumerableProperty, captureException, getClient } from '@sentry/core'; import { defaultShouldHandleError, INTEGRATION_NAME } from './utils'; diff --git a/packages/server-utils/src/integrations/fastify/instrumentation.ts b/packages/server-utils/src/integrations/fastify/instrumentation.ts index c64f16d1d67a..92f71c908fe4 100644 --- a/packages/server-utils/src/integrations/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/fastify/instrumentation.ts @@ -14,7 +14,7 @@ /* eslint-disable @typescript-eslint/no-this-alias */ /* eslint-disable max-lines */ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import { HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, diff --git a/packages/server-utils/src/integrations/firebase/instrumentation.ts b/packages/server-utils/src/integrations/firebase/instrumentation.ts index 0951e85194a6..5fcadfd2ae45 100644 --- a/packages/server-utils/src/integrations/firebase/instrumentation.ts +++ b/packages/server-utils/src/integrations/firebase/instrumentation.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan, safeChannelCallback } from '../../tracing-channel'; import type { FirestoreReference } from './firestore-types'; diff --git a/packages/server-utils/src/integrations/generic-pool.ts b/packages/server-utils/src/integrations/generic-pool.ts index ad6452e85f65..40d0f4c530a1 100644 --- a/packages/server-utils/src/integrations/generic-pool.ts +++ b/packages/server-utils/src/integrations/generic-pool.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import { SENTRY_OP } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn } from '@sentry/core'; diff --git a/packages/server-utils/src/integrations/google-genai.ts b/packages/server-utils/src/integrations/google-genai.ts index 9c37a5351f7e..6ddaf8757d6a 100644 --- a/packages/server-utils/src/integrations/google-genai.ts +++ b/packages/server-utils/src/integrations/google-genai.ts @@ -1,5 +1,5 @@ import { GEN_AI_REQUEST_MODEL, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span } from '@sentry/core'; import { _INTERNAL_shouldSkipAiProviderWrapping, diff --git a/packages/server-utils/src/integrations/graphql/index.ts b/packages/server-utils/src/integrations/graphql/index.ts index bd204e21eda7..e0397b1ac769 100644 --- a/packages/server-utils/src/integrations/graphql/index.ts +++ b/packages/server-utils/src/integrations/graphql/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, waitForTracingChannelBinding } from '@sentry/core'; import { subscribeGraphqlDiagnosticChannels, type GraphQLOptions } from './graphql-dc-subscriber'; diff --git a/packages/server-utils/src/integrations/hapi/index.ts b/packages/server-utils/src/integrations/hapi/index.ts index 70c462ca0324..e7b8af943694 100644 --- a/packages/server-utils/src/integrations/hapi/index.ts +++ b/packages/server-utils/src/integrations/hapi/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { CHANNELS } from '../../orchestrion/channels'; diff --git a/packages/server-utils/src/integrations/kafkajs/index.ts b/packages/server-utils/src/integrations/kafkajs/index.ts index 21d08cb536f0..5160dedf80ca 100644 --- a/packages/server-utils/src/integrations/kafkajs/index.ts +++ b/packages/server-utils/src/integrations/kafkajs/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { TracingChannelSubscribers } from 'node:diagnostics_channel'; import type { IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index e326a2e7a7ec..5d85d1491682 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -2,7 +2,7 @@ // emit them deliberately to preserve parity with what `@opentelemetry/instrumentation-knex` produced. /* oxlint-disable typescript/no-deprecated */ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributes } from '@sentry/core'; import { debug, diff --git a/packages/server-utils/src/integrations/koa/index.ts b/packages/server-utils/src/integrations/koa/index.ts index caa21288c656..3ee982041147 100644 --- a/packages/server-utils/src/integrations/koa/index.ts +++ b/packages/server-utils/src/integrations/koa/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { addNonEnumerableProperty, diff --git a/packages/server-utils/src/integrations/langchain.ts b/packages/server-utils/src/integrations/langchain.ts index 0e8016853445..d8513a80b926 100644 --- a/packages/server-utils/src/integrations/langchain.ts +++ b/packages/server-utils/src/integrations/langchain.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span } from '@sentry/core'; import { _INTERNAL_skipAiProviderWrapping, defineIntegration, startInactiveSpan } from '@sentry/core'; import { ANTHROPIC_AI_INTEGRATION_NAME } from '../ai/anthropic-ai/constants'; diff --git a/packages/server-utils/src/integrations/langgraph.ts b/packages/server-utils/src/integrations/langgraph.ts index 124260fe4029..a6b9f6f21541 100644 --- a/packages/server-utils/src/integrations/langgraph.ts +++ b/packages/server-utils/src/integrations/langgraph.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { debug, defineIntegration } from '@sentry/core'; import { resolveAIRecordingOptions } from '../ai/core/utils'; diff --git a/packages/server-utils/src/integrations/lru-memoizer.ts b/packages/server-utils/src/integrations/lru-memoizer.ts index 070d431bea99..6bb2b8acbd53 100644 --- a/packages/server-utils/src/integrations/lru-memoizer.ts +++ b/packages/server-utils/src/integrations/lru-memoizer.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { CHANNELS } from '../orchestrion/channels'; diff --git a/packages/server-utils/src/integrations/mastra.ts b/packages/server-utils/src/integrations/mastra.ts index 7a6b4c1f2a50..cfd1c5c70995 100644 --- a/packages/server-utils/src/integrations/mastra.ts +++ b/packages/server-utils/src/integrations/mastra.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import { createRequire } from 'node:module'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; diff --git a/packages/server-utils/src/integrations/mistral.ts b/packages/server-utils/src/integrations/mistral.ts index 7d53c2047cf8..117f0f965c0b 100644 --- a/packages/server-utils/src/integrations/mistral.ts +++ b/packages/server-utils/src/integrations/mistral.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; import { _INTERNAL_shouldSkipAiProviderWrapping, diff --git a/packages/server-utils/src/integrations/mongodb/index.ts b/packages/server-utils/src/integrations/mongodb/index.ts index 777764554f00..6f492a575a6b 100644 --- a/packages/server-utils/src/integrations/mongodb/index.ts +++ b/packages/server-utils/src/integrations/mongodb/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import type { MongodbNamespace, MongoV3Topology } from './mongodb-span'; diff --git a/packages/server-utils/src/integrations/mongoose/index.ts b/packages/server-utils/src/integrations/mongoose/index.ts index d08ffb1e0a14..4e00635939c1 100644 --- a/packages/server-utils/src/integrations/mongoose/index.ts +++ b/packages/server-utils/src/integrations/mongoose/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn, Span } from '@sentry/core'; import { defineIntegration, getActiveSpan, waitForTracingChannelBinding } from '@sentry/core'; import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber'; diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index f8a9b73464c8..ccbbad5a1565 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import { DB_NAMESPACE, DB_QUERY_SUMMARY, diff --git a/packages/server-utils/src/integrations/mysql2/index.ts b/packages/server-utils/src/integrations/mysql2/index.ts index 751a3ae9c2c3..4d01b681f1ce 100644 --- a/packages/server-utils/src/integrations/mysql2/index.ts +++ b/packages/server-utils/src/integrations/mysql2/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn, SpanAttributes } from '@sentry/core'; import { defineIntegration, diff --git a/packages/server-utils/src/integrations/openai-compatible.ts b/packages/server-utils/src/integrations/openai-compatible.ts index ae5c76a86135..b62764bb9473 100644 --- a/packages/server-utils/src/integrations/openai-compatible.ts +++ b/packages/server-utils/src/integrations/openai-compatible.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { Integration, IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; import { _INTERNAL_shouldSkipAiProviderWrapping, diff --git a/packages/server-utils/src/integrations/openai.ts b/packages/server-utils/src/integrations/openai.ts index e284d3b1bc4a..138cad048f93 100644 --- a/packages/server-utils/src/integrations/openai.ts +++ b/packages/server-utils/src/integrations/openai.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; import { _INTERNAL_shouldSkipAiProviderWrapping, diff --git a/packages/server-utils/src/integrations/postgres-js.ts b/packages/server-utils/src/integrations/postgres-js.ts index 24cf8e6c656a..afb8d5ecc099 100644 --- a/packages/server-utils/src/integrations/postgres-js.ts +++ b/packages/server-utils/src/integrations/postgres-js.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import { DB_OPERATION_NAME, DB_QUERY_SUMMARY, diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 350e7d5d41a2..1e7f0fdeb018 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import { DB_NAMESPACE, DB_QUERY_SUMMARY, diff --git a/packages/server-utils/src/integrations/redis/index.ts b/packages/server-utils/src/integrations/redis/index.ts index 05e4135ab63c..fb19018aa598 100644 --- a/packages/server-utils/src/integrations/redis/index.ts +++ b/packages/server-utils/src/integrations/redis/index.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import { DB_OPERATION_BATCH_SIZE, DB_OPERATION_NAME, diff --git a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts index ebadce735930..f50060a29a8e 100644 --- a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import { DB_OPERATION_NAME, DB_QUERY_TEXT, diff --git a/packages/server-utils/src/integrations/tedious.ts b/packages/server-utils/src/integrations/tedious.ts index 0c1cf4ca4927..a2b3ea352791 100644 --- a/packages/server-utils/src/integrations/tedious.ts +++ b/packages/server-utils/src/integrations/tedious.ts @@ -3,7 +3,7 @@ /* oxlint-disable typescript/no-deprecated */ import { EventEmitter } from 'node:events'; -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, SpanAttributes } from '@sentry/core'; import { defineIntegration, diff --git a/packages/server-utils/src/integrations/vercel-ai/index.ts b/packages/server-utils/src/integrations/vercel-ai/index.ts index f0387b9ef56b..6cf24b1e70b3 100644 --- a/packages/server-utils/src/integrations/vercel-ai/index.ts +++ b/packages/server-utils/src/integrations/vercel-ai/index.ts @@ -1,7 +1,7 @@ import { defineIntegration, waitForTracingChannelBinding, type IntegrationFn } from '@sentry/core'; import type { GenAiOptions } from '../../ai/core/utils'; import { subscribeVercelAiTracingChannel } from './vercel-ai-dc-subscriber'; -import * as dc from 'node:diagnostics_channel'; +import * as dc from '../../utils/diagnosticsChannel'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; import { vercelAiModuleNames } from '../../orchestrion/config/vercel-ai'; import { subscribeVercelAiOrchestrionChannels } from './vercel-ai-orchestrion-subscriber'; diff --git a/packages/server-utils/src/orchestrion/instrumentation.ts b/packages/server-utils/src/orchestrion/instrumentation.ts index 304d65d402a6..ded8aa30ac68 100644 --- a/packages/server-utils/src/orchestrion/instrumentation.ts +++ b/packages/server-utils/src/orchestrion/instrumentation.ts @@ -1,4 +1,4 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { Client } from '@sentry/core'; import { addNonEnumerableProperty, debug, waitForTracingChannelBinding } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; diff --git a/packages/server-utils/src/utils/diagnosticsChannel.ts b/packages/server-utils/src/utils/diagnosticsChannel.ts new file mode 100644 index 000000000000..3e7df8f85e20 --- /dev/null +++ b/packages/server-utils/src/utils/diagnosticsChannel.ts @@ -0,0 +1,51 @@ +import * as nodeDiagnosticsChannel from 'node:diagnostics_channel'; + +// Bun garbage-collects a diagnostics channel that no code references, together with its +// subscribers, so a subscription made at `init()` can stop receiving messages after the next GC. +// Node and Deno keep a channel alive while it has subscribers. On Bun these wrappers of the +// `node:diagnostics_channel` functions keep a reference to every channel they return, so the SDK's +// subscriptions stay active. Other runtimes get the original functions. +// See https://github.com/oven-sh/bun/issues/43086 +const keepChannelsReferenced = typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'; + +const channelsByName = new Map(); +const tracingChannelsByName = new Map(); + +/** `diagnostics_channel.channel()`, with the returned channel kept referenced on Bun. */ +export const channel: typeof nodeDiagnosticsChannel.channel = keepChannelsReferenced + ? name => { + let result = channelsByName.get(name); + if (!result) { + result = nodeDiagnosticsChannel.channel(name); + channelsByName.set(name, result); + } + return result; + } + : nodeDiagnosticsChannel.channel; + +/** `diagnostics_channel.subscribe()`, on Bun on a channel kept referenced by {@link channel}. */ +export const subscribe: typeof nodeDiagnosticsChannel.subscribe = keepChannelsReferenced + ? (name, onMessage) => { + channel(name).subscribe(onMessage); + } + : nodeDiagnosticsChannel.subscribe; + +/** + * `diagnostics_channel.tracingChannel()`, with the returned tracing channel (and so its five + * channels) kept referenced on Bun when it is created by name. `undefined` where the runtime has + * no `tracingChannel` (Node < 18.19), like the original export. + */ +export const tracingChannel: typeof nodeDiagnosticsChannel.tracingChannel = + keepChannelsReferenced && nodeDiagnosticsChannel.tracingChannel + ? (((nameOrChannels: Parameters[0]) => { + if (typeof nameOrChannels !== 'string') { + return nodeDiagnosticsChannel.tracingChannel(nameOrChannels); + } + let result = tracingChannelsByName.get(nameOrChannels); + if (!result) { + result = nodeDiagnosticsChannel.tracingChannel(nameOrChannels); + tracingChannelsByName.set(nameOrChannels, result); + } + return result; + }) as typeof nodeDiagnosticsChannel.tracingChannel) + : nodeDiagnosticsChannel.tracingChannel; diff --git a/packages/server-utils/test/utils/diagnosticsChannel.test.ts b/packages/server-utils/test/utils/diagnosticsChannel.test.ts new file mode 100644 index 000000000000..31c0e49e5df5 --- /dev/null +++ b/packages/server-utils/test/utils/diagnosticsChannel.test.ts @@ -0,0 +1,76 @@ +import * as nodeDiagnosticsChannel from 'node:diagnostics_channel'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import type * as DiagnosticsChannelModule from '../../src/utils/diagnosticsChannel'; + +describe('diagnosticsChannel', () => { + describe('outside of Bun', () => { + it('exports the original functions', async () => { + vi.resetModules(); + const diagnosticsChannel = await import('../../src/utils/diagnosticsChannel'); + + expect(diagnosticsChannel.channel).toBe(nodeDiagnosticsChannel.channel); + expect(diagnosticsChannel.subscribe).toBe(nodeDiagnosticsChannel.subscribe); + expect(diagnosticsChannel.tracingChannel).toBe(nodeDiagnosticsChannel.tracingChannel); + }); + }); + + describe('on Bun', () => { + let diagnosticsChannel: typeof DiagnosticsChannelModule; + + beforeAll(async () => { + vi.stubGlobal('Bun', {}); + vi.resetModules(); + diagnosticsChannel = await import('../../src/utils/diagnosticsChannel'); + }); + + afterAll(() => { + vi.unstubAllGlobals(); + }); + + it('wraps the original functions', () => { + expect(diagnosticsChannel.channel).not.toBe(nodeDiagnosticsChannel.channel); + expect(diagnosticsChannel.subscribe).not.toBe(nodeDiagnosticsChannel.subscribe); + expect(diagnosticsChannel.tracingChannel).not.toBe(nodeDiagnosticsChannel.tracingChannel); + }); + + it('returns the same channel object for a name', () => { + expect(diagnosticsChannel.channel('sentry-test:channel')).toBe(diagnosticsChannel.channel('sentry-test:channel')); + }); + + it('returns the same tracing channel object for a name', () => { + expect(diagnosticsChannel.tracingChannel('sentry-test:tracing')).toBe( + diagnosticsChannel.tracingChannel('sentry-test:tracing'), + ); + }); + + it('delivers messages to a handler subscribed by name', () => { + const messages: unknown[] = []; + diagnosticsChannel.subscribe('sentry-test:subscribe', message => messages.push(message)); + + nodeDiagnosticsChannel.channel('sentry-test:subscribe').publish('hello'); + + expect(messages).toEqual(['hello']); + }); + + it('delivers tracing events to subscribers of a tracing channel created by name', () => { + const events: string[] = []; + diagnosticsChannel.tracingChannel('sentry-test:tracing-events').subscribe({ + start: () => events.push('start'), + end: () => events.push('end'), + asyncStart: () => undefined, + asyncEnd: () => undefined, + error: () => undefined, + }); + + nodeDiagnosticsChannel.tracingChannel('sentry-test:tracing-events').traceSync(() => undefined, {}); + + expect(events).toEqual(['start', 'end']); + }); + + it('creates a tracing channel from channel objects without caching it', () => { + const channels = nodeDiagnosticsChannel.tracingChannel('sentry-test:objects'); + + expect(diagnosticsChannel.tracingChannel(channels).start).toBe(channels.start); + }); + }); +});