From 60fbfc187d37302085cd188fafd8f0e6241a4645 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:47:30 +0000 Subject: [PATCH 1/5] feat(cloudflare): Add Spotlight integration for local dev event forwarding Add a fetch-based spotlightIntegration to @sentry/cloudflare that mirrors envelopes to a local Spotlight sidecar during development. This closes the gap where Cloudflare Workers was the only server SDK without Spotlight support. - New integration at src/integrations/spotlight.ts using fetch + suppressTracing - Wire in sdk.ts init() with rollup-include-development-only markers (stripped from prod builds) - Read SENTRY_SPOTLIGHT from CF env binding in getFinalOptions (boolean or URL) - Export spotlightIntegration from package index - Full test coverage (integration tests + options tests) --- packages/cloudflare/src/index.ts | 1 + .../cloudflare/src/integrations/spotlight.ts | 89 ++++++++ packages/cloudflare/src/options.ts | 7 + packages/cloudflare/src/sdk.ts | 11 + .../test/integrations/spotlight.test.ts | 211 ++++++++++++++++++ packages/cloudflare/test/options.test.ts | 35 +++ 6 files changed, 354 insertions(+) create mode 100644 packages/cloudflare/src/integrations/spotlight.ts create mode 100644 packages/cloudflare/test/integrations/spotlight.test.ts diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 33bcf07c24e7..9fab08768510 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -127,6 +127,7 @@ export { getDefaultIntegrations } from './sdk'; export { httpServerIntegration } from './integrations/httpServer'; export { fetchIntegration } from './integrations/fetch'; +export { spotlightIntegration } from './integrations/spotlight'; export { vercelAIIntegration } from './integrations/tracing/vercelai'; // eslint-disable-next-line typescript/no-deprecated diff --git a/packages/cloudflare/src/integrations/spotlight.ts b/packages/cloudflare/src/integrations/spotlight.ts new file mode 100644 index 000000000000..ead0e8f552c3 --- /dev/null +++ b/packages/cloudflare/src/integrations/spotlight.ts @@ -0,0 +1,89 @@ +import type { Client, Envelope, IntegrationFn } from '@sentry/core'; +import { debug, defineIntegration, serializeEnvelope, suppressTracing } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; + +type SpotlightConnectionOptions = { + /** + * Set this if the Spotlight Sidecar is not running on localhost:8969. + * By default, the URL is set to http://localhost:8969/stream + */ + sidecarUrl?: string; +}; + +export const INTEGRATION_NAME = 'Spotlight' as const; + +const _spotlightIntegration = ((options: Partial = {}) => { + const sidecarUrl = options.sidecarUrl || 'http://localhost:8969/stream'; + + return { + name: INTEGRATION_NAME, + setup(client) { + DEBUG_BUILD && debug.log('[Spotlight] Using Sidecar URL', sidecarUrl); + setupSidecarForwarding(client, sidecarUrl); + }, + }; +}) satisfies IntegrationFn; + +/** + * Use this integration to send errors and transactions to Spotlight. + * + * Learn more about spotlight at https://spotlightjs.com + * + * Important: This integration is intended for local development only. + * Each forwarded envelope counts as a Worker subrequest (50 free / 1000 paid + * per invocation), so it should not be enabled in production. + */ +export const spotlightIntegration = defineIntegration(_spotlightIntegration); + +function setupSidecarForwarding(client: Client, sidecarUrl: string): void { + const parsedUrl = parseSidecarUrl(sidecarUrl); + if (!parsedUrl) { + return; + } + + let failCount = 0; + + client.on('beforeEnvelope', (envelope: Envelope) => { + if (failCount > 3) { + DEBUG_BUILD && debug.warn('[Spotlight] Disabled Sentry -> Spotlight forwarding due to too many failed requests'); + return; + } + + const body = serializeEnvelope(envelope); + + suppressTracing(() => { + fetch(parsedUrl.href, { + method: 'POST', + body, + headers: { + 'Content-Type': 'application/x-sentry-envelope', + }, + }).then( + res => { + // Consume the response body to satisfy Cloudflare Workers' requirement + // that all fetch response bodies are read or cancelled. + res.text().catch(() => { + // no-op + }); + + if (res.status >= 200 && res.status < 400) { + failCount = 0; + } + }, + () => { + failCount++; + DEBUG_BUILD && debug.warn('[Spotlight] Failed to send envelope to Spotlight Sidecar'); + }, + ); + }); + }); +} + +function parseSidecarUrl(url: string): URL | undefined { + try { + return new URL(url); + } catch { + DEBUG_BUILD && debug.warn(`[Spotlight] Invalid sidecar URL: ${url}`); + return undefined; + } +} diff --git a/packages/cloudflare/src/options.ts b/packages/cloudflare/src/options.ts index 7506dd34468e..d67767bd2421 100644 --- a/packages/cloudflare/src/options.ts +++ b/packages/cloudflare/src/options.ts @@ -54,6 +54,12 @@ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknow const tracesSampleRate = userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? ''); + // Spotlight: user option > env boolean > env URL string + const spotlightEnvVar = getEnvVar(env, 'SENTRY_SPOTLIGHT'); + const spotlightEnvBool = envToBool(spotlightEnvVar, { strict: true }); + const spotlight = + userOptions.spotlight ?? spotlightEnvBool ?? (spotlightEnvBool === null ? spotlightEnvVar : undefined); + return { release, ...userOptions, @@ -62,5 +68,6 @@ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknow tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined, debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')), tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'), + spotlight, }; } diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index 5bdb8f07e28c..ac85151846f0 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -17,6 +17,7 @@ import { CloudflareClient } from './client'; import { makeFlushLock } from './flush'; import { httpServerIntegration } from './integrations/httpServer'; import { fetchIntegration } from './integrations/fetch'; +import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from './integrations/spotlight'; import { setupOpenTelemetryTracer } from './opentelemetry/tracer'; import { makeCloudflareTransport } from './transport'; import { defaultStackParser } from './vendor/stacktrace'; @@ -88,6 +89,16 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined { flushLock, }; + /*! rollup-include-development-only */ + if (options.spotlight && !clientOptions.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { + clientOptions.integrations.push( + spotlightIntegration({ + sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, + }), + ); + } + /*! rollup-include-development-only-end */ + /** * The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility * via a custom trace provider. diff --git a/packages/cloudflare/test/integrations/spotlight.test.ts b/packages/cloudflare/test/integrations/spotlight.test.ts new file mode 100644 index 000000000000..4ac057b9102a --- /dev/null +++ b/packages/cloudflare/test/integrations/spotlight.test.ts @@ -0,0 +1,211 @@ +import type { Envelope, EventEnvelope } from '@sentry/core'; +import { createEnvelope, debug } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { CloudflareClient } from '../../src/client'; +import { INTEGRATION_NAME, spotlightIntegration } from '../../src/integrations/spotlight'; +import { createStackParser } from '@sentry/core'; + +function createTestClient(): CloudflareClient { + return new CloudflareClient({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + integrations: [], + transport: () => ({ + send: () => Promise.resolve({}), + flush: () => Promise.resolve(true), + }), + stackParser: createStackParser(), + }); +} + +function createTestEnvelope(): EventEnvelope { + return createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ + [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }], + ]); +} + +describe('Spotlight (Cloudflare)', () => { + const debugWarnSpy = vi.spyOn(debug, 'warn'); + let fetchSpy: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + fetchSpy = vi.fn().mockResolvedValue({ + status: 200, + text: () => Promise.resolve(''), + }); + vi.stubGlobal('fetch', fetchSpy); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('has integration name "Spotlight"', () => { + const integration = spotlightIntegration(); + expect(integration.name).toEqual(INTEGRATION_NAME); + expect(integration.name).toEqual('Spotlight'); + }); + + it('registers a callback on the beforeEnvelope hook', () => { + const client = createTestClient(); + const onSpy = vi.spyOn(client, 'on'); + + const integration = spotlightIntegration(); + integration.setup!(client); + + expect(onSpy).toHaveBeenCalledWith('beforeEnvelope', expect.any(Function)); + }); + + it('sends an envelope POST request to the default sidecar URL', () => { + let callback: (envelope: Envelope) => void = () => {}; + const client = createTestClient(); + vi.spyOn(client, 'on').mockImplementationOnce((_hook: string, cb: (envelope: Envelope) => void) => { + callback = cb; + return () => {}; + }); + + const integration = spotlightIntegration(); + integration.setup!(client); + + callback(createTestEnvelope()); + + expect(fetchSpy).toHaveBeenCalledWith( + 'http://localhost:8969/stream', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/x-sentry-envelope' }, + }), + ); + }); + + it('sends an envelope POST request to a custom sidecar URL', () => { + let callback: (envelope: Envelope) => void = () => {}; + const client = createTestClient(); + vi.spyOn(client, 'on').mockImplementationOnce((_hook: string, cb: (envelope: Envelope) => void) => { + callback = cb; + return () => {}; + }); + + const integration = spotlightIntegration({ sidecarUrl: 'http://mylocalhost:8888/abcd' }); + integration.setup!(client); + + callback(createTestEnvelope()); + + expect(fetchSpy).toHaveBeenCalledWith( + 'http://mylocalhost:8888/abcd', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/x-sentry-envelope' }, + }), + ); + }); + + it('serializes the envelope body', () => { + let callback: (envelope: Envelope) => void = () => {}; + const client = createTestClient(); + vi.spyOn(client, 'on').mockImplementationOnce((_hook: string, cb: (envelope: Envelope) => void) => { + callback = cb; + return () => {}; + }); + + const integration = spotlightIntegration(); + integration.setup!(client); + + callback(createTestEnvelope()); + + const body = fetchSpy.mock.calls[0]![1].body as string; + expect(body).toContain('aa3ff046696b4bc6b609ce6d28fde9e2'); + expect(typeof body).toBe('string'); + }); + + it('stops forwarding after more than 3 failed requests', async () => { + let callback: (envelope: Envelope) => void = () => {}; + const client = createTestClient(); + vi.spyOn(client, 'on').mockImplementationOnce((_hook: string, cb: (envelope: Envelope) => void) => { + callback = cb; + return () => {}; + }); + + fetchSpy.mockRejectedValue(new Error('connection refused')); + + const integration = spotlightIntegration(); + integration.setup!(client); + + const envelope = createTestEnvelope(); + + // 4 failed requests should trigger the disable + for (let i = 0; i < 4; i++) { + callback(envelope); + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(i + 1)); + } + + fetchSpy.mockClear(); + callback(envelope); + + // Wait a tick to ensure any async handling is done + await new Promise(resolve => setTimeout(resolve, 10)); + + // The 5th call should not reach fetch + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('resets fail count on successful request', async () => { + let callback: (envelope: Envelope) => void = () => {}; + const client = createTestClient(); + vi.spyOn(client, 'on').mockImplementationOnce((_hook: string, cb: (envelope: Envelope) => void) => { + callback = cb; + return () => {}; + }); + + // Fail 3 times, then succeed + fetchSpy + .mockRejectedValueOnce(new Error('fail')) + .mockRejectedValueOnce(new Error('fail')) + .mockRejectedValueOnce(new Error('fail')) + .mockResolvedValueOnce({ status: 200, text: () => Promise.resolve('') }); + + const integration = spotlightIntegration(); + integration.setup!(client); + + const envelope = createTestEnvelope(); + + for (let i = 0; i < 4; i++) { + callback(envelope); + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(i + 1)); + } + + // After the success, fail count should be reset, so the next call should go through + fetchSpy.mockResolvedValueOnce({ status: 200, text: () => Promise.resolve('') }); + callback(envelope); + + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(5)); + }); + + it('warns on invalid sidecar URL', () => { + const client = createTestClient(); + + const integration = spotlightIntegration({ sidecarUrl: 'not-a-valid-url' }); + integration.setup!(client); + + expect(debugWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid sidecar URL: not-a-valid-url')); + }); + + it('does not call fetch for invalid sidecar URL', () => { + let callback: (envelope: Envelope) => void = () => {}; + const client = createTestClient(); + vi.spyOn(client, 'on').mockImplementationOnce((_hook: string, cb: (envelope: Envelope) => void) => { + callback = cb; + return () => {}; + }); + + const integration = spotlightIntegration({ sidecarUrl: 'not-a-valid-url' }); + integration.setup!(client); + + // If the URL is invalid, the beforeEnvelope hook is never registered + // so callback is never replaced — it's still the no-op default + callback(createTestEnvelope()); + + expect(fetchSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cloudflare/test/options.test.ts b/packages/cloudflare/test/options.test.ts index 9dc21606b445..92935007025d 100644 --- a/packages/cloudflare/test/options.test.ts +++ b/packages/cloudflare/test/options.test.ts @@ -189,4 +189,39 @@ describe('getFinalOptions', () => { expect(result).toEqual(expect.objectContaining({ dsn: 'test-dsn', release: undefined })); }); }); + + describe('SENTRY_SPOTLIGHT', () => { + it('reads SENTRY_SPOTLIGHT boolean "true" from env', () => { + const result = getFinalOptions({}, { SENTRY_SPOTLIGHT: 'true' }); + expect(result.spotlight).toBe(true); + }); + + it('reads SENTRY_SPOTLIGHT boolean "false" from env', () => { + const result = getFinalOptions({}, { SENTRY_SPOTLIGHT: 'false' }); + expect(result.spotlight).toBe(false); + }); + + it('reads SENTRY_SPOTLIGHT URL string from env', () => { + const result = getFinalOptions({}, { SENTRY_SPOTLIGHT: 'http://localhost:9999/stream' }); + expect(result.spotlight).toBe('http://localhost:9999/stream'); + }); + + it('user option takes precedence over env', () => { + const result = getFinalOptions({ spotlight: false }, { SENTRY_SPOTLIGHT: 'true' }); + expect(result.spotlight).toBe(false); + }); + + it('user option string takes precedence over env', () => { + const result = getFinalOptions( + { spotlight: 'http://custom:1234/stream' }, + { SENTRY_SPOTLIGHT: 'http://other:5678/stream' }, + ); + expect(result.spotlight).toBe('http://custom:1234/stream'); + }); + + it('returns undefined when SENTRY_SPOTLIGHT is not set', () => { + const result = getFinalOptions({}, { SENTRY_DSN: 'test-dsn' }); + expect(result.spotlight).toBeUndefined(); + }); + }); }); From 320966e5ab43faffeb521ff4fdade328ae15e6e5 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:59:49 +0000 Subject: [PATCH 2/5] fix(cloudflare): Add spotlight property to BaseCloudflareOptions CloudflareOptions extends Options (CoreOptions) which does not include ServerRuntimeOptions where spotlight is declared. Add the property directly to BaseCloudflareOptions so TypeScript resolves it correctly. --- packages/cloudflare/src/client.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index 087c1ad720d9..c87729cb8c8d 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -252,7 +252,19 @@ interface BaseCloudflareOptions { * * @default false */ - instrumentPrototypeMethods?: boolean | string[]; + instrumentPrototypeMethods?: boolean | string[]; + + /** + * If you use Spotlight by Sentry during development, use + * this option to forward captured Sentry events to Spotlight. + * + * Either set it to true, or provide a specific Spotlight Sidecar URL. + * + * More details: https://spotlightjs.com/ + * + * IMPORTANT: Only set this option to `true` while developing, not in production! + */ + spotlight?: boolean | string; } /** From c1568fe77cbc9b4594024b6baf7436271066b758 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:10:06 +0000 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20Address=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20spotlight=20precedence,=20indent,=20test=20gaps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix spotlight precedence to match node-core's getSpotlightConfig: spotlight: true + env URL → uses env URL (not bare true) - Fix accidental extra-space indent on instrumentPrototypeMethods - Add tests: spotlight:true + env URL, 4xx/5xx status handling --- packages/cloudflare/src/client.ts | 2 +- packages/cloudflare/src/options.ts | 37 ++++++++++++++++--- .../test/integrations/spotlight.test.ts | 23 ++++++++++++ packages/cloudflare/test/options.test.ts | 15 ++++++++ 4 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index c87729cb8c8d..f14d1719f962 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -252,7 +252,7 @@ interface BaseCloudflareOptions { * * @default false */ - instrumentPrototypeMethods?: boolean | string[]; + instrumentPrototypeMethods?: boolean | string[]; /** * If you use Spotlight by Sentry during development, use diff --git a/packages/cloudflare/src/options.ts b/packages/cloudflare/src/options.ts index d67767bd2421..28e5f911d8b0 100644 --- a/packages/cloudflare/src/options.ts +++ b/packages/cloudflare/src/options.ts @@ -54,11 +54,11 @@ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknow const tracesSampleRate = userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? ''); - // Spotlight: user option > env boolean > env URL string - const spotlightEnvVar = getEnvVar(env, 'SENTRY_SPOTLIGHT'); - const spotlightEnvBool = envToBool(spotlightEnvVar, { strict: true }); - const spotlight = - userOptions.spotlight ?? spotlightEnvBool ?? (spotlightEnvBool === null ? spotlightEnvVar : undefined); + // Spotlight precedence (mirrors node-core's getSpotlightConfig): + // - false or explicit string from options: use as-is + // - true: enable, but prefer a custom URL from the env var if set + // - undefined: defer entirely to the env var (bool or URL) + const spotlight = getSpotlightFromEnv(userOptions.spotlight, getEnvVar(env, 'SENTRY_SPOTLIGHT')); return { release, @@ -71,3 +71,30 @@ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknow spotlight, }; } + +/** + * Resolve the spotlight option from a user-supplied value and an env binding string. + * Mirrors node-core's `getSpotlightConfig` precedence: + * - `false` or explicit string from options → use as-is + * - `true` → enable, but prefer a custom URL from the env var if set + * - `undefined` → defer entirely to the env var (bool or URL) + */ +function getSpotlightFromEnv( + optionsSpotlight: boolean | string | undefined, + envVar: string | undefined, +): boolean | string | undefined { + if (optionsSpotlight === false) { + return false; + } + if (typeof optionsSpotlight === 'string') { + return optionsSpotlight; + } + + // optionsSpotlight is true or undefined + const envBool = envToBool(envVar, { strict: true }); + const envUrl = envBool === null && envVar ? envVar : undefined; + + return optionsSpotlight === true + ? (envUrl ?? true) // true: use env URL if present, otherwise true + : (envBool ?? envUrl); // undefined: use env var (bool or URL) +} diff --git a/packages/cloudflare/test/integrations/spotlight.test.ts b/packages/cloudflare/test/integrations/spotlight.test.ts index 4ac057b9102a..e485715b6e6a 100644 --- a/packages/cloudflare/test/integrations/spotlight.test.ts +++ b/packages/cloudflare/test/integrations/spotlight.test.ts @@ -208,4 +208,27 @@ describe('Spotlight (Cloudflare)', () => { expect(fetchSpy).not.toHaveBeenCalled(); }); + + it('does not increment fail count on 4xx/5xx responses', async () => { + let callback: (envelope: Envelope) => void = () => {}; + const client = createTestClient(); + vi.spyOn(client, 'on').mockImplementationOnce((_hook: string, cb: (envelope: Envelope) => void) => { + callback = cb; + return () => {}; + }); + + fetchSpy.mockResolvedValue({ status: 500, text: () => Promise.resolve('') }); + + const integration = spotlightIntegration(); + integration.setup!(client); + + const envelope = createTestEnvelope(); + + // 5 calls with 500 status — fail count is NOT incremented for HTTP errors, + // only for network rejections, so fetch should still be called each time + for (let i = 0; i < 5; i++) { + callback(envelope); + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(i + 1)); + } + }); }); diff --git a/packages/cloudflare/test/options.test.ts b/packages/cloudflare/test/options.test.ts index 92935007025d..e999df4b7970 100644 --- a/packages/cloudflare/test/options.test.ts +++ b/packages/cloudflare/test/options.test.ts @@ -223,5 +223,20 @@ describe('getFinalOptions', () => { const result = getFinalOptions({}, { SENTRY_DSN: 'test-dsn' }); expect(result.spotlight).toBeUndefined(); }); + + it('spotlight: true prefers env URL over boolean true', () => { + const result = getFinalOptions({ spotlight: true }, { SENTRY_SPOTLIGHT: 'http://custom:1234/stream' }); + expect(result.spotlight).toBe('http://custom:1234/stream'); + }); + + it('spotlight: true stays true when env is boolean "true"', () => { + const result = getFinalOptions({ spotlight: true }, { SENTRY_SPOTLIGHT: 'true' }); + expect(result.spotlight).toBe(true); + }); + + it('spotlight: true stays true when env is not set', () => { + const result = getFinalOptions({ spotlight: true }, {}); + expect(result.spotlight).toBe(true); + }); }); }); From 169f735b92b1c14632cf6a151ef9768154e82ae7 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:00:41 +0000 Subject: [PATCH 4/5] fix(cloudflare): Remove non-functional dev-only markers around spotlight wiring The rollup-include-development-only markers only strip content when the package build uses splitDevProd (only @sentry/browser does). The Cloudflare package uses a single build without that plugin, so the markers were inert and misleading. Match the node-core pattern: guard spotlight wiring with the runtime 'if (options.spotlight)' check, which is falsy in production. --- packages/cloudflare/src/sdk.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index ac85151846f0..58ebc9625818 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -89,7 +89,6 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined { flushLock, }; - /*! rollup-include-development-only */ if (options.spotlight && !clientOptions.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { clientOptions.integrations.push( spotlightIntegration({ @@ -97,7 +96,6 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined { }), ); } - /*! rollup-include-development-only-end */ /** * The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility From dff8f1b52467c03f40bc058be25f2ccd67ec27e1 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 22 Jul 2026 17:53:37 +0100 Subject: [PATCH 5/5] feat(cloudflare): Add dev/prod bundle in favor of spotlight --- .size-limit.js | 4 +- packages/cloudflare/package.json | 56 ++++++++++------------- packages/cloudflare/rollup.npm.config.mjs | 1 + packages/cloudflare/src/options.ts | 4 ++ packages/cloudflare/src/sdk.ts | 2 + 5 files changed, 32 insertions(+), 35 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 4814ea0a8911..92e47f0f4981 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -436,7 +436,7 @@ module.exports = [ // Cloudflare SDK (ESM) - compressed, minified to match `wrangler deploy --dry-run --minify` output { name: '@sentry/cloudflare (withSentry) - minified', - path: 'packages/cloudflare/build/esm/index.js', + path: 'packages/cloudflare/build/esm/prod/index.js', import: createImport('withSentry', 'instrumentDurableObjectWithSentry', 'instrumentWorkflowWithSentry'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, @@ -456,7 +456,7 @@ module.exports = [ // Cloudflare SDK (ESM) - uncompressed, unminified to match `wrangler deploy --dry-run` output { name: '@sentry/cloudflare (withSentry)', - path: 'packages/cloudflare/build/esm/index.js', + path: 'packages/cloudflare/build/esm/prod/index.js', import: createImport('withSentry', 'instrumentDurableObjectWithSentry', 'instrumentWorkflowWithSentry'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index c4724f6ef29f..f21c46518565 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -12,50 +12,40 @@ "files": [ "/build" ], - "main": "build/cjs/index.js", - "module": "build/esm/index.js", + "main": "build/cjs/prod/index.js", + "module": "build/esm/prod/index.js", "types": "build/types/index.d.ts", "exports": { "./package.json": "./package.json", ".": { - "import": { - "types": "./build/types/index.d.ts", - "default": "./build/esm/index.js" + "types": "./build/types/index.d.ts", + "development": { + "import": "./build/esm/dev/index.js", + "require": "./build/cjs/dev/index.js" }, - "require": { - "types": "./build/types/index.d.ts", - "default": "./build/cjs/index.js" + "production": { + "import": "./build/esm/prod/index.js", + "require": "./build/cjs/prod/index.js" + }, + "default": { + "import": "./build/esm/prod/index.js", + "require": "./build/cjs/prod/index.js" } }, "./request": { - "import": { - "types": "./build/types/request.d.ts", - "default": "./build/esm/request.js" - }, - "require": { - "types": "./build/types/request.d.ts", - "default": "./build/cjs/request.js" - } + "types": "./build/types/request.d.ts", + "import": "./build/esm/prod/request.js", + "require": "./build/cjs/prod/request.js" }, "./nodejs_compat": { - "import": { - "types": "./build/types/nodejs_compat/index.d.ts", - "default": "./build/esm/nodejs_compat/index.js" - }, - "require": { - "types": "./build/types/nodejs_compat/index.d.ts", - "default": "./build/cjs/nodejs_compat/index.js" - } + "types": "./build/types/nodejs_compat/index.d.ts", + "import": "./build/esm/prod/nodejs_compat/index.js", + "require": "./build/cjs/prod/nodejs_compat/index.js" }, "./vite": { - "import": { - "types": "./build/types/vite/index.d.ts", - "default": "./build/esm/vite/index.js" - }, - "require": { - "types": "./build/types/vite/index.d.ts", - "default": "./build/cjs/vite/index.js" - } + "types": "./build/types/vite/index.d.ts", + "import": "./build/esm/prod/vite/index.js", + "require": "./build/cjs/prod/vite/index.js" } }, "publishConfig": { @@ -98,7 +88,7 @@ "clean": "rimraf build coverage sentry-cloudflare-*.tgz", "lint:fix": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --fix --type-aware", "lint": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --type-aware", - "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", + "lint:es-compatibility": "es-check es2022 ./build/cjs/prod/*.js && es-check es2022 ./build/esm/prod/*.js --module", "test": "yarn test:unit", "test:unit": "vitest run", "test:watch": "vitest --watch", diff --git a/packages/cloudflare/rollup.npm.config.mjs b/packages/cloudflare/rollup.npm.config.mjs index 9b674514ca4f..839ccaa4c8db 100644 --- a/packages/cloudflare/rollup.npm.config.mjs +++ b/packages/cloudflare/rollup.npm.config.mjs @@ -4,4 +4,5 @@ export default makeNPMConfigVariants( makeBaseNPMConfig({ entrypoints: ['src/index.ts', 'src/nodejs_compat/index.ts', 'src/vite/index.ts'], }), + { splitDevProd: true }, ); diff --git a/packages/cloudflare/src/options.ts b/packages/cloudflare/src/options.ts index 28e5f911d8b0..947d6d921892 100644 --- a/packages/cloudflare/src/options.ts +++ b/packages/cloudflare/src/options.ts @@ -58,7 +58,9 @@ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknow // - false or explicit string from options: use as-is // - true: enable, but prefer a custom URL from the env var if set // - undefined: defer entirely to the env var (bool or URL) + /*! rollup-include-development-only */ const spotlight = getSpotlightFromEnv(userOptions.spotlight, getEnvVar(env, 'SENTRY_SPOTLIGHT')); + /*! rollup-include-development-only-end */ return { release, @@ -68,7 +70,9 @@ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknow tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined, debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')), tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'), + /*! rollup-include-development-only */ spotlight, + /*! rollup-include-development-only-end */ }; } diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index 58ebc9625818..ac85151846f0 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -89,6 +89,7 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined { flushLock, }; + /*! rollup-include-development-only */ if (options.spotlight && !clientOptions.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { clientOptions.integrations.push( spotlightIntegration({ @@ -96,6 +97,7 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined { }), ); } + /*! rollup-include-development-only-end */ /** * The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility