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 a83a9b22..1277a0d7 100644 --- a/packages/spotlight/src/server/parser/processEnvelope.ts +++ b/packages/spotlight/src/server/parser/processEnvelope.ts @@ -1,11 +1,21 @@ 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"; +/** + * 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 }, Envelope[1]]; + envelope: [Envelope[0] & SpotlightEnvelopeExtensions, Envelope[1]]; rawEnvelope: RawEventContext; }; @@ -91,6 +101,11 @@ export function processEnvelope(rawEvent: RawEventContext, senderUserAgent?: str items.push([itemHeader, itemPayload] as EnvelopeItem); } + // Infer the envelope source (browser, server, or mobile) for UI display + // 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"], 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 }) {