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: