From 3299ddc8c8bc4d987e596b75d071fe457dbd1f5e Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 6 Jul 2026 10:49:25 -0700 Subject: [PATCH 1/5] chore(service-worker: Create scaffolding for sending message into the service-worker --- rspack.config.ts | 37 ++++- static/app/bootstrap/initializeApp.tsx | 2 - static/app/main.tsx | 47 +++--- static/app/serviceWorker/client/register.ts | 57 -------- .../client/serviceWorker.stories.tsx | 23 +++ .../client/serviceWorkerContext.tsx | 136 ++++++++++++++++++ .../client/serviceWorkerInterface.ts | 47 ++++++ static/app/serviceWorker/types.ts | 4 + .../worker/getUnhandledRejectionError.ts | 72 ++++++++++ .../worker/handleInboundEvent.ts | 14 ++ .../serviceWorker/worker/initializeSentry.ts | 115 +++++++++++++++ static/app/serviceWorker/worker/worker.ts | 85 ++++++++--- 12 files changed, 537 insertions(+), 102 deletions(-) delete mode 100644 static/app/serviceWorker/client/register.ts create mode 100644 static/app/serviceWorker/client/serviceWorker.stories.tsx create mode 100644 static/app/serviceWorker/client/serviceWorkerContext.tsx create mode 100644 static/app/serviceWorker/client/serviceWorkerInterface.ts create mode 100644 static/app/serviceWorker/types.ts create mode 100644 static/app/serviceWorker/worker/getUnhandledRejectionError.ts create mode 100644 static/app/serviceWorker/worker/handleInboundEvent.ts create mode 100644 static/app/serviceWorker/worker/initializeSentry.ts diff --git a/rspack.config.ts b/rspack.config.ts index de07270f55d9..da34be6f93d2 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -606,7 +606,40 @@ const workerConfig: Configuration = { context: staticPrefix, experiments: appConfig.experiments, lazyCompilation: appConfig.lazyCompilation, - module: appConfig.module, + module: { + rules: [ + { + test: /\.ts$/, + // core-js: Avoids recompiling core-js based on usage imports + // compiled via swc. + exclude: /node_modules[\\/](core-js)/, + loader: 'builtin:swc-loader', + options: { + env: { + mode: 'usage', + // https://rspack.rs/guide/features/builtin-swc-loader#polyfill-injection + coreJs: '3.45.0', + targets: packageJson.browserslist.production, + shippedProposals: true, + }, + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + transform: { + react: { + runtime: 'automatic', + development: DEV_MODE, + refresh: false, + }, + }, + }, + isModule: 'unknown', + }, + }, + ], + }, plugins: [ /** * Without this, webpack will chunk the locales but attempt to load them all @@ -626,7 +659,7 @@ const workerConfig: Configuration = { resolve: appConfig.resolve, // Don't clean: app's compiler owns cleaning `dist` (see its `clean.keep`). output: {...appConfig.output, clean: false}, - optimization: appConfig.optimization, + optimization: {...appConfig.optimization, runtimeChunk: false}, devtool: appConfig.devtool, }; diff --git a/static/app/bootstrap/initializeApp.tsx b/static/app/bootstrap/initializeApp.tsx index feef0a35d3a2..19206cb659d5 100644 --- a/static/app/bootstrap/initializeApp.tsx +++ b/static/app/bootstrap/initializeApp.tsx @@ -1,7 +1,6 @@ import './legacyTwitterBootstrap'; import './exportGlobals'; -import {registerWorker} from 'sentry/serviceWorker/client/register'; import type {Config} from 'sentry/types/system'; import {metric} from 'sentry/utils/analytics'; @@ -21,5 +20,4 @@ export function initializeApp(config: Config) { metric.mark({name: 'sentry-app-init'}); renderOnDomReady(renderMain); processInitQueue(); - registerWorker(); } diff --git a/static/app/main.tsx b/static/app/main.tsx index db54c836448f..7f0d11cf33a6 100644 --- a/static/app/main.tsx +++ b/static/app/main.tsx @@ -18,6 +18,7 @@ import {SENTRY_RELEASE_VERSION} from 'sentry/constants/sdk'; import {preload} from 'sentry/router/preload'; import {RouteConfigProvider} from 'sentry/router/routeConfigContext'; import {routes} from 'sentry/router/routes'; +import {ServiceWorkerProvider} from 'sentry/serviceWorker/client/serviceWorkerContext'; import {createReactRouter3Navigate} from 'sentry/utils/useNavigate'; function buildRouter() { @@ -39,28 +40,30 @@ export function Main() { - - - - - - - - - {USE_TANSTACK_DEVTOOL && ( - , - }, - formDevtoolsPlugin(), - pacerDevtoolsPlugin(), - ]} - /> - )} - + + + + + + + + + + {USE_TANSTACK_DEVTOOL && ( + , + }, + formDevtoolsPlugin(), + pacerDevtoolsPlugin(), + ]} + /> + )} + + diff --git a/static/app/serviceWorker/client/register.ts b/static/app/serviceWorker/client/register.ts deleted file mode 100644 index 3fa0b07f7cb5..000000000000 --- a/static/app/serviceWorker/client/register.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {addIntegration, webWorkerIntegration} from '@sentry/react'; - -type WebWorkerIntegration = ReturnType; -let integration: WebWorkerIntegration | null = null; - -function getWorkerUrl(): string { - return window.__SENTRY_DEV_UI ? '/entrypoints/service-worker.js' : '/service-worker.js'; -} - -function connectWorker(worker: ServiceWorker): void { - const w = worker as unknown as Worker; - if (integration) { - integration.addWorker(w); - } else { - integration = webWorkerIntegration({worker: w}); - addIntegration(integration); - } -} - -function waitForActivation(worker: ServiceWorker): void { - if (worker.state === 'activated') { - connectWorker(worker); - return; - } - worker.addEventListener('statechange', () => { - if (worker.state === 'activated') { - connectWorker(worker); - } - }); -} - -export function registerWorker(): void { - if (!('serviceWorker' in navigator)) { - return; - } - - navigator.serviceWorker - // https://rspack.rs/guide/features/web-workers - .register(getWorkerUrl(), {scope: '/'}) - .then(registration => { - const incoming = registration.installing ?? registration.waiting; - if (incoming) { - waitForActivation(incoming); - } else if (registration.active) { - connectWorker(registration.active); - } - - registration.addEventListener('updatefound', () => { - if (registration.installing) { - waitForActivation(registration.installing); - } - }); - }) - .catch(() => { - // Registration failed — not critical, silently ignore - }); -} diff --git a/static/app/serviceWorker/client/serviceWorker.stories.tsx b/static/app/serviceWorker/client/serviceWorker.stories.tsx new file mode 100644 index 000000000000..fa7fe0377b82 --- /dev/null +++ b/static/app/serviceWorker/client/serviceWorker.stories.tsx @@ -0,0 +1,23 @@ +import {Button} from '@sentry/scraps/button'; +import {Flex, Stack} from '@sentry/scraps/layout'; +import {Text} from '@sentry/scraps/text'; + +import {useServiceWorker} from 'sentry/serviceWorker/client/serviceWorkerContext'; +import * as Storybook from 'sentry/stories'; + +export default Storybook.story('ServiceWorker', story => { + story('Basic Events', () => { + const {controller} = useServiceWorker(); + + return ( + + + + + Look in the console for the ping event. + + ); + }); +}); diff --git a/static/app/serviceWorker/client/serviceWorkerContext.tsx b/static/app/serviceWorker/client/serviceWorkerContext.tsx new file mode 100644 index 000000000000..6a6fa6657800 --- /dev/null +++ b/static/app/serviceWorker/client/serviceWorkerContext.tsx @@ -0,0 +1,136 @@ +import {createContext, useContext, useEffect} from 'react'; +import * as Sentry from '@sentry/react'; + +import {useFrontendVersion} from 'sentry/components/frontendVersionContext'; +import {ServiceWorkerController} from 'sentry/serviceWorker/client/serviceWorkerInterface'; + +const DEBUG_LOGGING = true; + +function log(message: string, options?: Sentry.metrics.MetricOptions) { + Sentry.metrics.count(`service-worker.register.${message}`, 1, options); + if (DEBUG_LOGGING) { + // eslint-disable-next-line no-console + console.log(`service-worker.register.${message}`); + } +} + +function getWorkerUrl(): string { + return window.__SENTRY_DEV_UI ? '/entrypoints/service-worker.js' : '/service-worker.js'; +} + +const Context = createContext({ + controller: new ServiceWorkerController(), +}); + +export function ServiceWorkerProvider({children}: {children: React.ReactNode}) { + const context = useContext(Context); + + useRegisterServiceWorker(); + useServiceWorkerUpdateCheck(); + useLogControllerChangeEvent(); + + return {children}; +} + +/** + * @public + * @returns The service worker context. + * @example + * const {controller} = useServiceWorker(); + * controller.postMessage({name: 'ping', type: 'event'}); + */ +export function useServiceWorker() { + return useContext(Context); +} + +/** + * Register a service worker and send event `worker.init` to the newest worker + * available. + */ +function useRegisterServiceWorker() { + useEffect(() => { + if (!('serviceWorker' in navigator)) { + log('not-supported'); + return; + } + + navigator.serviceWorker + // https://rspack.rs/guide/features/web-workers + .register(getWorkerUrl(), {scope: '/'}) + .then(registration => { + log('registered', { + attributes: { + // An old version could be active while the new instance is incoming + active: registration.active ? 'true' : 'false', + // The new instance should be `installing` to start, and should skip + // `waiting` to become `active` soon. + installing: registration.installing ? 'true' : 'false', + waiting: registration.waiting ? 'true' : 'false', + }, + }); + + const worker = registration.installing; + worker?.addEventListener('statechange', () => { + log('statechange', { + attributes: { + state: worker.state, + }, + }); + }); + }) + .catch(error => { + log('service-worker.register.error'); + Sentry.captureException(error); + }); + }, []); +} + +function useServiceWorkerUpdateCheck() { + const {state} = useFrontendVersion(); + + useEffect(() => { + if (!('serviceWorker' in navigator) || state === 'current') { + return; + } + + // A long-lived tab only learns about a new worker on the browser's + // infrequent periodic update check. Re-check whenever the tab becomes + // visible so a fresh deploy is picked up (trigger: install -> activate -> + // clients.claim -> controllerchange) as soon as the user returns to it. + const checkForUpdate = () => { + if (document.visibilityState !== 'visible') { + return; + } + log('update-check'); + navigator.serviceWorker.ready + .then(registration => registration.update()) + .catch(error => { + Sentry.captureException(error); + }); + }; + + document.addEventListener('visibilitychange', checkForUpdate); + return () => { + document.removeEventListener('visibilitychange', checkForUpdate); + }; + }, [state]); +} + +function useLogControllerChangeEvent() { + useEffect(() => { + if (!('serviceWorker' in navigator)) { + return; + } + + // Log whenever controllerchange happens, which means we have a new worker + // ready to listen to handle fetch and push requests. + // This event means that users have multiple tabs open at the same time, and + // the new workers are taking control of the pages. + const handler = () => log('controllerchange'); + + navigator.serviceWorker.addEventListener('controllerchange', handler); + return () => { + navigator.serviceWorker.removeEventListener('controllerchange', handler); + }; + }, []); +} diff --git a/static/app/serviceWorker/client/serviceWorkerInterface.ts b/static/app/serviceWorker/client/serviceWorkerInterface.ts new file mode 100644 index 000000000000..388520241b2e --- /dev/null +++ b/static/app/serviceWorker/client/serviceWorkerInterface.ts @@ -0,0 +1,47 @@ +import * as Sentry from '@sentry/react'; + +import type {EventMessage} from 'sentry/serviceWorker/types'; + +/** + * Sends messages from the page to the service worker. + * + * Every message must be an `EventMessage` (see `sentry/serviceWorker/types`). + * Use to notify the worker that something has happened on the page. + */ +export class ServiceWorkerController { + /** + * Find the service worker that this page should send messages to. + * + * Since we only need to post messages & not intercept network requests, + * we use `navigator.serviceWorker.ready` to get the active worker. + * We don't need to wait for the page to be controlled by the worker. + */ + private async getWorker(): Promise { + if (!('serviceWorker' in navigator)) { + return null; + } + // For more read: https://web.dev/articles/service-worker-lifecycle + const registration = await navigator.serviceWorker.ready; + return registration.installing ?? registration.waiting ?? registration.active; + } + + public postMessage(message: EventMessage): Promise { + return Sentry.startSpan( + { + name: 'service-worker.controller', + op: 'sw.postMessage', + attributes: {type: message.type, name: message.name}, + }, + async () => { + const worker = await this.getWorker(); + if (!worker) { + return; + } + if (message.type === 'event') { + worker.postMessage(message); + return; + } + } + ); + } +} diff --git a/static/app/serviceWorker/types.ts b/static/app/serviceWorker/types.ts new file mode 100644 index 000000000000..30069eda072c --- /dev/null +++ b/static/app/serviceWorker/types.ts @@ -0,0 +1,4 @@ +export type EventMessage = { + name: 'ping'; + type: 'event'; +}; diff --git a/static/app/serviceWorker/worker/getUnhandledRejectionError.ts b/static/app/serviceWorker/worker/getUnhandledRejectionError.ts new file mode 100644 index 000000000000..451a97a32757 --- /dev/null +++ b/static/app/serviceWorker/worker/getUnhandledRejectionError.ts @@ -0,0 +1,72 @@ +/** + * The file is extracted from `@sentry/javascript/packages/core/src/integrations/globalhandlers.ts` + */ + +type Primitive = number | string | boolean | bigint | symbol | null | undefined; + +type ParameterizedString = string & { + __sentry_template_string__?: string; + __sentry_template_values__?: unknown[]; +}; + +/** + * Checks whether given string is parameterized + * {@link isParameterizedString}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isParameterizedString(wat: unknown): wat is ParameterizedString { + return ( + typeof wat === 'object' && + wat !== null && + '__sentry_template_string__' in wat && + '__sentry_template_values__' in wat + ); +} + +/** + * Checks whether given value is a primitive (undefined, null, number, boolean, string, bigint, symbol) + * {@link isPrimitive}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isPrimitive(wat: unknown): wat is Primitive { + return ( + wat === null || + isParameterizedString(wat) || + (typeof wat !== 'object' && typeof wat !== 'function') + ); +} + +export function getUnhandledRejectionError(error: unknown): unknown { + if (isPrimitive(error)) { + return error; + } + + // dig the object of the rejection out of known event types + try { + type ErrorWithReason = {reason: unknown}; + // PromiseRejectionEvents store the object of the rejection under 'reason' + // see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent + if ('reason' in (error as ErrorWithReason)) { + return (error as ErrorWithReason).reason; + } + + type CustomEventWithDetail = {detail: {reason: unknown}}; + // something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents + // to CustomEvents, moving the `promise` and `reason` attributes of the PRE into + // the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec + // see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and + // https://github.com/getsentry/sentry-javascript/issues/2380 + if ( + 'detail' in (error as CustomEventWithDetail) && + 'reason' in (error as CustomEventWithDetail).detail + ) { + return (error as CustomEventWithDetail).detail.reason; + } + } catch {} // eslint-disable-line no-empty + + return error; +} diff --git a/static/app/serviceWorker/worker/handleInboundEvent.ts b/static/app/serviceWorker/worker/handleInboundEvent.ts new file mode 100644 index 000000000000..87de8007034a --- /dev/null +++ b/static/app/serviceWorker/worker/handleInboundEvent.ts @@ -0,0 +1,14 @@ +import type {EventMessage} from 'sentry/serviceWorker/types'; + +export function handleInboundEvent( + _sw: ServiceWorkerGlobalScope, + message: EventMessage +): void | Promise { + switch (message.name) { + case 'ping': + // eslint-disable-next-line no-console + return console.log('pong!'); + default: + return; + } +} diff --git a/static/app/serviceWorker/worker/initializeSentry.ts b/static/app/serviceWorker/worker/initializeSentry.ts new file mode 100644 index 000000000000..31e6329b1904 --- /dev/null +++ b/static/app/serviceWorker/worker/initializeSentry.ts @@ -0,0 +1,115 @@ +import * as Sentry from '@sentry/browser'; +import type {Breadcrumb} from '@sentry/browser'; + +import { + IGNORED_BREADCRUMB_FETCH_HOSTS, + IGNORED_SPAN_NAMES, + SENTRY_RELEASE_VERSION, + SPA_DSN, + SPA_MODE_ALLOW_URLS, + SPA_MODE_TRACE_PROPAGATION_TARGETS, +} from 'sentry/constants/sdk'; +import type {Config} from 'sentry/types/system'; + +let lastEventId: string | undefined; + +/** + * @public + * @returns The last event id. + */ +export function getLastEventId(): string | undefined { + return lastEventId; +} + +export function initializeSentry({ + apmSampling, + customerDomain, + dsn, + sentryConfig, + userIdentity, +}: Config) { + Sentry.init({ + allowUrls: SPA_DSN ? SPA_MODE_ALLOW_URLS : sentryConfig.allowUrls, + dsn: SPA_DSN || dsn, + release: SENTRY_RELEASE_VERSION ?? sentryConfig.release, + environment: sentryConfig.environment, + + dataCollection: {}, + enableLogs: true, + _experiments: { + enableMetrics: true, + }, + + defaultIntegrations: false, + + beforeBreadcrumb(crumb) { + return isFilteredBreadcrumb(crumb) ? null : crumb; + }, + + beforeSend(event, hint) { + lastEventId = event.event_id || hint.event_id; + + return event; + }, + + ignoreErrors: [ + /** + * There is a bug in Safari, that causes `AbortError` when fetch is + * aborted, and you are in the middle of reading the response. In Chrome + * and other browsers, it is handled gracefully, where in Safari, it + * produces additional error, that is jumping outside of the original + * Promise chain and bubbles up to the `unhandledRejection` handler, that + * we then captures as error. + * + * Ref: https://bugs.webkit.org/show_bug.cgi?id=215771 + */ + /AbortError: Fetch is aborted/i, + /AbortError: The operation was aborted/i, + /AbortError: signal is aborted without reason/i, + /AbortError: The user aborted a request/i, + ], + ignoreSpans: IGNORED_SPAN_NAMES, + + tracesSampleRate: apmSampling ?? 0, + tracePropagationTargets: [ + 'localhost', + /^\//, + ...(SPA_DSN + ? SPA_MODE_TRACE_PROPAGATION_TARGETS + : sentryConfig.tracePropagationTargets), + ], + }); + + Sentry.addEventProcessor((event: Sentry.Event, _hint?: Sentry.EventHint) => { + event.tags = event.tags || {}; + return event; + }); + + if (SENTRY_RELEASE_VERSION) { + Sentry.setTag('sentry_version', SENTRY_RELEASE_VERSION); + } + + if (userIdentity) { + Sentry.setUser(userIdentity); + } + + if (customerDomain) { + Sentry.setTag('isCustomerDomain', 'yes'); + Sentry.setTag('customerDomain.organizationUrl', customerDomain.organizationUrl); + Sentry.setTag('customerDomain.sentryUrl', customerDomain.sentryUrl); + Sentry.setTag('customerDomain.subdomain', customerDomain.subdomain); + } +} + +function isFilteredBreadcrumb(crumb: Breadcrumb): boolean { + // Ignore fetch/xhr requests to certain hosts + const isFetch = crumb.category === 'fetch' || crumb.category === 'xhr'; + if ( + isFetch && + IGNORED_BREADCRUMB_FETCH_HOSTS.some(host => crumb.data?.url?.includes(host)) + ) { + return true; + } + + return false; +} diff --git a/static/app/serviceWorker/worker/worker.ts b/static/app/serviceWorker/worker/worker.ts index 626ce9960e6b..4b6fd92f0355 100644 --- a/static/app/serviceWorker/worker/worker.ts +++ b/static/app/serviceWorker/worker/worker.ts @@ -1,30 +1,77 @@ -import {registerWebWorker} from '@sentry/browser'; -import {startSpan} from '@sentry/core'; +import * as Sentry from '@sentry/browser'; + +import {getUnhandledRejectionError} from 'sentry/serviceWorker/worker/getUnhandledRejectionError'; +import {handleInboundEvent} from 'sentry/serviceWorker/worker/handleInboundEvent'; +import {initializeSentry} from 'sentry/serviceWorker/worker/initializeSentry'; const sw = self as unknown as ServiceWorkerGlobalScope; -// registerWebWorker expects DedicatedWorkerGlobalScope.postMessage, which -// doesn't exist on ServiceWorkerGlobalScope. The cast is safe: the only -// call to postMessage sends debug IDs and will be a no-op here; the -// unhandledrejection handler still registers correctly. -registerWebWorker({self: sw as any}); +const DEBUG_LOGGING = true; + +function log(message: string) { + Sentry.metrics.count(`service-worker.worker.${message}`); + if (DEBUG_LOGGING) { + // eslint-disable-next-line no-console + console.log(`service-worker.worker.${message}`); + } +} -sw.addEventListener('install', () => { - startSpan({name: 'service-worker.install', op: 'sw.lifecycle'}, () => { - return sw.skipWaiting(); - }); +sw.addEventListener('install', event => { + log('onInstall'); + event.waitUntil( + Promise.all([ + // Force activation to happen without waiting for the current worker to + // have zero clients. This worker instance will activate and claim all + // the clients for itself. + sw.skipWaiting(), + // If this fetch fails, or initializeSentry throws an error, the worker + // will not be 'installed' and cannot be activated. We'll try again when + // the next update-check downloads a new version. + fetch('/api/client-config/', { + credentials: 'include', + headers: {'Content-Type': 'application/json'}, + }) + .then(data => data.json()) + .then(initializeSentry), + ]).then(() => { + log('didInstall'); + }) + ); }); sw.addEventListener('activate', event => { + log('onActivate'); + + event.waitUntil(sw.clients.claim()); +}); + +sw.addEventListener('unhandledrejection', (event: unknown) => { + log('onUnhandledRejection'); + const reason = getUnhandledRejectionError(event); + Sentry.captureException(reason); +}); + +sw.addEventListener('message', event => { event.waitUntil( - startSpan({name: 'service-worker.activate', op: 'sw.lifecycle'}, () => - sw.clients.claim() + Sentry.startSpan( + { + name: 'service-worker.worker.onMessage', + op: 'sw.onmessage', + attributes: { + type: event.data.type, + name: event.data.name, + messageId: event.data.messageId, + }, + }, + async () => { + if (DEBUG_LOGGING) { + // eslint-disable-next-line no-console + console.log('service-worker.worker.onMessage'); + } + if (event.data.type === 'event') { + await handleInboundEvent(sw, event.data); + } + } ) ); }); - -sw.addEventListener('message', _event => { - startSpan({name: 'service-worker.message', op: 'sw.message'}, () => { - // No custom message handlers yet - }); -}); From d64ddd6eca31085e26d0c1dd3f66432fcf05dbcc Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 6 Jul 2026 15:18:18 -0700 Subject: [PATCH 2/5] fix log message duplicate prefix --- static/app/serviceWorker/client/serviceWorkerContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/serviceWorker/client/serviceWorkerContext.tsx b/static/app/serviceWorker/client/serviceWorkerContext.tsx index 6a6fa6657800..59d2c63d79a2 100644 --- a/static/app/serviceWorker/client/serviceWorkerContext.tsx +++ b/static/app/serviceWorker/client/serviceWorkerContext.tsx @@ -79,7 +79,7 @@ function useRegisterServiceWorker() { }); }) .catch(error => { - log('service-worker.register.error'); + log('error'); Sentry.captureException(error); }); }, []); From 201cfb77f5dba8cc62c6534519d3790b6eaad19c Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 6 Jul 2026 15:19:48 -0700 Subject: [PATCH 3/5] defend against messages without `event.data` object property --- static/app/serviceWorker/worker/worker.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/app/serviceWorker/worker/worker.ts b/static/app/serviceWorker/worker/worker.ts index 4b6fd92f0355..3d9f409b9671 100644 --- a/static/app/serviceWorker/worker/worker.ts +++ b/static/app/serviceWorker/worker/worker.ts @@ -52,6 +52,9 @@ sw.addEventListener('unhandledrejection', (event: unknown) => { }); sw.addEventListener('message', event => { + if (!event.data || typeof event.data !== 'object') { + return; + } event.waitUntil( Sentry.startSpan( { From 2473cfe46b99920bd76a1f99fdf3329c8ee2d792 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 6 Jul 2026 15:32:25 -0700 Subject: [PATCH 4/5] Dont import from static/app/types/system.tsx, it pulls in the whole app when we do `pnpm typecheck` --- .../serviceWorker/worker/initializeSentry.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/static/app/serviceWorker/worker/initializeSentry.ts b/static/app/serviceWorker/worker/initializeSentry.ts index 31e6329b1904..f09900fb64df 100644 --- a/static/app/serviceWorker/worker/initializeSentry.ts +++ b/static/app/serviceWorker/worker/initializeSentry.ts @@ -9,7 +9,6 @@ import { SPA_MODE_ALLOW_URLS, SPA_MODE_TRACE_PROPAGATION_TARGETS, } from 'sentry/constants/sdk'; -import type {Config} from 'sentry/types/system'; let lastEventId: string | undefined; @@ -21,13 +20,38 @@ export function getLastEventId(): string | undefined { return lastEventId; } +// A subset of the Config interface. (see: sentry/static/app/types/system.tsx) +export interface Props { + apmSampling: number; + customerDomain: { + organizationUrl: string | undefined; + sentryUrl: string; + subdomain: string; + } | null; + dsn: string; + sentryConfig: { + allowUrls: string[]; + dsn: string; + release: string; + tracePropagationTargets: string[]; + environment?: string; + profileSessionSampleRate?: number; + }; + userIdentity: { + email: string; + id: string; + ip_address: string; + isStaff: boolean; + }; +} + export function initializeSentry({ apmSampling, customerDomain, dsn, sentryConfig, userIdentity, -}: Config) { +}: Props) { Sentry.init({ allowUrls: SPA_DSN ? SPA_MODE_ALLOW_URLS : sentryConfig.allowUrls, dsn: SPA_DSN || dsn, From 6b778343954b76c44d16f9f47e3f912218ff0684 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 6 Jul 2026 16:38:48 -0700 Subject: [PATCH 5/5] disable debug logging into the console --- static/app/serviceWorker/client/serviceWorkerContext.tsx | 2 +- static/app/serviceWorker/worker/worker.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/serviceWorker/client/serviceWorkerContext.tsx b/static/app/serviceWorker/client/serviceWorkerContext.tsx index 59d2c63d79a2..718a5dbeac89 100644 --- a/static/app/serviceWorker/client/serviceWorkerContext.tsx +++ b/static/app/serviceWorker/client/serviceWorkerContext.tsx @@ -4,7 +4,7 @@ import * as Sentry from '@sentry/react'; import {useFrontendVersion} from 'sentry/components/frontendVersionContext'; import {ServiceWorkerController} from 'sentry/serviceWorker/client/serviceWorkerInterface'; -const DEBUG_LOGGING = true; +const DEBUG_LOGGING = false; function log(message: string, options?: Sentry.metrics.MetricOptions) { Sentry.metrics.count(`service-worker.register.${message}`, 1, options); diff --git a/static/app/serviceWorker/worker/worker.ts b/static/app/serviceWorker/worker/worker.ts index 3d9f409b9671..b248095cd247 100644 --- a/static/app/serviceWorker/worker/worker.ts +++ b/static/app/serviceWorker/worker/worker.ts @@ -6,7 +6,7 @@ import {initializeSentry} from 'sentry/serviceWorker/worker/initializeSentry'; const sw = self as unknown as ServiceWorkerGlobalScope; -const DEBUG_LOGGING = true; +const DEBUG_LOGGING = false; function log(message: string) { Sentry.metrics.count(`service-worker.worker.${message}`);