diff --git a/.changeset/hungry-dryers-hear.md b/.changeset/hungry-dryers-hear.md new file mode 100644 index 000000000..8e645afac --- /dev/null +++ b/.changeset/hungry-dryers-hear.md @@ -0,0 +1,6 @@ +--- +'@spotlightjs/overlay': patch +'@spotlightjs/astro': patch +--- + +feat(core): Add support for custom sidecar URL in client Sentry integration diff --git a/.changeset/lucky-fireants-whisper.md b/.changeset/lucky-fireants-whisper.md new file mode 100644 index 000000000..7ced6d170 --- /dev/null +++ b/.changeset/lucky-fireants-whisper.md @@ -0,0 +1,5 @@ +--- +'@spotlightjs/astro': patch +--- + +feat(astro): Add custom `sidecarUrl` option diff --git a/packages/astro/src/index.ts b/packages/astro/src/index.ts index 2016744ba..540ddaf24 100644 --- a/packages/astro/src/index.ts +++ b/packages/astro/src/index.ts @@ -1,21 +1,15 @@ import type { AstroIntegration } from 'astro'; -import { SPOTLIGHT_SERVER_SNIPPET, buildClientInitSnippet, type ClientInitOptions } from './snippets'; +import { buildClientInitSnippet, buildServerSnippet } from './snippets'; import path from 'path'; import url from 'url'; +import type { SpotlightAstroIntegrationOptions } from './types'; import { errorPageInjectionPlugin } from './vite/error-page'; const PKG_NAME = '@spotlightjs/astro'; -export type SpotlightOptions = - | { - __debugOptions?: ClientInitOptions; - debug: boolean; - } - | undefined; - -const createPlugin = (options?: SpotlightOptions): AstroIntegration => { +const createPlugin = (options?: SpotlightAstroIntegrationOptions): AstroIntegration => { const thisFilePath = url.fileURLToPath(import.meta.url); return { @@ -51,17 +45,22 @@ const createPlugin = (options?: SpotlightOptions): AstroIntegration => { } injectScript('page', buildClientInitSnippet({ importPath: PKG_NAME, showTriggerButton, ...options })); - injectScript('page-ssr', SPOTLIGHT_SERVER_SNIPPET); + injectScript('page-ssr', buildServerSnippet(options)); const importPath = path.dirname(url.fileURLToPath(import.meta.url)); const pluginPath = path.join(importPath, 'overlay/index.ts'); addDevOverlayPlugin(pluginPath); } else if (options?.__debugOptions) { - injectScript('page', buildClientInitSnippet(options.__debugOptions)); + injectScript('page', buildClientInitSnippet({ importPath: PKG_NAME, ...options })); } }, 'astro:server:start': async () => { + if (options?.sidecarUrl) { + // If users set a custom sidecar URL, we assume they started the sidecar manually outside of Astro. + // So we don't setup the default sidecar instance. + return; + } // Importing this dynamically because for some reason, the top level import // caused a dev server error because the sidecar code was bundled into the server const { setupSidecar } = await import('@spotlightjs/sidecar'); diff --git a/packages/astro/src/snippets.ts b/packages/astro/src/snippets.ts index f8fa1cbb2..fef21baeb 100644 --- a/packages/astro/src/snippets.ts +++ b/packages/astro/src/snippets.ts @@ -1,3 +1,5 @@ +import type { SpotlightAstroIntegrationOptions } from './types'; + type SupportedIntegrations = 'sentry' | 'console' | 'viteInspect'; export type ClientInitOptions = { @@ -5,15 +7,15 @@ export type ClientInitOptions = { showTriggerButton?: boolean; integrationNames?: SupportedIntegrations[]; injectImmediately?: boolean; -}; - -const DEFAULT_INTEGRATIONS = ['sentry']; +} & SpotlightAstroIntegrationOptions; const buildClientImport = (importPath: string) => `import * as Spotlight from '${importPath}';`; const buildClientInit = (options: ClientInitOptions) => { - const integrations = options.integrationNames || DEFAULT_INTEGRATIONS; - const integrationCalls = integrations.map(i => `Spotlight.${i}()`).join(', '); + const integrationCalls = options.integrationNames + ? options.integrationNames.map(i => `Spotlight.${i}()`).join(', ') + : `Spotlight.sentry({sidecarUrl: ${options.sidecarUrl ? `'${options.sidecarUrl}'` : undefined}})`; + return ` Spotlight.init({ integrations: [ @@ -22,6 +24,7 @@ Spotlight.init({ showTriggerButton: ${options.showTriggerButton === false ? 'false' : 'true'}, injectImmediately: ${options.injectImmediately === true ? 'true' : 'false'}, debug: ${options.debug === true ? 'true' : 'false'}, + ${options.sidecarUrl ? `sidecar: '${options.sidecarUrl}'` : ''} }); `; }; @@ -58,8 +61,11 @@ if (enableOverlay) { } `; -export const SPOTLIGHT_SERVER_SNIPPET = ` +export const buildServerSnippet: (options: SpotlightAstroIntegrationOptions) => string = options => ` import * as _SentrySDKForSpotlight from '@sentry/astro'; + _SentrySDKForSpotlight.getClient().setupIntegrations(true); -_SentrySDKForSpotlight.addIntegration(new _SentrySDKForSpotlight.Integrations.Spotlight()); +_SentrySDKForSpotlight.addIntegration(new _SentrySDKForSpotlight.Integrations.Spotlight({ + ${options?.sidecarUrl ? `sidecarUrl: '${options.sidecarUrl}'` : ''} +})); `; diff --git a/packages/astro/src/types.ts b/packages/astro/src/types.ts new file mode 100644 index 000000000..b718ff7ea --- /dev/null +++ b/packages/astro/src/types.ts @@ -0,0 +1,25 @@ +export type SpotlightAstroIntegrationOptions = + | { + /** + * Set this URL if you're running the Spotlight sidecar on a custom URL. + * Setting this URL will cause server- and client-side events to be forwarded to the sidcar running on the passed URL. + * + * IMPORTANT: This option assumes that you manually started the sidecar outside of Astro. Therefore, if it is set, + * the spotlight Astro integration will not start its own sidecar. + * + * @default 'http://localhost:8969/stream' + */ + sidecarUrl?: string; + + /** + * If enabled, Spotlight will log additional debug output to the console. + */ + debug?: boolean; + + /** + * Additional debug options. + * WARNING: These options are not part of the public API and may change at any time. + */ + __debugOptions?: Record; + } + | undefined; diff --git a/packages/astro/src/vite/error-page.ts b/packages/astro/src/vite/error-page.ts index 77abc57bc..6a4fc51cb 100644 --- a/packages/astro/src/vite/error-page.ts +++ b/packages/astro/src/vite/error-page.ts @@ -1,12 +1,13 @@ import { buildSpotlightErrorPageSnippet } from '../snippets'; import type { Plugin } from 'vite'; +import type { SpotlightAstroIntegrationOptions } from '../types'; type ErrorPagePluginOptions = { importPath: string; -}; +} & SpotlightAstroIntegrationOptions; -export const errorPageInjectionPlugin: (options: ErrorPagePluginOptions) => Plugin = ({ importPath }) => { +export const errorPageInjectionPlugin: (options: ErrorPagePluginOptions) => Plugin = options => { return { name: 'spotlight-vite-client-snippet-plugin', transform(code, id, opts = {}) { @@ -14,7 +15,7 @@ export const errorPageInjectionPlugin: (options: ErrorPagePluginOptions) => Plug if (!id.includes('vite/dist/client/client.mjs')) return; const initSnippet = buildSpotlightErrorPageSnippet({ - importPath, + ...options, // Astro's toolbar isn't available in the error page showTriggerButton: true, // don't wait for the window.load event to be fired because in the error page, diff --git a/packages/overlay/src/index.tsx b/packages/overlay/src/index.tsx index 463e301ef..e87af4c92 100644 --- a/packages/overlay/src/index.tsx +++ b/packages/overlay/src/index.tsx @@ -48,12 +48,12 @@ const DEFAULT_SIDECAR = 'http://localhost:8969/stream'; export async function init({ fullScreen = false, showTriggerButton = true, - integrations = [sentry()], defaultEventId, injectImmediately = false, sidecar = DEFAULT_SIDECAR, anchor = DEFAULT_ANCHOR, debug = false, + integrations, }: { integrations?: Integration[]; fullScreen?: boolean; @@ -77,7 +77,11 @@ export async function init({ if (debug) { activateLogger(); } - const initializedIntegrations = await initIntegrations(integrations); + + // Sentry is enabled by default + const defaultInitegrations = [sentry({ sidecarUrl: sidecar })]; + + const initializedIntegrations = await initIntegrations(integrations ?? defaultInitegrations); // build shadow dom container to contain styles const docRoot = document.createElement('div'); diff --git a/packages/overlay/src/integrations/sentry/index.ts b/packages/overlay/src/integrations/sentry/index.ts index 70fc47119..76543e9b8 100644 --- a/packages/overlay/src/integrations/sentry/index.ts +++ b/packages/overlay/src/integrations/sentry/index.ts @@ -10,13 +10,17 @@ import TracesTab from './tabs/TracesTab'; const HEADER = 'application/x-sentry-envelope'; -export default function sentryIntegration() { +type SentryIntegrationOptions = { + sidecarUrl?: string; +}; + +export default function sentryIntegration(options?: SentryIntegrationOptions) { return { name: 'sentry', forwardedContentType: [HEADER], setup: () => { - addSpotlightIntegrationToSentry(); + addSpotlightIntegrationToSentry(options); }, processEvent: (event: RawEventContext) => processEnvelope(event), @@ -89,12 +93,15 @@ function isErrorEnvelope(envelope: Envelope) { return envelope[1].some(([itemHeader]) => itemHeader.type === 'event'); } -function addSpotlightIntegrationToSentry() { +function addSpotlightIntegrationToSentry(options?: SentryIntegrationOptions) { // A very hacky way to hook into Sentry's SDK // but we love hacks const sentryHub = (window as WindowWithSentry).__SENTRY__?.hub; const sentryClient = sentryHub?.getClient(); if (sentryClient) { - sentryClient.addIntegration(new Spotlight()); + const spotlightIntegration = new Spotlight({ + sidecarUrl: options?.sidecarUrl, + }); + sentryClient.addIntegration(spotlightIntegration); } } diff --git a/packages/overlay/src/integrations/sentry/sentry-integration.ts b/packages/overlay/src/integrations/sentry/sentry-integration.ts index 297d70c33..85a4f63ed 100644 --- a/packages/overlay/src/integrations/sentry/sentry-integration.ts +++ b/packages/overlay/src/integrations/sentry/sentry-integration.ts @@ -1,9 +1,25 @@ import { Client, Envelope, Event, EventProcessor, Hub, Integration } from '@sentry/types'; import { serializeEnvelope } from '@sentry/utils'; +type SpotlightBrowserIntegationOptions = { + /** + * The URL of the Sidecar instance to connect and forward events to. + * If not set, Spotlight will try to connect to the Sidecar running on localhost:8969. + * + * @default "http://localhost:8969/stream" + */ + sidecarUrl?: string; +}; export class Spotlight implements Integration { public name: string = 'DevServerContextLines'; + private _sidecarUrl: string; + + public constructor(options?: SpotlightBrowserIntegationOptions) { + this._sidecarUrl = options?.sidecarUrl ?? 'http://localhost:8969/stream'; + console.log('Spotlight: Using Sidecar URL', this._sidecarUrl); + } + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor(async (event: Event) => { if (event.type || !event.exception || !event.exception.values) { @@ -32,18 +48,18 @@ export class Spotlight implements Integration { const client = getCurrentHub().getClient(); if (client) { - sendEnvelopesToSidecar(client); + sendEnvelopesToSidecar(client, this._sidecarUrl); } } } -function sendEnvelopesToSidecar(client: Client) { +function sendEnvelopesToSidecar(client: Client, sidecarUrl: string) { // Ensure, integrations are initialized even if no DSN was set client?.setupIntegrations(true); if (client.on) { client?.on('beforeEnvelope', (envelope: Envelope) => { - fetch('http://localhost:8969/stream', { + fetch(sidecarUrl, { method: 'POST', body: serializeEnvelope(envelope), headers: {