diff --git a/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario-no-integrations.ts b/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario-no-integrations.ts new file mode 100644 index 000000000000..d7f5b49d899f --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario-no-integrations.ts @@ -0,0 +1,6 @@ +import * as Sentry from '@sentry/bun'; + +Sentry.initWithoutDefaultIntegrations({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, +}); diff --git a/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario.ts b/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario.ts new file mode 100644 index 000000000000..0ce01f05a57b --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario.ts @@ -0,0 +1,6 @@ +import * as Sentry from '@sentry/bun'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, +}); diff --git a/dev-packages/bun-integration-tests/suites/no-channel-injection/test.ts b/dev-packages/bun-integration-tests/suites/no-channel-injection/test.ts new file mode 100644 index 000000000000..0a8953ea3aba --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/no-channel-injection/test.ts @@ -0,0 +1,22 @@ +import { spawnSync } from 'child_process'; +import { join } from 'path'; +import { describe, expect, it } from 'vitest'; + +function runScenario(file: string): string { + const res = spawnSync('bun', ['run', join(__dirname, file)], { cwd: __dirname, encoding: 'utf8' }); + expect(res.status, `run failed:\nstderr:\n${res.stderr}\nstdout:\n${res.stdout}`).toBe(0); + return `${res.stdout}${res.stderr}`; +} + +describe('no channel injection (Bun)', () => { + it('warns without `debug` when the app runs unbuilt with `bun run`', () => { + const output = runScenario('scenario.ts'); + + expect(output).toContain('No diagnostics-channel injection detected'); + expect(output).toContain('@sentry/bun/plugin'); + }); + + it('does not warn when no channel integration is installed', () => { + expect(runScenario('scenario-no-integrations.ts')).not.toContain('No diagnostics-channel injection detected'); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/runner/createRunner.ts b/dev-packages/node-integration-tests/utils/runner/createRunner.ts index b592869e9aca..db07a127ff12 100644 --- a/dev-packages/node-integration-tests/utils/runner/createRunner.ts +++ b/dev-packages/node-integration-tests/utils/runner/createRunner.ts @@ -665,8 +665,18 @@ export function createRunner(...paths: string[]) { if (process.env.DEBUG) log('stderr line', output); + // Bun runs these scenarios without the Bun build plugin, so the SDK warns that channel + // injection is missing. That warning is expected here and is not an error. + const unexpectedOutput = + process.env.RUNTIME === 'bun' + ? output + .split('\n') + .filter(line => !line.includes('[Sentry] No diagnostics-channel injection detected.')) + .join('\n') + : output; + // Ignore deprecation warnings for this purpose - if (ensureNoErrorOutput && !`${output}`.includes('DeprecationWarning:')) { + if (ensureNoErrorOutput && unexpectedOutput.trim() && !unexpectedOutput.includes('DeprecationWarning:')) { complete(new Error(`Expected no error output but got: '${output}'`)); } }); diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts index 2ca3d60bc21d..fd88b420a2bc 100644 --- a/packages/core/src/utils/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -95,6 +95,16 @@ export type InternalGlobal = { * Dedupes the one-time warning and short-circuits repeat calls. */ runtimeUnavailable?: boolean; + /** + * Set when an integration asks to subscribe to orchestrion-injected + * channels, so a missing injection is only reported when it loses data. + */ + channelIntegrations?: boolean; + /** + * Set once the Bun warning for missing channel injection has been shown. + * Lives on the marker so that duplicate SDK copies warn only once. + */ + warnedBunRunNotInjected?: boolean; /** * Module namespaces stashed by build-time provider imports, keyed by module * name (e.g. `@mastra/observability`). A bundler plugin can splice a static diff --git a/packages/server-utils/src/orchestrion/detect.ts b/packages/server-utils/src/orchestrion/detect.ts index 572afea7b5f5..c0674371e50a 100644 --- a/packages/server-utils/src/orchestrion/detect.ts +++ b/packages/server-utils/src/orchestrion/detect.ts @@ -1,4 +1,4 @@ -import { debug, GLOBAL_OBJ } from '@sentry/core'; +import { consoleSandbox, debug, GLOBAL_OBJ } from '@sentry/core'; /** * The module names (e.g. `mysql`, `@hapi/hapi`) orchestrion has already injected @@ -32,9 +32,25 @@ export function getOrchestrionInjectedModules(): string[] { * is fine. */ export function detectOrchestrionSetup(): void { - const { runtime, bundler } = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ ?? {}; + const marker = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ ?? {}; + const { runtime, bundler, channelIntegrations } = marker; if (!runtime && !bundler) { + if (channelIntegrations && typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') { + if (!marker.warnedBunRunNotInjected) { + marker.warnedBunRunNotInjected = true; + consoleSandbox(() => { + // oxlint-disable-next-line no-console + console.warn( + '[Sentry] No diagnostics-channel injection detected. Channel-based integrations will not record ' + + 'spans or capture errors, because Bun cannot instrument modules at runtime. Build your app with ' + + '`@sentry/bun/plugin` to enable them.', + ); + }); + } + return; + } + debug.warn( '[Sentry] No diagnostics-channel injection detected. Channel-based integrations ' + 'will not record spans. Make sure the diagnostics channels are injected ' + diff --git a/packages/server-utils/src/orchestrion/instrumentation.ts b/packages/server-utils/src/orchestrion/instrumentation.ts index 0dbddefc4da7..bf793330c9eb 100644 --- a/packages/server-utils/src/orchestrion/instrumentation.ts +++ b/packages/server-utils/src/orchestrion/instrumentation.ts @@ -58,6 +58,8 @@ export function invokeOrchestrionInstrumentation { beforeEach(() => { @@ -25,3 +25,65 @@ describe('getOrchestrionInjectedModules', () => { expect(getOrchestrionInjectedModules()).toEqual(['pg']); }); }); + +describe('detectOrchestrionSetup', () => { + beforeEach(() => { + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + }); + + afterEach(() => { + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it('always warns on Bun when no injection is detected', () => { + vi.stubGlobal('Bun', {}); + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { channelIntegrations: true }; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('@sentry/bun/plugin')); + }); + + it('warns on Bun only once when called again, as on a repeated `init()`', () => { + vi.stubGlobal('Bun', {}); + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { channelIntegrations: true }; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + detectOrchestrionSetup(); + + expect(warn).toHaveBeenCalledTimes(1); + }); + + it('does not warn on Bun when no channel integration is installed', () => { + vi.stubGlobal('Bun', {}); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + + expect(warn).not.toHaveBeenCalled(); + }); + + it('does not warn on Bun when the bundler plugin ran', () => { + vi.stubGlobal('Bun', {}); + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: new Set(), channelIntegrations: true }; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + + expect(warn).not.toHaveBeenCalled(); + }); + + it('does not warn outside Bun when debug is off', () => { + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { channelIntegrations: true }; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + + expect(warn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/server-utils/test/orchestrion/instrumentation.test.ts b/packages/server-utils/test/orchestrion/instrumentation.test.ts index 07c61fd5bb7a..0322105fd948 100644 --- a/packages/server-utils/test/orchestrion/instrumentation.test.ts +++ b/packages/server-utils/test/orchestrion/instrumentation.test.ts @@ -46,6 +46,12 @@ describe('invokeOrchestrionInstrumentation', () => { delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; }); + it('records on the marker that a channel integration is installed', () => { + invokeOrchestrionInstrumentation(makeClient() as never, ['mysql'], vi.fn(), []); + + expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.channelIntegrations).toBe(true); + }); + it('subscribes immediately when a module is already injected', () => { GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { runtime: ['mysql'] }; const client = makeClient();