Skip to content

Commit 69a5e8e

Browse files
BYKBurak Yigit Kaya
and
Burak Yigit Kaya
authored
feat(browser): Add spotlightBrowser integration (#13263)
Adds a browser-side integration for sending events and Sentry requests to Spotlight. The integration is not enabled by default but can be added by users if they want to explicitly send browser SDK events to spotlight. This is especially helpful if people use spotlight in the electron app or a standalone browser window instead of the overlay. --- Co-authored-by: Burak Yigit Kaya <[email protected]>
1 parent 2c24a33 commit 69a5e8e

File tree

8 files changed

+122
-29
lines changed

8 files changed

+122
-29
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ module.exports = [
193193
import: createImport('init'),
194194
ignore: ['next/router', 'next/constants'],
195195
gzip: true,
196-
limit: '38 KB',
196+
limit: '38.03 KB',
197197
},
198198
// SvelteKit SDK (ESM)
199199
{

.vscode/settings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@
3636
],
3737
"deno.enablePaths": ["packages/deno/test"],
3838
"editor.codeActionsOnSave": {
39-
"source.organizeImports.biome": "explicit",
39+
"source.organizeImports.biome": "explicit"
4040
},
4141
"editor.defaultFormatter": "biomejs.biome",
4242
"[typescript]": {
4343
"editor.defaultFormatter": "biomejs.biome"
44-
}
44+
},
45+
"cSpell.words": ["arrayify"]
4546
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { debugIntegration } from '@sentry/core';
2+
export { spotlightBrowserIntegration } from '../integrations/spotlight';
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { getNativeImplementation } from '@sentry-internal/browser-utils';
2+
import { defineIntegration } from '@sentry/core';
3+
import type { Client, Envelope, Event, IntegrationFn } from '@sentry/types';
4+
import { logger, serializeEnvelope } from '@sentry/utils';
5+
import type { WINDOW } from '../helpers';
6+
7+
import { DEBUG_BUILD } from '../debug-build';
8+
9+
export type SpotlightConnectionOptions = {
10+
/**
11+
* Set this if the Spotlight Sidecar is not running on localhost:8969
12+
* By default, the Url is set to http://localhost:8969/stream
13+
*/
14+
sidecarUrl?: string;
15+
};
16+
17+
export const INTEGRATION_NAME = 'SpotlightBrowser';
18+
19+
const _spotlightIntegration = ((options: Partial<SpotlightConnectionOptions> = {}) => {
20+
const sidecarUrl = options.sidecarUrl || 'http://localhost:8969/stream';
21+
22+
return {
23+
name: INTEGRATION_NAME,
24+
setup: () => {
25+
DEBUG_BUILD && logger.log('Using Sidecar URL', sidecarUrl);
26+
},
27+
// We don't want to send interaction transactions/root spans created from
28+
// clicks within Spotlight to Sentry. Neither do we want them to be sent to
29+
// spotlight.
30+
processEvent: event => (isSpotlightInteraction(event) ? null : event),
31+
afterAllSetup: (client: Client) => {
32+
setupSidecarForwarding(client, sidecarUrl);
33+
},
34+
};
35+
}) satisfies IntegrationFn;
36+
37+
function setupSidecarForwarding(client: Client, sidecarUrl: string): void {
38+
const makeFetch: typeof WINDOW.fetch | undefined = getNativeImplementation('fetch');
39+
let failCount = 0;
40+
41+
client.on('beforeEnvelope', (envelope: Envelope) => {
42+
if (failCount > 3) {
43+
logger.warn('[Spotlight] Disabled Sentry -> Spotlight integration due to too many failed requests:', failCount);
44+
return;
45+
}
46+
47+
makeFetch(sidecarUrl, {
48+
method: 'POST',
49+
body: serializeEnvelope(envelope),
50+
headers: {
51+
'Content-Type': 'application/x-sentry-envelope',
52+
},
53+
mode: 'cors',
54+
}).then(
55+
res => {
56+
if (res.status >= 200 && res.status < 400) {
57+
// Reset failed requests counter on success
58+
failCount = 0;
59+
}
60+
},
61+
err => {
62+
failCount++;
63+
logger.error(
64+
"Sentry SDK can't connect to Sidecar is it running? See: https://spotlightjs.com/sidecar/npx/",
65+
err,
66+
);
67+
},
68+
);
69+
});
70+
}
71+
72+
/**
73+
* Use this integration to send errors and transactions to Spotlight.
74+
*
75+
* Learn more about spotlight at https://spotlightjs.com
76+
*/
77+
export const spotlightBrowserIntegration = defineIntegration(_spotlightIntegration);
78+
79+
/**
80+
* Flags if the event is a transaction created from an interaction with the spotlight UI.
81+
*/
82+
export function isSpotlightInteraction(event: Event): boolean {
83+
return Boolean(
84+
event.type === 'transaction' &&
85+
event.spans &&
86+
event.contexts &&
87+
event.contexts.trace &&
88+
event.contexts.trace.op === 'ui.action.click' &&
89+
event.spans.some(({ description }) => description && description.includes('#sentry-spotlight')),
90+
);
91+
}

packages/core/src/baseclient.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,15 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
311311

312312
/** @inheritdoc */
313313
public init(): void {
314-
if (this._isEnabled()) {
314+
if (
315+
this._isEnabled() ||
316+
// Force integrations to be setup even if no DSN was set when we have
317+
// Spotlight enabled. This is particularly important for browser as we
318+
// don't support the `spotlight` option there and rely on the users
319+
// adding the `spotlightBrowserIntegration()` to their integrations which
320+
// wouldn't get initialized with the check below when there's no DSN set.
321+
this._options.integrations.some(({ name }) => name.startsWith('Spotlight'))
322+
) {
315323
this._setupIntegrations();
316324
}
317325
}

packages/core/src/integration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type IntegrationIndex = {
1919

2020
/**
2121
* Remove duplicates from the given array, preferring the last instance of any duplicate. Not guaranteed to
22-
* preseve the order of integrations in the array.
22+
* preserve the order of integrations in the array.
2323
*
2424
* @private
2525
*/

packages/node/src/integrations/spotlight.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type SpotlightConnectionOptions = {
1111
sidecarUrl?: string;
1212
};
1313

14-
const INTEGRATION_NAME = 'Spotlight';
14+
export const INTEGRATION_NAME = 'Spotlight';
1515

1616
const _spotlightIntegration = ((options: Partial<SpotlightConnectionOptions> = {}) => {
1717
const _options = {
@@ -66,6 +66,10 @@ function connectToSpotlight(client: Client, options: Required<SpotlightConnectio
6666
},
6767
},
6868
res => {
69+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) {
70+
// Reset failed requests counter on success
71+
failedRequests = 0;
72+
}
6973
res.on('data', () => {
7074
// Drain socket
7175
});

packages/node/src/sdk/index.ts

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
setOpenTelemetryContextAsyncContextStrategy,
1818
setupEventContextTrace,
1919
} from '@sentry/opentelemetry';
20-
import type { Client, Integration, Options } from '@sentry/types';
20+
import type { Integration, Options } from '@sentry/types';
2121
import {
2222
consoleSandbox,
2323
dropUndefinedKeys,
@@ -36,7 +36,7 @@ import { modulesIntegration } from '../integrations/modules';
3636
import { nativeNodeFetchIntegration } from '../integrations/node-fetch';
3737
import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexception';
3838
import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection';
39-
import { spotlightIntegration } from '../integrations/spotlight';
39+
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
4040
import { getAutoPerformanceIntegrations } from '../integrations/tracing';
4141
import { makeNodeTransport } from '../transports';
4242
import type { NodeClientOptions, NodeOptions } from '../types';
@@ -140,13 +140,19 @@ function _init(
140140
const scope = getCurrentScope();
141141
scope.update(options.initialScope);
142142

143+
if (options.spotlight && !options.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) {
144+
options.integrations.push(
145+
spotlightIntegration({
146+
sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined,
147+
}),
148+
);
149+
}
150+
143151
const client = new NodeClient(options);
144152
// The client is on the current scope, from where it generally is inherited
145153
getCurrentScope().setClient(client);
146154

147-
if (isEnabled(client)) {
148-
client.init();
149-
}
155+
client.init();
150156

151157
logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`);
152158

@@ -158,20 +164,6 @@ function _init(
158164

159165
updateScopeFromEnvVariables();
160166

161-
if (options.spotlight) {
162-
// force integrations to be setup even if no DSN was set
163-
// If they have already been added before, they will be ignored anyhow
164-
const integrations = client.getOptions().integrations;
165-
for (const integration of integrations) {
166-
client.addIntegration(integration);
167-
}
168-
client.addIntegration(
169-
spotlightIntegration({
170-
sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined,
171-
}),
172-
);
173-
}
174-
175167
// If users opt-out of this, they _have_ to set up OpenTelemetry themselves
176168
// There is no way to use this SDK without OpenTelemetry!
177169
if (!options.skipOpenTelemetrySetup) {
@@ -336,7 +328,3 @@ function startSessionTracking(): void {
336328
}
337329
});
338330
}
339-
340-
function isEnabled(client: Client): boolean {
341-
return client.getOptions().enabled !== false && client.getTransport() !== undefined;
342-
}

0 commit comments

Comments
 (0)