Skip to content

Commit e96e15e

Browse files
feat(v10/cloudflare): Add Spotlight integration for local dev event forwarding (#22796)
Backport of: #22490 And reverted dev/prod bundle split, as `wrangler` on its own doesn't support that --------- Co-authored-by: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com>
1 parent ea4a322 commit e96e15e

8 files changed

Lines changed: 430 additions & 1 deletion

File tree

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ module.exports = [
480480
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
481481
gzip: false,
482482
brotli: false,
483-
limit: '480 KiB',
483+
limit: '490 KiB',
484484
disablePlugins: ['@size-limit/webpack'],
485485
webpack: false,
486486
modifyEsbuildConfig: function (config) {

packages/cloudflare/src/client.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,18 @@ interface BaseCloudflareOptions {
294294
* @default false
295295
*/
296296
instrumentPrototypeMethods?: boolean | string[];
297+
298+
/**
299+
* If you use Spotlight by Sentry during development, use
300+
* this option to forward captured Sentry events to Spotlight.
301+
*
302+
* Either set it to true, or provide a specific Spotlight Sidecar URL.
303+
*
304+
* More details: https://spotlightjs.com/
305+
*
306+
* IMPORTANT: Only set this option to `true` while developing, not in production!
307+
*/
308+
spotlight?: boolean | string;
297309
}
298310

299311
/**

packages/cloudflare/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export { getDefaultIntegrations } from './sdk';
130130

131131
export { httpServerIntegration } from './integrations/httpServer';
132132
export { fetchIntegration } from './integrations/fetch';
133+
export { spotlightIntegration } from './integrations/spotlight';
133134
export { vercelAIIntegration } from './integrations/tracing/vercelai';
134135
// eslint-disable-next-line typescript/no-deprecated
135136
export { honoIntegration } from './integrations/hono';
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { Client, Envelope, IntegrationFn } from '@sentry/core';
2+
import { debug, defineIntegration, serializeEnvelope, suppressTracing } from '@sentry/core';
3+
import { DEBUG_BUILD } from '../debug-build';
4+
5+
type SpotlightConnectionOptions = {
6+
/**
7+
* Set this if the Spotlight Sidecar is not running on localhost:8969.
8+
* By default, the URL is set to http://localhost:8969/stream
9+
*/
10+
sidecarUrl?: string;
11+
};
12+
13+
export const INTEGRATION_NAME = 'Spotlight' as const;
14+
15+
const _spotlightIntegration = ((options: Partial<SpotlightConnectionOptions> = {}) => {
16+
const sidecarUrl = options.sidecarUrl || 'http://localhost:8969/stream';
17+
18+
return {
19+
name: INTEGRATION_NAME,
20+
setup(client) {
21+
DEBUG_BUILD && debug.log('[Spotlight] Using Sidecar URL', sidecarUrl);
22+
setupSidecarForwarding(client, sidecarUrl);
23+
},
24+
};
25+
}) satisfies IntegrationFn;
26+
27+
/**
28+
* Use this integration to send errors and transactions to Spotlight.
29+
*
30+
* Learn more about spotlight at https://spotlightjs.com
31+
*
32+
* Important: This integration is intended for local development only.
33+
* Each forwarded envelope counts as a Worker subrequest (50 free / 1000 paid
34+
* per invocation), so it should not be enabled in production.
35+
*/
36+
export const spotlightIntegration = defineIntegration(_spotlightIntegration);
37+
38+
function setupSidecarForwarding(client: Client, sidecarUrl: string): void {
39+
const parsedUrl = parseSidecarUrl(sidecarUrl);
40+
if (!parsedUrl) {
41+
return;
42+
}
43+
44+
let failCount = 0;
45+
46+
client.on('beforeEnvelope', (envelope: Envelope) => {
47+
if (failCount > 3) {
48+
DEBUG_BUILD && debug.warn('[Spotlight] Disabled Sentry -> Spotlight forwarding due to too many failed requests');
49+
return;
50+
}
51+
52+
const body = serializeEnvelope(envelope);
53+
54+
suppressTracing(() => {
55+
fetch(parsedUrl.href, {
56+
method: 'POST',
57+
body,
58+
headers: {
59+
'Content-Type': 'application/x-sentry-envelope',
60+
},
61+
}).then(
62+
res => {
63+
// Consume the response body to satisfy Cloudflare Workers' requirement
64+
// that all fetch response bodies are read or cancelled.
65+
res.text().catch(() => {
66+
// no-op
67+
});
68+
69+
if (res.status >= 200 && res.status < 400) {
70+
failCount = 0;
71+
}
72+
},
73+
() => {
74+
failCount++;
75+
DEBUG_BUILD && debug.warn('[Spotlight] Failed to send envelope to Spotlight Sidecar');
76+
},
77+
);
78+
});
79+
});
80+
}
81+
82+
function parseSidecarUrl(url: string): URL | undefined {
83+
try {
84+
return new URL(url);
85+
} catch {
86+
DEBUG_BUILD && debug.warn(`[Spotlight] Invalid sidecar URL: ${url}`);
87+
return undefined;
88+
}
89+
}

packages/cloudflare/src/options.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknow
5454
const tracesSampleRate =
5555
userOptions.tracesSampleRate ?? parseFloat(getEnvVar(env, 'SENTRY_TRACES_SAMPLE_RATE') ?? '');
5656

57+
// Spotlight precedence (mirrors node-core's getSpotlightConfig):
58+
// - false or explicit string from options: use as-is
59+
// - true: enable, but prefer a custom URL from the env var if set
60+
// - undefined: defer entirely to the env var (bool or URL)
61+
const spotlight = getSpotlightFromEnv(userOptions.spotlight, getEnvVar(env, 'SENTRY_SPOTLIGHT'));
62+
5763
return {
5864
release,
5965
...userOptions,
@@ -62,5 +68,33 @@ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknow
6268
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
6369
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),
6470
tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
71+
spotlight,
6572
};
6673
}
74+
75+
/**
76+
* Resolve the spotlight option from a user-supplied value and an env binding string.
77+
* Mirrors node-core's `getSpotlightConfig` precedence:
78+
* - `false` or explicit string from options → use as-is
79+
* - `true` → enable, but prefer a custom URL from the env var if set
80+
* - `undefined` → defer entirely to the env var (bool or URL)
81+
*/
82+
function getSpotlightFromEnv(
83+
optionsSpotlight: boolean | string | undefined,
84+
envVar: string | undefined,
85+
): boolean | string | undefined {
86+
if (optionsSpotlight === false) {
87+
return false;
88+
}
89+
if (typeof optionsSpotlight === 'string') {
90+
return optionsSpotlight;
91+
}
92+
93+
// optionsSpotlight is true or undefined
94+
const envBool = envToBool(envVar, { strict: true });
95+
const envUrl = envBool === null && envVar ? envVar : undefined;
96+
97+
return optionsSpotlight === true
98+
? (envUrl ?? true) // true: use env URL if present, otherwise true
99+
: (envBool ?? envUrl); // undefined: use env var (bool or URL)
100+
}

packages/cloudflare/src/sdk.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { makeFlushLock } from './flush';
1818
import { httpServerIntegration } from './integrations/httpServer';
1919
import { fetchIntegration } from './integrations/fetch';
2020
import { honoIntegration } from './integrations/hono';
21+
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from './integrations/spotlight';
2122
import { setupOpenTelemetryTracer } from './opentelemetry/tracer';
2223
import { makeCloudflareTransport } from './transport';
2324
import { defaultStackParser } from './vendor/stacktrace';
@@ -91,6 +92,14 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined {
9192
flushLock,
9293
};
9394

95+
if (options.spotlight && !clientOptions.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) {
96+
clientOptions.integrations.push(
97+
spotlightIntegration({
98+
sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined,
99+
}),
100+
);
101+
}
102+
94103
/**
95104
* The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility
96105
* via a custom trace provider.

0 commit comments

Comments
 (0)