diff --git a/packages/hono/src/bun/sdk.ts b/packages/hono/src/bun/sdk.ts index d30269058f4d..892b7535bca6 100644 --- a/packages/hono/src/bun/sdk.ts +++ b/packages/hono/src/bun/sdk.ts @@ -1,5 +1,5 @@ import type { Client } from '@sentry/core'; -import { applySdkMetadata } from '@sentry/core'; +import { applySdkMetadata, consoleSandbox, getClient } from '@sentry/core'; import { init as initBun } from '@sentry/bun'; import type { HonoBunOptions } from './middleware'; import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations'; @@ -12,6 +12,15 @@ import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations'; * When manually calling `init`, add the `honoIntegration` to the `integrations` array to set up the Hono integration. */ export function init(options: HonoBunOptions): Client | undefined { + if (getClient()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] Sentry is already initialized. Sentry should only be initialized once, through the `sentry()` middleware. Remove the `Sentry.init()` call, if one exists.', + ); + }); + } + applySdkMetadata(options, 'hono', ['hono', 'bun']); // Remove Hono from the SDK defaults to prevent double instrumentation: @sentry/bun diff --git a/packages/hono/test/bun/middleware.test.ts b/packages/hono/test/bun/middleware.test.ts index 8e3135e52afb..0506a211e06e 100644 --- a/packages/hono/test/bun/middleware.test.ts +++ b/packages/hono/test/bun/middleware.test.ts @@ -3,6 +3,7 @@ import { SDK_VERSION } from '@sentry/core'; import { Hono } from 'hono'; import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; import { sentry } from '../../src/bun/middleware'; +import { init } from '../../src/bun/sdk'; vi.mock('@sentry/bun', () => ({ init: vi.fn(), @@ -18,10 +19,14 @@ vi.mock('@sentry/core', async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore applySdkMetadata: vi.fn(actual.applySdkMetadata), + getClient: vi.fn(() => undefined), + // Pass-through so console.warn calls inside consoleSandbox are observable in tests + consoleSandbox: vi.fn((cb: () => unknown) => cb()), }; }); const applySdkMetadataMock = SentryCore.applySdkMetadata as Mock; +const getClientMock = SentryCore.getClient as Mock; describe('Hono Bun Middleware', () => { beforeEach(() => { @@ -51,7 +56,8 @@ describe('Hono Bun Middleware', () => { expect(applySdkMetadataMock).toHaveBeenCalledWith(options, 'hono', ['hono', 'bun']); }); - it('calls init from @sentry/bun', () => { + it('calls init from @sentry/bun when no client exists yet', () => { + getClientMock.mockReturnValue(undefined); const app = new Hono(); const options = { dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -156,4 +162,47 @@ describe('Hono Bun Middleware', () => { ); }); }); + + describe('double-init guard', () => { + it('still calls init even when Sentry is already initialized', () => { + const fakeClient = { getOptions: () => ({}) }; + getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + + const app = new Hono(); + sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(initBunMock).toHaveBeenCalledTimes(1); + }); + + it('emits a console.warn directing to remove the duplicate init call when Sentry is already initialized', () => { + const warnSpy = vi.spyOn(console, 'warn'); + const fakeClient = { getOptions: () => ({}) }; + getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + + const app = new Hono(); + sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Sentry is already initialized')); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Remove the `Sentry.init()` call')); + }); + + it('does not emit a console.warn when no client exists yet', () => { + const warnSpy = vi.spyOn(console, 'warn'); + getClientMock.mockReturnValue(undefined); + + const app = new Hono(); + sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('always calls init regardless of whether a client already exists', () => { + getClientMock.mockReturnValue(undefined); + + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(initBunMock).toHaveBeenCalledTimes(1); + }); + }); });