From e431b48a57ec799ca5dc3475e8a70a3ac05eed19 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 24 Dec 2025 00:14:43 +0000 Subject: [PATCH 1/3] feat: Add origin badge to display event source Co-authored-by: burak.kaya --- .../src/server/parser/processEnvelope.ts | 7 +++- .../telemetry/components/events/EventList.tsx | 2 + .../components/shared/OriginBadge.tsx | 42 +++++++++++++++++++ .../telemetry/components/traces/TraceItem.tsx | 2 + .../telemetry/store/slices/envelopesSlice.ts | 7 ++++ packages/spotlight/src/ui/telemetry/types.ts | 2 + 6 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 packages/spotlight/src/ui/telemetry/components/shared/OriginBadge.tsx diff --git a/packages/spotlight/src/server/parser/processEnvelope.ts b/packages/spotlight/src/server/parser/processEnvelope.ts index a83a9b22..1da5bdee 100644 --- a/packages/spotlight/src/server/parser/processEnvelope.ts +++ b/packages/spotlight/src/server/parser/processEnvelope.ts @@ -1,11 +1,12 @@ import type { Envelope, EnvelopeItem } from "@sentry/core"; import { type UUID, uuidv7obj } from "uuidv7"; import { RAW_TYPES } from "../constants.ts"; +import { type SourceType, inferEnvelopeSource } from "../formatters/human/utils.ts"; import { logger } from "../logger.ts"; import type { RawEventContext } from "./types.ts"; export type ParsedEnvelope = { - envelope: [Envelope[0] & { __spotlight_envelope_id: UUID }, Envelope[1]]; + envelope: [Envelope[0] & { __spotlight_envelope_id: UUID; __spotlight_inferred_source?: SourceType }, Envelope[1]]; rawEnvelope: RawEventContext; }; @@ -91,6 +92,10 @@ export function processEnvelope(rawEvent: RawEventContext, senderUserAgent?: str items.push([itemHeader, itemPayload] as EnvelopeItem); } + // Infer the envelope source (browser, server, or mobile) for UI display + const firstEvent = items.length > 0 ? items[0][1] : undefined; + envelopeHeader.__spotlight_inferred_source = inferEnvelopeSource(envelopeHeader, firstEvent); + return { envelope: [envelopeHeader, items] as ParsedEnvelope["envelope"], rawEnvelope: rawEvent, diff --git a/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx b/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx index 302a5d2d..08231ff9 100644 --- a/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx +++ b/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx @@ -1,4 +1,5 @@ import CardList from "@spotlight/ui/telemetry/components/shared/CardList"; +import { OriginBadge } from "@spotlight/ui/telemetry/components/shared/OriginBadge"; import TimeSince from "@spotlight/ui/telemetry/components/shared/TimeSince"; import { Link } from "react-router-dom"; import { useSentryEvents } from "../../data/useSentryEvents"; @@ -25,6 +26,7 @@ export default function EventList({ traceId }: { traceId?: string }) {
{truncateId(e.event_id)}
+
diff --git a/packages/spotlight/src/ui/telemetry/components/shared/OriginBadge.tsx b/packages/spotlight/src/ui/telemetry/components/shared/OriginBadge.tsx new file mode 100644 index 00000000..78e9df5a --- /dev/null +++ b/packages/spotlight/src/ui/telemetry/components/shared/OriginBadge.tsx @@ -0,0 +1,42 @@ +import { Badge } from "@spotlight/ui/ui/badge"; + +type SourceType = "browser" | "server" | "mobile"; + +type OriginBadgeProps = { + sourceType: SourceType | undefined; +}; + +const SOURCE_CONFIG: Record = { + browser: { + label: "Browser", + // Yellow matching terminal output (#FDB81B) + className: "bg-yellow-500/20 text-yellow-200 border-yellow-500/30", + title: "This event originated from a browser", + }, + server: { + label: "Server", + // Magenta matching terminal output (#FF45A8) + className: "bg-pink-500/20 text-pink-200 border-pink-500/30", + title: "This event originated from a server", + }, + mobile: { + label: "Mobile", + // Blue matching terminal output (#226DFC) + className: "bg-blue-500/20 text-blue-200 border-blue-500/30", + title: "This event originated from a mobile device", + }, +}; + +export function OriginBadge({ sourceType }: OriginBadgeProps) { + if (!sourceType) { + return null; + } + + const config = SOURCE_CONFIG[sourceType]; + + return ( + + {config.label} + + ); +} diff --git a/packages/spotlight/src/ui/telemetry/components/traces/TraceItem.tsx b/packages/spotlight/src/ui/telemetry/components/traces/TraceItem.tsx index be4d959c..e044b3fe 100644 --- a/packages/spotlight/src/ui/telemetry/components/traces/TraceItem.tsx +++ b/packages/spotlight/src/ui/telemetry/components/traces/TraceItem.tsx @@ -1,4 +1,5 @@ import { cn } from "@spotlight/ui/lib/cn"; +import { OriginBadge } from "@spotlight/ui/telemetry/components/shared/OriginBadge"; import TimeSince from "@spotlight/ui/telemetry/components/shared/TimeSince"; import { Badge } from "@spotlight/ui/ui/badge"; import { Link, useParams } from "react-router-dom"; @@ -89,6 +90,7 @@ export default function TraceItem({ trace, className }: TraceItemProps) {
{truncatedId}
+
diff --git a/packages/spotlight/src/ui/telemetry/store/slices/envelopesSlice.ts b/packages/spotlight/src/ui/telemetry/store/slices/envelopesSlice.ts index 8cad3998..c868febe 100644 --- a/packages/spotlight/src/ui/telemetry/store/slices/envelopesSlice.ts +++ b/packages/spotlight/src/ui/telemetry/store/slices/envelopesSlice.ts @@ -18,6 +18,9 @@ export const createEnvelopesSlice: StateCreator { const [header, items] = envelope; const lastSeen = new Date(header.sent_at as string).getTime(); + // Read the inferred source type from the envelope header (set by server) + const sourceType = (header as { __spotlight_inferred_source?: "browser" | "server" | "mobile" }) + .__spotlight_inferred_source; let sdk: Sdk; if (header.sdk?.name && header.sdk.version) { @@ -86,6 +89,10 @@ export const createEnvelopesSlice: StateCreator Date: Wed, 24 Dec 2025 00:30:29 +0000 Subject: [PATCH 2/3] Refactor: Improve envelope source inference logic Co-authored-by: burak.kaya --- .../src/server/formatters/human/utils.ts | 102 +++++++++++------- .../src/server/parser/processEnvelope.ts | 16 ++- 2 files changed, 79 insertions(+), 39 deletions(-) diff --git a/packages/spotlight/src/server/formatters/human/utils.ts b/packages/spotlight/src/server/formatters/human/utils.ts index 3e44070c..5757c924 100644 --- a/packages/spotlight/src/server/formatters/human/utils.ts +++ b/packages/spotlight/src/server/formatters/human/utils.ts @@ -48,16 +48,71 @@ function isBrowserUserAgent(userAgent: string): boolean { ); } +/** + * Check if a platform string indicates a server environment + */ +function isServerPlatform(platform: string | undefined): boolean { + return ( + platform === "node" || + platform === "python" || + platform === "ruby" || + platform === "php" || + platform === "java" || + platform === "go" || + platform === "rust" || + platform === "perl" || + platform === "elixir" || + platform === "csharp" || + platform === "dotnet" + ); +} + +/** + * Try to infer source from a single event's properties + * Returns the source if deterministically detected, null otherwise + */ +function inferSourceFromEvent(event: any): SourceType | null { + if (!event) return null; + + // Runtime tags check - deterministic browser + if (event.tags?.runtime === "browser") { + return "browser"; + } + + // Runtime context indicates server + if (event.contexts?.runtime?.name) { + return "server"; + } + + // server_name is a server-specific field + if (event.server_name) { + return "server"; + } + + // Platform check + if (isServerPlatform(event.platform)) { + return "server"; + } + + return null; +} + /** * Infer the source of an envelope as browser, mobile, or server using multiple signals * Priority order: - * 1. Sender User-Agent (from HTTP request header) - * 2. Platform & Runtime tags (from event payload) - * 3. SDK name (fallback) + * 1. SDK name (mobile detection) + * 2. Sender User-Agent (from HTTP request header) + * 3. Platform & Runtime tags (from event payloads - scans all events) + * 4. SDK name (browser/server fallback) * * Rules based on https://release-registry.services.sentry.io/sdks + * + * @param envelopeHeader The envelope header containing SDK info and spotlight extensions + * @param eventsOrEvent Single event or array of events to scan for source signals (scans until deterministic match) */ -export function inferEnvelopeSource(envelopeHeader: Envelope[0], event?: any): SourceType { +export function inferEnvelopeSource(envelopeHeader: Envelope[0], eventsOrEvent?: any | any[]): SourceType { + // Normalize to array for consistent handling + const events = eventsOrEvent ? (Array.isArray(eventsOrEvent) ? eventsOrEvent : [eventsOrEvent]) : []; const sdkName = envelopeHeader?.sdk?.name || ""; // 1. Mobile check (unchanged - already reliable from SDK name) @@ -87,40 +142,15 @@ export function inferEnvelopeSource(envelopeHeader: Envelope[0], event?: any): S // If we have a non-browser UA, we continue to further checks } - // 3. Runtime tags check - if (event?.tags?.runtime === "browser") { - return "browser"; - } - - // 4. Platform & server-specific signals - if (event?.contexts?.runtime?.name) { - // Runtime context (node, CPython, etc.) indicates server - return "server"; - } - - if (event?.server_name) { - // server_name is a server-specific field - return "server"; - } - - const platform = event?.platform; - if ( - platform === "node" || - platform === "python" || - platform === "ruby" || - platform === "php" || - platform === "java" || - platform === "go" || - platform === "rust" || - platform === "perl" || - platform === "elixir" || - platform === "csharp" || - platform === "dotnet" - ) { - return "server"; + // 3. Scan all events for deterministic source signals + for (const event of events) { + const source = inferSourceFromEvent(event); + if (source) { + return source; + } } - // 5. SDK name check (existing logic as fallback) + // 4. SDK name check (existing logic as fallback) // Browser: JavaScript frameworks/libraries (excluding server/native runtimes and meta-frameworks) if ( sdkName.startsWith("sentry.javascript.") && diff --git a/packages/spotlight/src/server/parser/processEnvelope.ts b/packages/spotlight/src/server/parser/processEnvelope.ts index 1da5bdee..1277a0d7 100644 --- a/packages/spotlight/src/server/parser/processEnvelope.ts +++ b/packages/spotlight/src/server/parser/processEnvelope.ts @@ -5,8 +5,17 @@ import { type SourceType, inferEnvelopeSource } from "../formatters/human/utils. import { logger } from "../logger.ts"; import type { RawEventContext } from "./types.ts"; +/** + * Spotlight-specific extensions added to envelope headers for internal tracking + */ +export type SpotlightEnvelopeExtensions = { + __spotlight_envelope_id: UUID; + __spotlight_sender_user_agent?: string; + __spotlight_inferred_source?: SourceType; +}; + export type ParsedEnvelope = { - envelope: [Envelope[0] & { __spotlight_envelope_id: UUID; __spotlight_inferred_source?: SourceType }, Envelope[1]]; + envelope: [Envelope[0] & SpotlightEnvelopeExtensions, Envelope[1]]; rawEnvelope: RawEventContext; }; @@ -93,8 +102,9 @@ export function processEnvelope(rawEvent: RawEventContext, senderUserAgent?: str } // Infer the envelope source (browser, server, or mobile) for UI display - const firstEvent = items.length > 0 ? items[0][1] : undefined; - envelopeHeader.__spotlight_inferred_source = inferEnvelopeSource(envelopeHeader, firstEvent); + // Scan all events to find a deterministic match + const eventPayloads = items.map(([, payload]) => payload); + envelopeHeader.__spotlight_inferred_source = inferEnvelopeSource(envelopeHeader, eventPayloads); return { envelope: [envelopeHeader, items] as ParsedEnvelope["envelope"], From 8e2187adaeff4b16aa0d3b4823b61d4a11d4dd24 Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 24 Dec 2025 10:56:40 +0100 Subject: [PATCH 3/3] account for orphan traces --- .../spotlight/src/ui/telemetry/store/utils/traceProcessor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/spotlight/src/ui/telemetry/store/utils/traceProcessor.ts b/packages/spotlight/src/ui/telemetry/store/utils/traceProcessor.ts index 5cdd3727..16a7a6e7 100644 --- a/packages/spotlight/src/ui/telemetry/store/utils/traceProcessor.ts +++ b/packages/spotlight/src/ui/telemetry/store/utils/traceProcessor.ts @@ -144,6 +144,8 @@ export function updateTraceMetadata(trace: Trace): void { `[Spotlight] Orphan trace detected (trace_id: ${trace.trace_id}). ` + `Using first transaction "${trace.transactions[0].transaction}" as fallback.`, ); + // use the first transcation for orphan traces + trace.rootTransaction = trace.transactions[0]; trace.rootTransactionName = trace.transactions[0].transaction || "(orphan transaction)"; } else { trace.rootTransactionName = "(missing root transaction)";