From 71de7e2b39bec72e214c00a70eb43d026db3b7ce Mon Sep 17 00:00:00 2001 From: Faizan Date: Wed, 14 Oct 2026 14:47:45 +0530 Subject: [PATCH 1/4] feat(browser): add LoaderSentry type --- packages/browser/src/exports.ts | 1 + packages/browser/src/loader.ts | 27 +++++++++++++++++++++ packages/browser/test/loader.test-d.ts | 33 ++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 packages/browser/src/loader.ts create mode 100644 packages/browser/test/loader.test-d.ts diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 48b09d3c1acc..f7d96bb5f872 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'; export { addEventProcessor, diff --git a/packages/browser/src/loader.ts b/packages/browser/src/loader.ts new file mode 100644 index 000000000000..654812b39d18 --- /dev/null +++ b/packages/browser/src/loader.ts @@ -0,0 +1,27 @@ + +type LoaderApi = typeof import('@sentry/core') & + typeof import('./sdk') & + typeof import('./report-dialog'); + +type VoidReturningFunction = T extends (...args: infer Args) => unknown + ? (...args: Args) => void + : never; + +type LoaderQueuedMethod = + | 'init' + | 'addBreadcrumb' + | 'captureMessage' + | 'captureException' + | 'captureEvent' + | 'withScope' + | 'showReportDialog'; + +/** + * The subset of the Sentry browser API supported by the CDN loader. + * + * Queued calls do not synchronously return the result of the underlying SDK + * method, so their return types are void. + */ +export type LoaderSentry = { + [Method in LoaderQueuedMethod]: VoidReturningFunction; +} & Pick; diff --git a/packages/browser/test/loader.test-d.ts b/packages/browser/test/loader.test-d.ts new file mode 100644 index 000000000000..c06eb393fa9b --- /dev/null +++ b/packages/browser/test/loader.test-d.ts @@ -0,0 +1,33 @@ +import { expectTypeOf, it, describe } from 'vitest'; +import type { LoaderSentry } from '../src/loader'; + +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(); + }); +}); From 40c2d23beb2d2f774035a264f687441bc9286728 Mon Sep 17 00:00:00 2001 From: Faizan Date: Wed, 14 Oct 2026 15:38:00 +0530 Subject: [PATCH 2/4] test(browser): enable loader type tests --- packages/browser/tsconfig.loader.test.json | 4 ++++ packages/browser/vite.config.ts | 6 ++++++ 2 files changed, 10 insertions(+) create mode 100644 packages/browser/tsconfig.loader.test.json diff --git a/packages/browser/tsconfig.loader.test.json b/packages/browser/tsconfig.loader.test.json new file mode 100644 index 000000000000..efe243dbdad2 --- /dev/null +++ b/packages/browser/tsconfig.loader.test.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.test.json", + "include": ["test/loader.test-d.ts"] +} diff --git a/packages/browser/vite.config.ts b/packages/browser/vite.config.ts index 0869faa66b74..f29f65d0a6a3 100644 --- a/packages/browser/vite.config.ts +++ b/packages/browser/vite.config.ts @@ -1,3 +1,4 @@ + import { defineConfig } from 'vitest/config'; import baseConfig from '../../vite/vite.config'; @@ -9,5 +10,10 @@ export default defineConfig({ fakeTimers: { toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'setImmediate', 'clearImmediate', 'Date'], }, + typecheck: { + enabled: true, + include: ['test/loader.test-d.ts'], + tsconfig: './tsconfig.loader.test.json', + }, }, }); From d30573d0d926336c6af51a6441c771996824421a Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 16 Oct 2026 12:10:48 +0200 Subject: [PATCH 3/4] ref(browser): Use named type imports for LoaderSentry Replace the `typeof import()` type annotations, which the consistent-type-imports lint rule rejects, with named type imports and an explicit interface. Type `withScope` with only its callback overload, since inferring from the overloaded function kept just the two-argument form. Add a usage example and rename the file to loader-types.ts. Run the type tests with the shared tsconfig.test.json and ignoreSourceErrors, like the cloudflare package does. Refs #21806 Co-Authored-By: Claude Opus 5.5 --- packages/browser/src/exports.ts | 2 +- packages/browser/src/loader-types.ts | 42 +++++++++++++++++++ packages/browser/src/loader.ts | 27 ------------ ...oader.test-d.ts => loader-types.test-d.ts} | 17 +++++++- packages/browser/tsconfig.loader.test.json | 4 -- packages/browser/vite.config.ts | 5 +-- 6 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 packages/browser/src/loader-types.ts delete mode 100644 packages/browser/src/loader.ts rename packages/browser/test/{loader.test-d.ts => loader-types.test-d.ts} (65%) delete mode 100644 packages/browser/tsconfig.loader.test.json diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index f7d96bb5f872..b56b786dc877 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -23,7 +23,7 @@ export type { } from '@sentry/core'; export type { BrowserOptions } from './client'; -export type { LoaderSentry } from './loader'; +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..a8fe5ffe7721 --- /dev/null +++ b/packages/browser/src/loader-types.ts @@ -0,0 +1,42 @@ +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()`. + * + * @example + * ```ts + * import type { LoaderSentry } from '@sentry/browser'; + * + * declare global { + * interface Window { + * Sentry: LoaderSentry; + * } + * } + * + * window.Sentry.onLoad(() => { + * window.Sentry.init({ release: 'my-app@1.0.0' }); + * }); + * + * window.Sentry.captureException(new Error('Something went wrong')); + * ``` + */ +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/src/loader.ts b/packages/browser/src/loader.ts deleted file mode 100644 index 654812b39d18..000000000000 --- a/packages/browser/src/loader.ts +++ /dev/null @@ -1,27 +0,0 @@ - -type LoaderApi = typeof import('@sentry/core') & - typeof import('./sdk') & - typeof import('./report-dialog'); - -type VoidReturningFunction = T extends (...args: infer Args) => unknown - ? (...args: Args) => void - : never; - -type LoaderQueuedMethod = - | 'init' - | 'addBreadcrumb' - | 'captureMessage' - | 'captureException' - | 'captureEvent' - | 'withScope' - | 'showReportDialog'; - -/** - * The subset of the Sentry browser API supported by the CDN loader. - * - * Queued calls do not synchronously return the result of the underlying SDK - * method, so their return types are void. - */ -export type LoaderSentry = { - [Method in LoaderQueuedMethod]: VoidReturningFunction; -} & Pick; diff --git a/packages/browser/test/loader.test-d.ts b/packages/browser/test/loader-types.test-d.ts similarity index 65% rename from packages/browser/test/loader.test-d.ts rename to packages/browser/test/loader-types.test-d.ts index c06eb393fa9b..e3b8e29621cd 100644 --- a/packages/browser/test/loader.test-d.ts +++ b/packages/browser/test/loader-types.test-d.ts @@ -1,5 +1,6 @@ import { expectTypeOf, it, describe } from 'vitest'; -import type { LoaderSentry } from '../src/loader'; +import type { Scope } from '@sentry/core'; +import type { LoaderSentry } from '../src/loader-types'; describe('LoaderSentry', () => { it('exposes the APIs supported by the loader', () => { @@ -30,4 +31,18 @@ describe('LoaderSentry', () => { 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/tsconfig.loader.test.json b/packages/browser/tsconfig.loader.test.json deleted file mode 100644 index efe243dbdad2..000000000000 --- a/packages/browser/tsconfig.loader.test.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.test.json", - "include": ["test/loader.test-d.ts"] -} diff --git a/packages/browser/vite.config.ts b/packages/browser/vite.config.ts index f29f65d0a6a3..2513b813e44b 100644 --- a/packages/browser/vite.config.ts +++ b/packages/browser/vite.config.ts @@ -1,4 +1,3 @@ - import { defineConfig } from 'vitest/config'; import baseConfig from '../../vite/vite.config'; @@ -12,8 +11,8 @@ export default defineConfig({ }, typecheck: { enabled: true, - include: ['test/loader.test-d.ts'], - tsconfig: './tsconfig.loader.test.json', + tsconfig: './tsconfig.test.json', + ignoreSourceErrors: true, }, }, }); From 14abcc2e3f2dbf0873b1d5d7c7403891e5adb90e Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 16 Oct 2026 12:20:31 +0200 Subject: [PATCH 4/4] docs(browser): Show full SDK cast inside onLoad in LoaderSentry example Inside an `onLoad` callback the full SDK is loaded, but the loader calls the callback without arguments, so the type can't express this. Document casting `window.Sentry` to the full SDK type instead. Refs #21806 Co-Authored-By: Claude Opus 5.5 --- packages/browser/src/loader-types.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/loader-types.ts b/packages/browser/src/loader-types.ts index a8fe5ffe7721..871bdf80089a 100644 --- a/packages/browser/src/loader-types.ts +++ b/packages/browser/src/loader-types.ts @@ -9,10 +9,12 @@ type Queued = Fn extends (...args: infer Args) => unknown ? (...args: Args) * 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()`. + * 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 { @@ -21,11 +23,13 @@ type Queued = Fn extends (...args: infer Args) => unknown ? (...args: Args) * } * } * + * window.Sentry.captureException(new Error('Something went wrong')); + * * window.Sentry.onLoad(() => { - * window.Sentry.init({ release: 'my-app@1.0.0' }); + * const Sentry = window.Sentry as typeof SentryBrowser; + * Sentry.init({ release: 'my-app@1.0.0' }); + * Sentry.addIntegration(Sentry.replayIntegration()); * }); - * - * window.Sentry.captureException(new Error('Something went wrong')); * ``` */ export interface LoaderSentry {