Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 66 additions & 36 deletions packages/spotlight/src/server/formatters/human/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.") &&
Expand Down
17 changes: 16 additions & 1 deletion packages/spotlight/src/server/parser/processEnvelope.ts
Original file line number Diff line number Diff line change
@@ -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;
};

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,6 +26,7 @@ export default function EventList({ traceId }: { traceId?: string }) {
<div className="text-primary-300 flex w-48 flex-col truncate font-mono text-sm">
<div className="flex items-center gap-x-2">
<div>{truncateId(e.event_id)}</div>
<OriginBadge sourceType={e.__sourceType} />
</div>
<span />
<TimeSince date={e.timestamp} />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SourceType, { label: string; className: string; title: string }> = {
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 (
<Badge title={config.title} className={config.className}>
{config.label}
</Badge>
);
}
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -89,6 +90,7 @@ export default function TraceItem({ trace, className }: TraceItemProps) {
<div className="text-primary-300 flex w-48 flex-col truncate font-mono text-sm">
<div className="flex items-center gap-x-2">
<div>{truncatedId}</div>
<OriginBadge sourceType={trace.rootTransaction?.__sourceType} />
</div>
<TimeSince date={trace.start_timestamp} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const createEnvelopesSlice: StateCreator<SentryStore, [], [], EnvelopesSl
pushEnvelope: (envelope: Envelope) => {
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) {
Expand Down Expand Up @@ -86,6 +89,10 @@ export const createEnvelopesSlice: StateCreator<SentryStore, [], [], EnvelopesSl
}
item.contexts.trace ??= traceContext;
}
// Attach the inferred source type to the event for UI display
if (sourceType) {
(item as { __sourceType?: typeof sourceType }).__sourceType = sourceType;
}
const eventId =
item.event_id ?? ("event_id" in itemHeader ? (itemHeader.event_id as string | undefined) : undefined);
let attachmentsForEvent = eventId ? attachmentsByEventId.get(eventId) : undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
Expand Down
2 changes: 2 additions & 0 deletions packages/spotlight/src/ui/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ type CommonEventAttrs = {
sdk?: Sdk;
measurements?: Measurements;
attachments?: EventAttachment[];
// Inferred source type for distinguishing browser/server/mobile events
__sourceType?: "browser" | "server" | "mobile";
};

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