Skip to content

Commit

Permalink
ref(integrations): Refactor processEvent and tab content props (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lms24 committed Nov 15, 2023
1 parent c925e1a commit 6b40ed5
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/funny-adults-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@spotlightjs/core': patch
---

ref(integrations): Adjust input and return types of `processEvent`
5 changes: 5 additions & 0 deletions .changeset/rare-ladybugs-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@spotlightjs/core': patch
---

Removed `integrationData` prop in favour of `processedEvents` in tab component
6 changes: 3 additions & 3 deletions packages/core/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import Debugger from './components/Debugger';
import Trigger from './components/Trigger';
import type { Integration } from './integrations/integration';
import type { Integration, IntegrationData } from './integrations/integration';
import { connectToSidecar } from './sidecar';
import { TriggerButtonCount } from './types';

Expand All @@ -22,7 +22,7 @@ export default function App({
}) {
console.log('[Spotlight] App rerender');

const [integrationData, setIntegrationData] = useState<Record<string, Array<unknown>>>({});
const [integrationData, setIntegrationData] = useState<IntegrationData<unknown>>({});
const [isOnline, setOnline] = useState(false);
const [triggerButtonCount, setTriggerButtonCount] = useState<TriggerButtonCount>({ general: 0, severe: 0 });

Expand Down Expand Up @@ -51,7 +51,7 @@ export default function App({
console.log('[Spotlight] useeffect cleanup');
cleanupListeners();
};
}, []);
}, [integrations]);

const [isOpen, setOpen] = useState(fullScreen);

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/components/Debugger.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Integration } from '~/integrations/integration';
import { Integration, IntegrationData } from '~/integrations/integration';
import classNames from '~/lib/classNames';
import useKeyPress from '~/lib/useKeyPress';
import Overview from './Overview';
Expand All @@ -14,7 +14,7 @@ export default function Debugger({
isOpen: boolean;
setOpen: (value: boolean) => void;
defaultEventId?: string;
integrationData: Record<string, Array<unknown>>;
integrationData: IntegrationData<unknown>;
isOnline: boolean;
}) {
useKeyPress('Escape', () => {
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/components/Overview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { Integration } from '~/integrations/integration';
import { Integration, IntegrationData } from '~/integrations/integration';
import Tabs from './Tabs';

const DEFAULT_TAB = 'errors';
Expand All @@ -10,19 +10,21 @@ export default function Overview({
integrationData,
}: {
integrations: Integration[];
integrationData: Record<string, Array<unknown>>;
integrationData: IntegrationData<unknown>;
}) {
const [activeTab, setActiveTab] = useState(DEFAULT_TAB);

const tabs = integrations
.map(integration => {
if (integration.tabs) {
return integration.tabs({ integrationData }).map(tab => ({
const processedEvents = integrationData[integration.name]?.map(container => container.event) || [];
return integration.tabs({ processedEvents }).map(tab => ({
...tab,
active: activeTab === tab.id,
onSelect: () => {
setActiveTab(tab.id);
},
processedEvents: processedEvents,
}));
}
return [];
Expand All @@ -45,7 +47,7 @@ export default function Overview({
<Route
path={`/${tab.id}/*`}
key={tab.id}
element={<TabContent integrationData={integrationData} key={tab.id} />}
element={<TabContent processedEvents={tab.processedEvents} key={tab.id} />}
></Route>
);
})}
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/integrations/console/console-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { ConsoleMessage } from './types';

type Props = {
consoleMessages?: ConsoleMessage[];
integrationData: Record<string, Array<ConsoleMessage>>;
processedEvents?: ConsoleMessage[];
};
export default function ConsoleTab({ integrationData }: Props) {
const messages = (integrationData['application/x-spotlight-console'] || []) as ConsoleMessage[];
export default function ConsoleTab({ processedEvents }: Props) {
const messages = (processedEvents || []) as ConsoleMessage[];

return (
<div className="divide-y divide-indigo-500 bg-indigo-950 p-4">
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/integrations/console/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export default function consoleIntegration() {
return {
name: 'console',
forwardedContentType: [HEADER],
tabs: ({ integrationData }) => [
tabs: ({ processedEvents }) => [
{
id: 'console',
title: 'Browser Console Logs',
notificationCount: integrationData[HEADER]?.length,
notificationCount: processedEvents.length,
content: ConsoleTab,
},
],
Expand All @@ -29,7 +29,9 @@ export default function consoleIntegration() {
processEvent({ data }) {
const msgJson = JSON.parse(data) as ConsoleMessage;

return msgJson;
return {
event: msgJson,
};
},
} satisfies Integration;
}
Expand Down
43 changes: 30 additions & 13 deletions packages/core/src/integrations/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export type Integration<T = any> = {

/**
* A function returning an array of tabs to be displayed in the UI.
*
* @param context contains the processed events for the tabs. Use this information to
* e.g. update the notification count badge of the tab.
*/
tabs?: TabsCreationFunction<T>;

Expand All @@ -27,11 +30,15 @@ export type Integration<T = any> = {

/**
* Hook called whenever spotlight forwards a new raw event to this integration.
*
* Use this hook to process and convert the raw request payload (string) to a
* data structure that your integration works with in the UI.
* The returned object will be passed to your tabs.
*
* If you want to disregard the sent event, simply return `undefined`.
*
* The returned object will be passed to your tabs function.
*/
processEvent?: (eventContext: RawEventContext) => T | Promise<T>;
processEvent?: (eventContext: RawEventContext) => ProcessedEventContainer<T> | undefined;
};

export type IntegrationTab<T> = {
Expand All @@ -55,7 +62,7 @@ export type IntegrationTab<T> = {
* JSX content of the tab. Go crazy, this is all yours!
*/
content?: React.ComponentType<{
integrationData: IntegrationData<T>;
processedEvents: T[];
}>;

onSelect?: () => void;
Expand All @@ -64,10 +71,28 @@ export type IntegrationTab<T> = {
active?: boolean;
};

type IntegrationData<T> = Record<string, T[]>;
type ProcessedEventContainer<T> = {
/**
* The processed event data to be passed to your tabs.
*/
event: T;

/**
* A level indicating the impact or severity of the processed event. Set this to
* 'severe' if the event is critical and users should be aware of it (e.g. a thrown error).
*
* If this is set to 'severe', a red notification count badge will be displayed
* next to the spotlight trigger button in the UI.
*
* @default value is 'default'
*/
severity?: 'default' | 'severe';
};

export type IntegrationData<T> = Record<string, ProcessedEventContainer<T>[]>;

type TabsContext<T> = {
integrationData: IntegrationData<T>;
processedEvents: T[];
};

type TabsCreationFunction<T> = (context: TabsContext<T>) => IntegrationTab<T>[];
Expand All @@ -86,14 +111,6 @@ type RawEventContext = {
* Return the processed object or undefined if the event should be ignored.
*/
data: string;

/**
* Calling this function will tell spotlight that the processed event is a severe
* event that should be highlighted in the general UI.
*
* For instance, this will have an effect on the Spotlight trigger button's counter appearance.
*/
markEventSevere: () => void;
};

// export type IntegrationParameter = Array<Integration<unknown>>;
Expand Down
8 changes: 3 additions & 5 deletions packages/core/src/integrations/sentry/data/sentryDataCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ class SentryDataCache {
event_id?: string;
})[] = [],
) {
initial.forEach(e => this.pushEvent(e, () => {}));
initial.forEach(e => this.pushEvent(e));
}

pushEnvelope(envelope: Envelope, markSevere: () => void) {
pushEnvelope(envelope: Envelope) {
const [header, items] = envelope;
if (header.sdk && header.sdk.name && header.sdk.version) {
const existingSdk = this.sdks.find(s => s.name === header.sdk!.name && s.version === header.sdk!.version);
Expand All @@ -46,7 +46,7 @@ class SentryDataCache {
}
items.forEach(([itemHeader, itemData]) => {
if (itemHeader.type === 'event' || itemHeader.type === 'transaction') {
this.pushEvent(itemData as SentryEvent, markSevere);
this.pushEvent(itemData as SentryEvent);
}
});
}
Expand All @@ -55,7 +55,6 @@ class SentryDataCache {
event: SentryEvent & {
event_id?: string;
},
markSevere: () => void,
) {
if (!event.event_id) event.event_id = generate_uuidv4();

Expand Down Expand Up @@ -125,7 +124,6 @@ class SentryDataCache {
}
this.subscribers.forEach(([type, cb]) => type === 'trace' && cb(trace));
}
markSevere();
this.subscribers.forEach(([type, cb]) => type === 'event' && cb(event));
}

Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/integrations/sentry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import TracesTab from './tabs/TracesTab';
const HEADER = 'application/x-sentry-envelope';

export default function sentryIntegration() {
console.log('spotlight', sentryDataCache.getEvents());

return {
name: 'sentry',
forwardedContentType: [HEADER],
Expand All @@ -19,7 +21,7 @@ export default function sentryIntegration() {
hookIntoSentry();
},

processEvent({ data, markEventSevere }) {
processEvent({ data }) {
console.log('[spotlight] Received new envelope');
const [rawHeader, ...rawEntries] = data.split('\n');
const header = JSON.parse(rawHeader) as Envelope[0];
Expand All @@ -31,22 +33,25 @@ export default function sentryIntegration() {
}

const envelope = [header, items] as Envelope;
sentryDataCache.pushEnvelope(envelope, markEventSevere);
sentryDataCache.pushEnvelope(envelope);

return envelope;
return {
event: envelope,
severity: isErrorEnvelope(envelope) ? 'severe' : 'default',
};
},

tabs: ({ integrationData }) => [
tabs: () => [
{
id: 'errors',
title: 'Errors',
notificationCount: integrationData['application/x-sentry-envelope']?.length,
notificationCount: sentryDataCache.getEvents().filter(e => !e.type).length,
content: ErrorsTab,
},
{
id: 'traces',
title: 'Traces',
notificationCount: integrationData['application/x-sentry-envelope']?.length + 1,
notificationCount: sentryDataCache.getTraces().length,
content: TracesTab,
},
{
Expand All @@ -71,6 +76,10 @@ type WindowWithSentry = Window & {
};
};

function isErrorEnvelope(envelope: Envelope) {
return envelope[1].some(([itemHeader]) => itemHeader.type === 'event');
}

function hookIntoSentry() {
// A very hacky way to hook into Sentry's SDK
// but we love hacks
Expand Down
20 changes: 8 additions & 12 deletions packages/core/src/sidecar.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import { Integration } from './integrations/integration';
import { Integration, IntegrationData } from './integrations/integration';
import { TriggerButtonCount } from './types';

export function connectToSidecar(
sidecarUrl: string,
contentTypeToIntegrations: Map<string, Integration<unknown>[]>,
setIntegrationData: React.Dispatch<React.SetStateAction<Record<string, Array<unknown>>>>,
setIntegrationData: React.Dispatch<React.SetStateAction<IntegrationData<unknown>>>,
setOnline: React.Dispatch<React.SetStateAction<boolean>>,
setTriggerButtonCount: React.Dispatch<React.SetStateAction<TriggerButtonCount>>,
): () => void {
Expand All @@ -19,39 +19,35 @@ export function connectToSidecar(
console.log(`[spotlight] Received new ${contentType} event`);
integrations.forEach(integration => {
if (integration.processEvent) {
// TODO: This will not stay but I'll refactor it later with a better processEvent API.
let isSevere = false;
const markEventSevere = (severe: boolean = true) => {
isSevere = severe;
};
const processedEvent = integration.processEvent({
contentType,
data: event.data,
markEventSevere,
});
if (processedEvent) {
setIntegrationData(prev => {
const integrationName = integration.name;
return {
...prev,
[contentType]: [...(prev[contentType] || []), processedEvent],
[integrationName]: [...(prev[integrationName] || []), processedEvent],
};
});
setTriggerButtonCount(prev => {
const keyToUpdate = processedEvent.severity === 'severe' ? 'severe' : 'general';
return {
...prev,
[isSevere ? 'severe' : 'general']: prev[isSevere ? 'severe' : 'general'] + 1,
[keyToUpdate]: prev[keyToUpdate] + 1,
};
});
}
}
});
};

console.log('[spotlight] adding listener for', contentType, 'sum', contentTypeListeners.length);

// `contentType` could for example be "application/x-sentry-envelope"
contentTypeListeners.push([contentType, listener]);
source.addEventListener(contentType, listener);

console.log('[spotlight] added listener for', contentType, 'sum', contentTypeListeners.length);
}

source.addEventListener('open', () => {
Expand Down

0 comments on commit 6b40ed5

Please sign in to comment.