diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 48b09d3c1acc..b56b786dc877 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -23,6 +23,7 @@ export type { } from '@sentry/core'; export type { BrowserOptions } from './client'; +export type { LoaderSentry } from './loader-types'; export { addEventProcessor, diff --git a/packages/browser/src/loader-types.ts b/packages/browser/src/loader-types.ts new file mode 100644 index 000000000000..871bdf80089a --- /dev/null +++ b/packages/browser/src/loader-types.ts @@ -0,0 +1,46 @@ +import type { addBreadcrumb, captureEvent, captureException, captureMessage, Scope } from '@sentry/core'; +import type { showReportDialog } from './report-dialog'; +import type { forceLoad, init, onLoad } from './sdk'; + +/** Queued calls are replayed once the SDK loads, so they can't return the SDK method's result. */ +type Queued = Fn extends (...args: infer Args) => unknown ? (...args: Args) => void : never; + +/** + * The subset of the Sentry browser API that is safe to call on the CDN loader's + * `window.Sentry` stub before the full SDK has loaded. + * + * Any other SDK API needs to be wrapped in `Sentry.onLoad()`. Inside the callback, the full SDK is + * loaded, so `window.Sentry` can be cast to the full SDK type. + * + * @example + * ```ts + * import type * as SentryBrowser from '@sentry/browser'; + * import type { LoaderSentry } from '@sentry/browser'; + * + * declare global { + * interface Window { + * Sentry: LoaderSentry; + * } + * } + * + * window.Sentry.captureException(new Error('Something went wrong')); + * + * window.Sentry.onLoad(() => { + * const Sentry = window.Sentry as typeof SentryBrowser; + * Sentry.init({ release: 'my-app@1.0.0' }); + * Sentry.addIntegration(Sentry.replayIntegration()); + * }); + * ``` + */ +export interface LoaderSentry { + init: Queued; + addBreadcrumb: Queued; + captureMessage: Queued; + captureException: Queued; + captureEvent: Queued; + showReportDialog: Queued; + // Only the callback overload: there's no way to get hold of a `Scope` before the SDK has loaded. + withScope: (callback: (scope: Scope) => void) => void; + onLoad: typeof onLoad; + forceLoad: typeof forceLoad; +} diff --git a/packages/browser/test/loader-types.test-d.ts b/packages/browser/test/loader-types.test-d.ts new file mode 100644 index 000000000000..e3b8e29621cd --- /dev/null +++ b/packages/browser/test/loader-types.test-d.ts @@ -0,0 +1,48 @@ +import { expectTypeOf, it, describe } from 'vitest'; +import type { Scope } from '@sentry/core'; +import type { LoaderSentry } from '../src/loader-types'; + +describe('LoaderSentry', () => { + it('exposes the APIs supported by the loader', () => { + expectTypeOf().toHaveProperty('init'); + expectTypeOf().toHaveProperty('addBreadcrumb'); + expectTypeOf().toHaveProperty('captureMessage'); + expectTypeOf().toHaveProperty('captureException'); + expectTypeOf().toHaveProperty('captureEvent'); + expectTypeOf().toHaveProperty('withScope'); + expectTypeOf().toHaveProperty('showReportDialog'); + expectTypeOf().toHaveProperty('onLoad'); + expectTypeOf().toHaveProperty('forceLoad'); + }); + + it('does not expose unsupported APIs', () => { + expectTypeOf().not.toHaveProperty('addIntegration'); + expectTypeOf().not.toHaveProperty('captureFeedback'); + expectTypeOf().not.toHaveProperty('flush'); + expectTypeOf().not.toHaveProperty('close'); + }); + + it('uses void return types for queued methods', () => { + expectTypeOf().returns.toBeVoid(); + expectTypeOf().returns.toBeVoid(); + expectTypeOf().returns.toBeVoid(); + expectTypeOf().returns.toBeVoid(); + expectTypeOf().returns.toBeVoid(); + expectTypeOf().returns.toBeVoid(); + expectTypeOf().returns.toBeVoid(); + }); + + it('keeps the parameters of the SDK methods', () => { + expectTypeOf().toBeCallableWith({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + expectTypeOf().toBeCallableWith(new Error('test'), { tags: { foo: 'bar' } }); + expectTypeOf().toBeCallableWith('test', 'warning'); + expectTypeOf().toBeCallableWith(() => {}); + }); + + it('only exposes the callback overload of withScope', () => { + expectTypeOf().toBeCallableWith(scope => { + scope.setTag('foo', 'bar'); + }); + expectTypeOf().parameters.toEqualTypeOf<[callback: (scope: Scope) => void]>(); + }); +}); diff --git a/packages/browser/vite.config.ts b/packages/browser/vite.config.ts index 0869faa66b74..2513b813e44b 100644 --- a/packages/browser/vite.config.ts +++ b/packages/browser/vite.config.ts @@ -9,5 +9,10 @@ export default defineConfig({ fakeTimers: { toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'setImmediate', 'clearImmediate', 'Date'], }, + typecheck: { + enabled: true, + tsconfig: './tsconfig.test.json', + ignoreSourceErrors: true, + }, }, });