Skip to content

Commit 9a78b22

Browse files
BYKcursoragentbetegon
authored
Event origin badges ui (#1221)
<!-- Tick these boxes if they're applicable to your PR. - Changesets are only required for PRs to Spotlight library packages (e.g. @spotlightjs/overlay). Not for the website/docs or demo app contributions. - Typo correction or small bugfix PRs don't require an issue. If you're making a bigger change, please open an issue first. --> Before opening this PR: - [ ] I added a [Changeset Entry](https://spotlightjs.com/contribute/changesets/) with `pnpm changeset:add` - [x] I referenced issues that this PR addresses This PR implements event origin badges in the Spotlight UI to differentiate between browser-side, server-side, and mobile events, addressing [#654](#654). Previously, all events from meta-frameworks like Next.js would show "JS" as the platform, making it difficult to quickly identify the true origin. **Changes:** 1. **Server-side (`processEnvelope.ts`):** Infers the event's source (`browser`, `server`, `mobile`) and attaches it to the envelope header. 2. **UI Store (`envelopesSlice.ts`, `types.ts`):** Reads the inferred source from the envelope header and attaches it to the event object in the UI store. 3. **UI Components (`OriginBadge.tsx`, `EventList.tsx`, `TraceItem.tsx`):** A new `OriginBadge` component displays the source type with distinct color coding, integrated into the event list and trace item views. --- <a href="https://cursor.com/background-agent?bcId=bc-ea56a316-f89e-4c9c-a4d6-d813bb5c74a1"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-cursor-light.svg"><img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg"></picture></a>&nbsp;<a href="https://cursor.com/agents?id=bc-ea56a316-f89e-4c9c-a4d6-d813bb5c74a1"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-web-light.svg"><img alt="Open in Web" src="https://cursor.com/open-in-web.svg"></picture></a> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: betegon <miguelbetegongarcia@gmail.com>
1 parent 171069a commit 9a78b22

File tree

8 files changed

+139
-37
lines changed

8 files changed

+139
-37
lines changed

packages/spotlight/src/server/formatters/human/utils.ts

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,71 @@ function isBrowserUserAgent(userAgent: string): boolean {
4848
);
4949
}
5050

51+
/**
52+
* Check if a platform string indicates a server environment
53+
*/
54+
function isServerPlatform(platform: string | undefined): boolean {
55+
return (
56+
platform === "node" ||
57+
platform === "python" ||
58+
platform === "ruby" ||
59+
platform === "php" ||
60+
platform === "java" ||
61+
platform === "go" ||
62+
platform === "rust" ||
63+
platform === "perl" ||
64+
platform === "elixir" ||
65+
platform === "csharp" ||
66+
platform === "dotnet"
67+
);
68+
}
69+
70+
/**
71+
* Try to infer source from a single event's properties
72+
* Returns the source if deterministically detected, null otherwise
73+
*/
74+
function inferSourceFromEvent(event: any): SourceType | null {
75+
if (!event) return null;
76+
77+
// Runtime tags check - deterministic browser
78+
if (event.tags?.runtime === "browser") {
79+
return "browser";
80+
}
81+
82+
// Runtime context indicates server
83+
if (event.contexts?.runtime?.name) {
84+
return "server";
85+
}
86+
87+
// server_name is a server-specific field
88+
if (event.server_name) {
89+
return "server";
90+
}
91+
92+
// Platform check
93+
if (isServerPlatform(event.platform)) {
94+
return "server";
95+
}
96+
97+
return null;
98+
}
99+
51100
/**
52101
* Infer the source of an envelope as browser, mobile, or server using multiple signals
53102
* Priority order:
54-
* 1. Sender User-Agent (from HTTP request header)
55-
* 2. Platform & Runtime tags (from event payload)
56-
* 3. SDK name (fallback)
103+
* 1. SDK name (mobile detection)
104+
* 2. Sender User-Agent (from HTTP request header)
105+
* 3. Platform & Runtime tags (from event payloads - scans all events)
106+
* 4. SDK name (browser/server fallback)
57107
*
58108
* Rules based on https://release-registry.services.sentry.io/sdks
109+
*
110+
* @param envelopeHeader The envelope header containing SDK info and spotlight extensions
111+
* @param eventsOrEvent Single event or array of events to scan for source signals (scans until deterministic match)
59112
*/
60-
export function inferEnvelopeSource(envelopeHeader: Envelope[0], event?: any): SourceType {
113+
export function inferEnvelopeSource(envelopeHeader: Envelope[0], eventsOrEvent?: any | any[]): SourceType {
114+
// Normalize to array for consistent handling
115+
const events = eventsOrEvent ? (Array.isArray(eventsOrEvent) ? eventsOrEvent : [eventsOrEvent]) : [];
61116
const sdkName = envelopeHeader?.sdk?.name || "";
62117

63118
// 1. Mobile check (unchanged - already reliable from SDK name)
@@ -87,40 +142,15 @@ export function inferEnvelopeSource(envelopeHeader: Envelope[0], event?: any): S
87142
// If we have a non-browser UA, we continue to further checks
88143
}
89144

90-
// 3. Runtime tags check
91-
if (event?.tags?.runtime === "browser") {
92-
return "browser";
93-
}
94-
95-
// 4. Platform & server-specific signals
96-
if (event?.contexts?.runtime?.name) {
97-
// Runtime context (node, CPython, etc.) indicates server
98-
return "server";
99-
}
100-
101-
if (event?.server_name) {
102-
// server_name is a server-specific field
103-
return "server";
104-
}
105-
106-
const platform = event?.platform;
107-
if (
108-
platform === "node" ||
109-
platform === "python" ||
110-
platform === "ruby" ||
111-
platform === "php" ||
112-
platform === "java" ||
113-
platform === "go" ||
114-
platform === "rust" ||
115-
platform === "perl" ||
116-
platform === "elixir" ||
117-
platform === "csharp" ||
118-
platform === "dotnet"
119-
) {
120-
return "server";
145+
// 3. Scan all events for deterministic source signals
146+
for (const event of events) {
147+
const source = inferSourceFromEvent(event);
148+
if (source) {
149+
return source;
150+
}
121151
}
122152

123-
// 5. SDK name check (existing logic as fallback)
153+
// 4. SDK name check (existing logic as fallback)
124154
// Browser: JavaScript frameworks/libraries (excluding server/native runtimes and meta-frameworks)
125155
if (
126156
sdkName.startsWith("sentry.javascript.") &&

packages/spotlight/src/server/parser/processEnvelope.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import type { Envelope, EnvelopeItem } from "@sentry/core";
22
import { type UUID, uuidv7obj } from "uuidv7";
33
import { RAW_TYPES } from "../constants.ts";
4+
import { type SourceType, inferEnvelopeSource } from "../formatters/human/utils.ts";
45
import { logger } from "../logger.ts";
56
import type { RawEventContext } from "./types.ts";
67

8+
/**
9+
* Spotlight-specific extensions added to envelope headers for internal tracking
10+
*/
11+
export type SpotlightEnvelopeExtensions = {
12+
__spotlight_envelope_id: UUID;
13+
__spotlight_sender_user_agent?: string;
14+
__spotlight_inferred_source?: SourceType;
15+
};
16+
717
export type ParsedEnvelope = {
8-
envelope: [Envelope[0] & { __spotlight_envelope_id: UUID }, Envelope[1]];
18+
envelope: [Envelope[0] & SpotlightEnvelopeExtensions, Envelope[1]];
919
rawEnvelope: RawEventContext;
1020
};
1121

@@ -91,6 +101,11 @@ export function processEnvelope(rawEvent: RawEventContext, senderUserAgent?: str
91101
items.push([itemHeader, itemPayload] as EnvelopeItem);
92102
}
93103

104+
// Infer the envelope source (browser, server, or mobile) for UI display
105+
// Scan all events to find a deterministic match
106+
const eventPayloads = items.map(([, payload]) => payload);
107+
envelopeHeader.__spotlight_inferred_source = inferEnvelopeSource(envelopeHeader, eventPayloads);
108+
94109
return {
95110
envelope: [envelopeHeader, items] as ParsedEnvelope["envelope"],
96111
rawEnvelope: rawEvent,

packages/spotlight/src/ui/telemetry/components/events/EventList.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import CardList from "@spotlight/ui/telemetry/components/shared/CardList";
2+
import { OriginBadge } from "@spotlight/ui/telemetry/components/shared/OriginBadge";
23
import TimeSince from "@spotlight/ui/telemetry/components/shared/TimeSince";
34
import { Link } from "react-router-dom";
45
import { useSentryEvents } from "../../data/useSentryEvents";
@@ -25,6 +26,7 @@ export default function EventList({ traceId }: { traceId?: string }) {
2526
<div className="text-primary-300 flex w-48 flex-col truncate font-mono text-sm">
2627
<div className="flex items-center gap-x-2">
2728
<div>{truncateId(e.event_id)}</div>
29+
<OriginBadge sourceType={e.__sourceType} />
2830
</div>
2931
<span />
3032
<TimeSince date={e.timestamp} />
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Badge } from "@spotlight/ui/ui/badge";
2+
3+
type SourceType = "browser" | "server" | "mobile";
4+
5+
type OriginBadgeProps = {
6+
sourceType: SourceType | undefined;
7+
};
8+
9+
const SOURCE_CONFIG: Record<SourceType, { label: string; className: string; title: string }> = {
10+
browser: {
11+
label: "Browser",
12+
// Yellow matching terminal output (#FDB81B)
13+
className: "bg-yellow-500/20 text-yellow-200 border-yellow-500/30",
14+
title: "This event originated from a browser",
15+
},
16+
server: {
17+
label: "Server",
18+
// Magenta matching terminal output (#FF45A8)
19+
className: "bg-pink-500/20 text-pink-200 border-pink-500/30",
20+
title: "This event originated from a server",
21+
},
22+
mobile: {
23+
label: "Mobile",
24+
// Blue matching terminal output (#226DFC)
25+
className: "bg-blue-500/20 text-blue-200 border-blue-500/30",
26+
title: "This event originated from a mobile device",
27+
},
28+
};
29+
30+
export function OriginBadge({ sourceType }: OriginBadgeProps) {
31+
if (!sourceType) {
32+
return null;
33+
}
34+
35+
const config = SOURCE_CONFIG[sourceType];
36+
37+
return (
38+
<Badge title={config.title} className={config.className}>
39+
{config.label}
40+
</Badge>
41+
);
42+
}

packages/spotlight/src/ui/telemetry/components/traces/TraceItem.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { cn } from "@spotlight/ui/lib/cn";
2+
import { OriginBadge } from "@spotlight/ui/telemetry/components/shared/OriginBadge";
23
import TimeSince from "@spotlight/ui/telemetry/components/shared/TimeSince";
34
import { Badge } from "@spotlight/ui/ui/badge";
45
import { Link, useParams } from "react-router-dom";
@@ -89,6 +90,7 @@ export default function TraceItem({ trace, className }: TraceItemProps) {
8990
<div className="text-primary-300 flex w-48 flex-col truncate font-mono text-sm">
9091
<div className="flex items-center gap-x-2">
9192
<div>{truncatedId}</div>
93+
<OriginBadge sourceType={trace.rootTransaction?.__sourceType} />
9294
</div>
9395
<TimeSince date={trace.start_timestamp} />
9496
</div>

packages/spotlight/src/ui/telemetry/store/slices/envelopesSlice.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export const createEnvelopesSlice: StateCreator<SentryStore, [], [], EnvelopesSl
1818
pushEnvelope: (envelope: Envelope) => {
1919
const [header, items] = envelope;
2020
const lastSeen = new Date(header.sent_at as string).getTime();
21+
// Read the inferred source type from the envelope header (set by server)
22+
const sourceType = (header as { __spotlight_inferred_source?: "browser" | "server" | "mobile" })
23+
.__spotlight_inferred_source;
2124
let sdk: Sdk;
2225

2326
if (header.sdk?.name && header.sdk.version) {
@@ -86,6 +89,10 @@ export const createEnvelopesSlice: StateCreator<SentryStore, [], [], EnvelopesSl
8689
}
8790
item.contexts.trace ??= traceContext;
8891
}
92+
// Attach the inferred source type to the event for UI display
93+
if (sourceType) {
94+
(item as { __sourceType?: typeof sourceType }).__sourceType = sourceType;
95+
}
8996
const eventId =
9097
item.event_id ?? ("event_id" in itemHeader ? (itemHeader.event_id as string | undefined) : undefined);
9198
let attachmentsForEvent = eventId ? attachmentsByEventId.get(eventId) : undefined;

packages/spotlight/src/ui/telemetry/store/utils/traceProcessor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ export function updateTraceMetadata(trace: Trace): void {
144144
`[Spotlight] Orphan trace detected (trace_id: ${trace.trace_id}). ` +
145145
`Using first transaction "${trace.transactions[0].transaction}" as fallback.`,
146146
);
147+
// use the first transcation for orphan traces
148+
trace.rootTransaction = trace.transactions[0];
147149
trace.rootTransactionName = trace.transactions[0].transaction || "(orphan transaction)";
148150
} else {
149151
trace.rootTransactionName = "(missing root transaction)";

packages/spotlight/src/ui/telemetry/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ type CommonEventAttrs = {
7878
sdk?: Sdk;
7979
measurements?: Measurements;
8080
attachments?: EventAttachment[];
81+
// Inferred source type for distinguishing browser/server/mobile events
82+
__sourceType?: "browser" | "server" | "mobile";
8183
};
8284

8385
// Note: For some reason the `sentry/core` module doesn't have these additional properties

0 commit comments

Comments
 (0)