diff --git a/demos/astro-playground/sentry.client.config.ts b/demos/astro-playground/sentry.client.config.ts
index f5fb77b30..1a32598cb 100644
--- a/demos/astro-playground/sentry.client.config.ts
+++ b/demos/astro-playground/sentry.client.config.ts
@@ -1,7 +1,21 @@
import * as Sentry from '@sentry/astro';
+import { getCurrentHub, type Event } from '@sentry/astro';
Sentry.init({
debug: true,
integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()],
tracesSampleRate: 1.0,
});
+
+if (process.env.NODE_ENV === 'development') {
+ import('@spotlightjs/astro').then(Spotlight => {
+ const client = getCurrentHub().getClient();
+ if (client && client.on) {
+ client.on('beforeSendEvent', (event: Event) => {
+ if (event.exception && event.exception.values && event.exception.values[0].mechanism?.handled === false) {
+ Spotlight.trigger('sentry:showError', { eventId: event.event_id, event });
+ }
+ });
+ }
+ });
+}
diff --git a/demos/astro-playground/src/layouts/Layout.astro b/demos/astro-playground/src/layouts/Layout.astro
index 7b552be19..121182ff6 100644
--- a/demos/astro-playground/src/layouts/Layout.astro
+++ b/demos/astro-playground/src/layouts/Layout.astro
@@ -1,6 +1,6 @@
---
interface Props {
- title: string;
+ title: string;
}
const { title } = Astro.props;
@@ -8,44 +8,39 @@ const { title } = Astro.props;
-
-
-
-
-
-
- {title}
-
-
-
-
+
+
+
+
+
+
+ {title}
+
+
+
+
diff --git a/demos/astro-playground/src/pages/index.astro b/demos/astro-playground/src/pages/index.astro
index fdb21667b..4121657d0 100644
--- a/demos/astro-playground/src/pages/index.astro
+++ b/demos/astro-playground/src/pages/index.astro
@@ -4,130 +4,102 @@ import Card from '../components/Card.astro';
---
-
-
- Welcome to Astro
-
- To get started, open the directory src/pages in your project.
- Code Challenge: Tweak the "Welcome to Astro" message above.
-
-
-
+
+
+ Welcome to Astro
+
+ To get started, open the directory src/pages in your project.
+ Code Challenge: Tweak the "Welcome to Astro" message above.
+
+
+
diff --git a/packages/overlay/src/App.tsx b/packages/overlay/src/App.tsx
index e9ead29ba..0c1cdde35 100644
--- a/packages/overlay/src/App.tsx
+++ b/packages/overlay/src/App.tsx
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import Debugger from './components/Debugger';
import Trigger from './components/Trigger';
import type { Integration, IntegrationData } from './integrations/integration';
@@ -13,7 +14,6 @@ type AppProps = Omit &
export default function App({
openOnInit = false,
showTriggerButton = true,
- defaultEventId,
integrations = [],
sidecarUrl,
anchor,
@@ -48,10 +48,17 @@ export default function App({
const spotlightEventTarget = getSpotlightEventTarget();
+ const navigate = useNavigate();
+
useEffect(() => {
- const onOpen = () => {
+ const onOpen = (
+ e: CustomEvent<{
+ path: string | undefined;
+ }>,
+ ) => {
log('Open');
setOpen(true);
+ if (e.detail.path) navigate(e.detail.path);
};
const onClose = () => {
@@ -59,14 +66,21 @@ export default function App({
setOpen(false);
};
- spotlightEventTarget.addEventListener('open', onOpen);
+ const onNavigate = (e: CustomEvent) => {
+ log('Navigate');
+ navigate(e.detail);
+ };
+
+ spotlightEventTarget.addEventListener('open', onOpen as EventListener);
spotlightEventTarget.addEventListener('close', onClose);
+ spotlightEventTarget.addEventListener('navigate', onNavigate as EventListener);
return () => {
- spotlightEventTarget.removeEventListener('open', onOpen);
+ spotlightEventTarget.removeEventListener('open', onOpen as EventListener);
spotlightEventTarget.removeEventListener('close', onClose);
+ spotlightEventTarget.removeEventListener('navigate', onNavigate as EventListener);
};
- }, [spotlightEventTarget]);
+ }, [spotlightEventTarget, navigate]);
useEffect(() => {
if (!isOpen) {
@@ -97,7 +111,6 @@ export default function App({
isOpen={isOpen}
setOpen={setOpen}
isOnline={isOnline}
- defaultEventId={defaultEventId}
integrations={integrations}
integrationData={integrationData}
setTriggerButtonCount={setTriggerButtonCount}
diff --git a/packages/overlay/src/components/Debugger.tsx b/packages/overlay/src/components/Debugger.tsx
index d4c7ff3d7..4c0a1cf54 100644
--- a/packages/overlay/src/components/Debugger.tsx
+++ b/packages/overlay/src/components/Debugger.tsx
@@ -15,7 +15,6 @@ export default function Debugger({
integrations: Integration[];
isOpen: boolean;
setOpen: (value: boolean) => void;
- defaultEventId?: string;
integrationData: IntegrationData;
isOnline: boolean;
setTriggerButtonCount: (count: NotificationCount) => void;
diff --git a/packages/overlay/src/components/Overview.tsx b/packages/overlay/src/components/Overview.tsx
index 3e2209770..c2ae8caf8 100644
--- a/packages/overlay/src/components/Overview.tsx
+++ b/packages/overlay/src/components/Overview.tsx
@@ -1,5 +1,5 @@
import { createElement, useEffect, useState } from 'react';
-import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { Route, Routes } from 'react-router-dom';
import { Integration, IntegrationData } from '~/integrations/integration';
import { NotificationCount } from '~/types';
import Tabs from './Tabs';
@@ -46,30 +46,26 @@ export default function Overview({
setTriggerButtonCount(notificationCountSum);
}, [notificationCountSum, setTriggerButtonCount]);
- const initialTab = tabs.length ? `/${tabs[0].id}` : '/no-tabs';
-
return (
<>
-
-
-
- Not Found - How'd you manage to get here?} key={'not-found'}>
- {tabs.map(tab => {
- const TabContent = tab.content && tab.content;
+
+
+ Not Found - How'd you manage to get here?} key={'not-found'}>
+ {tabs.map(tab => {
+ const TabContent = tab.content && tab.content;
- if (TabContent) {
- return (
-
- );
- }
- return null;
- })}
-
-
+ if (TabContent) {
+ return (
+
+ );
+ }
+ return null;
+ })}
+
>
);
}
diff --git a/packages/overlay/src/index.tsx b/packages/overlay/src/index.tsx
index afdf53f1f..dca651ea7 100644
--- a/packages/overlay/src/index.tsx
+++ b/packages/overlay/src/index.tsx
@@ -2,10 +2,11 @@ import ReactDOM from 'react-dom/client';
import fontStyles from '@fontsource/raleway/index.css?inline';
+import { MemoryRouter } from 'react-router-dom';
import App from './App.tsx';
import { DEFAULT_ANCHOR } from './components/Trigger.tsx';
import globalStyles from './index.css?inline';
-import { initIntegrations } from './integrations/integration.ts';
+import { SpotlightContext, initIntegrations } from './integrations/integration.ts';
import { default as sentry } from './integrations/sentry/index.ts';
import { getSpotlightEventTarget } from './lib/eventTarget.ts';
import { activateLogger, log } from './lib/logger.ts';
@@ -15,6 +16,8 @@ export { default as console } from './integrations/console/index.ts';
export { default as sentry } from './integrations/sentry/index.ts';
export { default as viteInspect } from './integrations/vite-inspect/index.ts';
+const DEFAULT_SIDECAR_URL = 'http://localhost:8969/stream';
+
function createStyleSheet(styles: string) {
const sheet = new CSSStyleSheet();
sheet.replaceSync(styles);
@@ -24,8 +27,12 @@ function createStyleSheet(styles: string) {
/**
* Open the Spotlight debugger Window
*/
-export async function openSpotlight() {
- getSpotlightEventTarget().dispatchEvent(new CustomEvent('open'));
+export async function openSpotlight(path?: string | undefined) {
+ getSpotlightEventTarget().dispatchEvent(
+ new CustomEvent('open', {
+ detail: { path },
+ }),
+ );
}
/**
@@ -60,12 +67,25 @@ export async function onSevereEvent(cb: (count: number) => void) {
});
}
-const DEFAULT_SIDECAR_URL = 'http://localhost:8969/stream';
+/**
+ * Trigger an event in Spotlight.
+ *
+ * This is primarily useful for handling an uncaught error/crash, and forcing the debugger
+ * to render vs a native error handler.
+ *
+ * e.g. trigger("sentry.showError", {eventId});
+ */
+export async function trigger(eventName: string, payload: unknown) {
+ getSpotlightEventTarget().dispatchEvent(
+ new CustomEvent(eventName, {
+ detail: payload,
+ }),
+ );
+}
export async function init({
openOnInit = false,
showTriggerButton = true,
- defaultEventId,
injectImmediately = false,
sidecarUrl = DEFAULT_SIDECAR_URL,
anchor = DEFAULT_ANCHOR,
@@ -88,7 +108,12 @@ export async function init({
// Sentry is enabled by default
const defaultInitegrations = [sentry({ sidecarUrl })];
- const initializedIntegrations = await initIntegrations(integrations ?? defaultInitegrations);
+ const context: SpotlightContext = {
+ open: openSpotlight,
+ close: closeSpotlight,
+ };
+
+ const [initializedIntegrations] = await initIntegrations(integrations ?? defaultInitegrations, context);
// build shadow dom container to contain styles
const docRoot = document.createElement('div');
@@ -110,16 +135,31 @@ export async function init({
});
}
+ const tabs = initializedIntegrations
+ .map(integration => {
+ if (integration.tabs) {
+ return integration.tabs({ processedEvents: [] }).map(tab => ({
+ ...tab,
+ processedEvents: [],
+ }));
+ }
+ return [];
+ })
+ .flat();
+
+ const initialTab = tabs.length ? `/${tabs[0].id}` : '/no-tabs';
+
ReactDOM.createRoot(appRoot).render(
//
- ,
+
+
+ ,
//
);
diff --git a/packages/overlay/src/integrations/integration.ts b/packages/overlay/src/integrations/integration.ts
index 642d5920c..3141c2937 100644
--- a/packages/overlay/src/integrations/integration.ts
+++ b/packages/overlay/src/integrations/integration.ts
@@ -1,6 +1,11 @@
import { type ComponentType } from 'react';
import { NotificationCount } from '~/types';
+export type SpotlightContext = {
+ open: (path: string | undefined) => void;
+ close: () => void;
+};
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Integration = {
/**
@@ -29,7 +34,7 @@ export type Integration = {
*
* Use this hook to setup any global state, instrument handlers, etc.
*/
- setup?: () => void | Promise;
+ setup?: (context: SpotlightContext) => void | Promise | TeardownFunction;
/**
* Hook called whenever spotlight forwards a new raw event to this integration.
@@ -104,25 +109,34 @@ export type RawEventContext = {
data: string;
};
+type TeardownFunction = () => void | Promise<() => void>;
+
// export type IntegrationParameter = Array>;
-export async function initIntegrations(integrations?: Integration[]): Promise {
+export async function initIntegrations(
+ integrations: Integration[] = [],
+ context: SpotlightContext,
+): Promise<[Integration[], TeardownFunction[]]> {
if (!integrations) {
- return [];
+ return [[], []];
}
const initializedIntegrations: Integration[] = [];
+ const teardownFns: TeardownFunction[] = [];
// iterate over integrations and call their hooks
for (const integration of integrations) {
if (Array.isArray(integration)) {
- initializedIntegrations.push(...(await initIntegrations(integration)));
+ const rv = await initIntegrations(integration, context);
+ initializedIntegrations.push(...rv[0]);
+ teardownFns.push(...rv[1]);
} else if (integration) {
if (integration.setup) {
- await integration.setup();
+ const fn = await integration.setup(context);
+ if (fn) teardownFns.push(fn);
}
initializedIntegrations.push(integration);
}
}
- return initializedIntegrations;
+ return [initializedIntegrations, teardownFns];
}
diff --git a/packages/overlay/src/integrations/sentry/data/sentryDataCache.ts b/packages/overlay/src/integrations/sentry/data/sentryDataCache.ts
index 5836ca219..41ad88518 100644
--- a/packages/overlay/src/integrations/sentry/data/sentryDataCache.ts
+++ b/packages/overlay/src/integrations/sentry/data/sentryDataCache.ts
@@ -24,6 +24,7 @@ function sdkToPlatform(name: string) {
class SentryDataCache {
protected events: SentryEvent[] = [];
+ protected eventIds: Set = new Set();
protected sdks: Sdk[] = [];
protected traces: Trace[] = [];
protected tracesById: { [id: string]: Trace } = {};
@@ -96,6 +97,8 @@ class SentryDataCache {
event.event_id = generate_uuidv4();
}
+ if (this.eventIds.has(event.event_id)) return;
+
if (isErrorEvent(event)) {
reverseStackTraces(event);
}
@@ -167,6 +170,7 @@ class SentryDataCache {
this.subscribers.forEach(([type, cb]) => type === 'trace' && cb(trace));
}
this.subscribers.forEach(([type, cb]) => type === 'event' && cb(event));
+ this.eventIds.add(event.event_id);
}
getEvents() {
diff --git a/packages/overlay/src/integrations/sentry/index.ts b/packages/overlay/src/integrations/sentry/index.ts
index 5347fde10..9519f54a8 100644
--- a/packages/overlay/src/integrations/sentry/index.ts
+++ b/packages/overlay/src/integrations/sentry/index.ts
@@ -1,7 +1,7 @@
import type { Envelope } from '@sentry/types';
-
+import { getSpotlightEventTarget } from '../../lib/eventTarget';
+import { log } from '../../lib/logger';
import type { Integration, RawEventContext } from '../integration';
-
import sentryDataCache from './data/sentryDataCache';
import { Spotlight } from './sentry-integration';
import ErrorsTab from './tabs/ErrorsTab';
@@ -19,8 +19,23 @@ export default function sentryIntegration(options?: SentryIntegrationOptions) {
name: 'sentry',
forwardedContentType: [HEADER],
- setup: () => {
+ setup: ({ open }) => {
addSpotlightIntegrationToSentry(options);
+
+ const spotlightEventTarget = getSpotlightEventTarget();
+
+ const onRenderError = (e: CustomEvent) => {
+ log('Sentry Event', e.detail.event_id);
+ if (e.detail.event) sentryDataCache.pushEvent(e.detail.event);
+ // TODO: handle async
+ open(`/errors/${e.detail.eventId}`);
+ };
+
+ spotlightEventTarget.addEventListener('sentry:showError', onRenderError as EventListener);
+
+ return () => {
+ spotlightEventTarget.removeEventListener('renderSentryEvent', onRenderError as EventListener);
+ };
},
processEvent: (event: RawEventContext) => processEnvelope(event),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 807d0d7cd..54952b697 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,4 +1,4 @@
-lockfileVersion: '6.0'
+lockfileVersion: '6.1'
settings:
autoInstallPeers: true
@@ -2746,7 +2746,7 @@ packages:
svelte: 4.2.7
tiny-glob: 0.2.9
undici: 5.26.5
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
transitivePeerDependencies:
- supports-color
@@ -2762,7 +2762,7 @@ packages:
'@sveltejs/vite-plugin-svelte': 2.5.2(svelte@4.2.7)(vite@4.5.0)
debug: 4.3.4
svelte: 4.2.7
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
transitivePeerDependencies:
- supports-color
@@ -2778,7 +2778,7 @@ packages:
'@sveltejs/vite-plugin-svelte': 2.5.2(svelte@5.0.0-next.4)(vite@4.5.0)
debug: 4.3.4
svelte: 5.0.0-next.4
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
transitivePeerDependencies:
- supports-color
dev: false
@@ -2798,7 +2798,7 @@ packages:
magic-string: 0.30.5
svelte: 4.2.7
svelte-hmr: 0.15.3(svelte@4.2.7)
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
vitefu: 0.2.5(vite@4.5.0)
transitivePeerDependencies:
- supports-color
@@ -2818,7 +2818,7 @@ packages:
magic-string: 0.30.5
svelte: 5.0.0-next.4
svelte-hmr: 0.15.3(svelte@5.0.0-next.4)
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
vitefu: 0.2.5(vite@4.5.0)
transitivePeerDependencies:
- supports-color
@@ -3181,7 +3181,6 @@ packages:
/@types/node@18.0.0:
resolution:
{ integrity: sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA== }
- dev: true
/@types/node@18.18.8:
resolution:
@@ -3578,7 +3577,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.23.2)
'@types/babel__core': 7.20.3
react-refresh: 0.14.0
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
transitivePeerDependencies:
- supports-color
@@ -3594,7 +3593,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.3)
'@types/babel__core': 7.20.5
react-refresh: 0.14.0
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
transitivePeerDependencies:
- supports-color
@@ -4079,7 +4078,7 @@ packages:
tsconfck: 3.0.0(typescript@5.0.4)
unist-util-visit: 4.1.2
vfile: 5.3.7
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
vitefu: 0.2.5(vite@4.5.0)
which-pm: 2.1.1
yargs-parser: 21.1.1
@@ -4157,7 +4156,7 @@ packages:
tsconfck: 3.0.0(typescript@5.2.2)
unist-util-visit: 4.1.2
vfile: 5.3.7
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
vitefu: 0.2.5(vite@4.5.0)
which-pm: 2.1.1
yargs-parser: 21.1.1
@@ -4235,7 +4234,7 @@ packages:
tsconfck: 3.0.0(typescript@5.3.2)
unist-util-visit: 4.1.2
vfile: 5.3.7
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
vitefu: 0.2.5(vite@4.5.0)
which-pm: 2.1.1
yargs-parser: 21.1.1
@@ -10976,7 +10975,7 @@ packages:
debug: 4.3.4
kolorist: 1.8.0
typescript: 5.3.2
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
vue-tsc: 1.8.22(typescript@5.3.2)
transitivePeerDependencies:
- '@types/node'
@@ -11003,7 +11002,7 @@ packages:
open: 9.1.0
picocolors: 1.0.0
sirv: 2.0.3
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
transitivePeerDependencies:
- rollup
- supports-color
@@ -11018,48 +11017,13 @@ packages:
'@rollup/pluginutils': 5.0.5
'@svgr/core': 7.0.0(typescript@5.3.2)
'@svgr/plugin-jsx': 7.0.0
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
transitivePeerDependencies:
- rollup
- supports-color
- typescript
dev: true
- /vite@4.5.0:
- resolution:
- { integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw== }
- engines: { node: ^14.18.0 || >=16.0.0 }
- hasBin: true
- peerDependencies:
- '@types/node': '>= 14'
- less: '*'
- lightningcss: ^1.21.0
- sass: '*'
- stylus: '*'
- sugarss: '*'
- terser: ^5.4.0
- peerDependenciesMeta:
- '@types/node':
- optional: true
- less:
- optional: true
- lightningcss:
- optional: true
- sass:
- optional: true
- stylus:
- optional: true
- sugarss:
- optional: true
- terser:
- optional: true
- dependencies:
- esbuild: 0.18.20
- postcss: 8.4.31
- rollup: 3.29.4
- optionalDependencies:
- fsevents: 2.3.3
-
/vite@4.5.0(@types/node@18.0.0):
resolution:
{ integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw== }
@@ -11095,7 +11059,6 @@ packages:
rollup: 3.29.4
optionalDependencies:
fsevents: 2.3.3
- dev: true
/vite@4.5.0(@types/node@18.18.8):
resolution:
@@ -11143,7 +11106,7 @@ packages:
vite:
optional: true
dependencies:
- vite: 4.5.0
+ vite: 4.5.0(@types/node@18.0.0)
/vitest@0.34.6:
resolution: