Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/hungry-dryers-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@spotlightjs/overlay': patch
'@spotlightjs/astro': patch
---

feat(core): Add support for custom sidecar URL in client Sentry integration
5 changes: 5 additions & 0 deletions .changeset/lucky-fireants-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@spotlightjs/astro': patch
---

feat(astro): Add custom `sidecarUrl` option
21 changes: 10 additions & 11 deletions packages/astro/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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');
Expand Down
20 changes: 13 additions & 7 deletions packages/astro/src/snippets.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type { SpotlightAstroIntegrationOptions } from './types';

type SupportedIntegrations = 'sentry' | 'console' | 'viteInspect';

export type ClientInitOptions = {
importPath: string;
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: [
Expand All @@ -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}'` : ''}
});
`;
};
Expand Down Expand Up @@ -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}'` : ''}
}));
`;
25 changes: 25 additions & 0 deletions packages/astro/src/types.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
}
| undefined;
7 changes: 4 additions & 3 deletions packages/astro/src/vite/error-page.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
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 = {}) {
if (opts.ssr) return;
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,
Expand Down
8 changes: 6 additions & 2 deletions packages/overlay/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
Expand Down
15 changes: 11 additions & 4 deletions packages/overlay/src/integrations/sentry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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);
}
}
22 changes: 19 additions & 3 deletions packages/overlay/src/integrations/sentry/sentry-integration.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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: {
Expand Down