187c0476c3
## Summary The run trace page loader serialized every span's raw OTel events (with full properties) into the response, even though the tree UI only renders the derived `timelineEvents` and the span detail panel refetches what it needs. On event-heavy traces that inflated both the loader payload and the server-side heap copies built per request. This PR keeps raw span events server-side and pairs that with a few related trace-view improvements: - A new optional `TRACE_VIEW_EMERGENCY_SPAN_CAP` env var (unset by default) clamps the trace summary and detailed trace summary span limits on both event store paths, including the public run trace endpoint, so operators can bound trace query sizes in one place without retuning the per-store limits. - The TreeView virtualizer resolved every rendered row with a linear scan over the whole tree (and `getNodeProps` did the same via `findIndex`); rows now resolve through memoized id lookup maps, which matters once traces reach tens of thousands of spans. - The run stream SSE lookup now applies the same organization membership scoping as the rest of the run page presenters, for consistency. Behavior is unchanged by default: the trace tree renders from the same `timelineEvents` it always has, and the new cap only takes effect when set.
224 lines
5.4 KiB
TypeScript
224 lines
5.4 KiB
TypeScript
import { SpanEvent } from "@trigger.dev/core/v3";
|
|
import { millisecondsToNanoseconds } from "@trigger.dev/core/v3/utils/durations";
|
|
|
|
export type TimelineEventState = "complete" | "error" | "inprogress" | "delayed";
|
|
|
|
export type TimelineLineVariant = "light" | "normal";
|
|
|
|
export type TimelineEventVariant =
|
|
| "start-cap"
|
|
| "dot-hollow"
|
|
| "dot-solid"
|
|
| "start-cap-thick"
|
|
| "end-cap-thick"
|
|
| "end-cap";
|
|
|
|
export type TimelineSpanEvent = {
|
|
name: string;
|
|
offset: number;
|
|
timestamp: Date;
|
|
duration?: number;
|
|
helpText?: string;
|
|
markerVariant: TimelineEventVariant;
|
|
lineVariant: TimelineLineVariant;
|
|
};
|
|
|
|
export function createTimelineSpanEventsFromSpanEvents(
|
|
spanEvents: SpanEvent[],
|
|
isAdmin: boolean,
|
|
relativeStartTime?: number
|
|
): Array<TimelineSpanEvent> {
|
|
if (!spanEvents) {
|
|
return [];
|
|
}
|
|
|
|
const matchingSpanEvents = spanEvents.filter((spanEvent) =>
|
|
spanEvent.name.startsWith("trigger.dev/")
|
|
);
|
|
|
|
if (matchingSpanEvents.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
// Check if there's a fork event
|
|
const hasForkEvent = matchingSpanEvents.some(
|
|
(spanEvent) =>
|
|
"event" in spanEvent.properties &&
|
|
typeof spanEvent.properties.event === "string" &&
|
|
spanEvent.properties.event === "fork"
|
|
);
|
|
|
|
const sortedSpanEvents = [...matchingSpanEvents].sort((a, b) => {
|
|
if (a.time === b.time) {
|
|
return a.name.localeCompare(b.name);
|
|
}
|
|
|
|
const aTime = typeof a.time === "string" ? new Date(a.time) : a.time;
|
|
const bTime = typeof b.time === "string" ? new Date(b.time) : b.time;
|
|
|
|
return aTime.getTime() - bTime.getTime();
|
|
});
|
|
|
|
const visibleSpanEvents = sortedSpanEvents.filter((spanEvent) => {
|
|
const eventName =
|
|
"event" in spanEvent.properties && typeof spanEvent.properties.event === "string"
|
|
? spanEvent.properties.event
|
|
: spanEvent.name;
|
|
|
|
// If we're admin, everything is visible
|
|
if (isAdmin) {
|
|
return true;
|
|
}
|
|
|
|
// If there's no fork event, import events are also visible to non-admins
|
|
if (!hasForkEvent && eventName === "import") {
|
|
return true;
|
|
}
|
|
|
|
// Otherwise use normal admin-only logic
|
|
return !getAdminOnlyForEvent(eventName);
|
|
});
|
|
|
|
if (visibleSpanEvents.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const firstEventTime =
|
|
typeof visibleSpanEvents[0].time === "string"
|
|
? new Date(visibleSpanEvents[0].time)
|
|
: visibleSpanEvents[0].time;
|
|
|
|
const $relativeStartTime = relativeStartTime ?? firstEventTime.getTime();
|
|
|
|
const events = visibleSpanEvents.map((spanEvent, index) => {
|
|
const timestamp =
|
|
typeof spanEvent.time === "string" ? new Date(spanEvent.time) : spanEvent.time;
|
|
|
|
const offset = millisecondsToNanoseconds(timestamp.getTime() - $relativeStartTime);
|
|
|
|
const duration =
|
|
"duration" in spanEvent.properties && typeof spanEvent.properties.duration === "number"
|
|
? spanEvent.properties.duration
|
|
: undefined;
|
|
|
|
const name =
|
|
"event" in spanEvent.properties && typeof spanEvent.properties.event === "string"
|
|
? spanEvent.properties.event
|
|
: spanEvent.name;
|
|
|
|
let markerVariant: TimelineEventVariant = "dot-hollow";
|
|
|
|
if (index === 0) {
|
|
markerVariant = "start-cap";
|
|
}
|
|
|
|
return {
|
|
name: getFriendlyNameForEvent(name, spanEvent.properties),
|
|
offset,
|
|
timestamp,
|
|
duration,
|
|
helpText: getHelpTextForEvent(name),
|
|
markerVariant,
|
|
lineVariant: "light" as const,
|
|
};
|
|
});
|
|
|
|
// Now sort by offset, ascending
|
|
events.sort((a, b) => a.offset - b.offset);
|
|
|
|
return events;
|
|
}
|
|
|
|
function getFriendlyNameForEvent(event: string, properties?: Record<string, any>): string {
|
|
switch (event) {
|
|
case "dequeue": {
|
|
return "Dequeued";
|
|
}
|
|
case "fork": {
|
|
return "Launched";
|
|
}
|
|
case "create_attempt": {
|
|
return "Attempt created";
|
|
}
|
|
case "import": {
|
|
return "Importing task file";
|
|
}
|
|
case "lazy_payload": {
|
|
return "Lazy attempt initialized";
|
|
}
|
|
case "pod_scheduled": {
|
|
return "Pod scheduled";
|
|
}
|
|
default: {
|
|
return event;
|
|
}
|
|
}
|
|
}
|
|
|
|
function getAdminOnlyForEvent(event: string): boolean {
|
|
switch (event) {
|
|
case "dequeue": {
|
|
return false;
|
|
}
|
|
case "fork": {
|
|
return false;
|
|
}
|
|
case "create_attempt": {
|
|
return true;
|
|
}
|
|
case "import": {
|
|
return false;
|
|
}
|
|
case "lazy_payload": {
|
|
return true;
|
|
}
|
|
case "pod_scheduled": {
|
|
return true;
|
|
}
|
|
default: {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getHelpTextForEvent(event: string): string | undefined {
|
|
switch (event) {
|
|
case "dequeue": {
|
|
return "The run was dequeued from the queue";
|
|
}
|
|
case "fork": {
|
|
return "The process was created to run the task";
|
|
}
|
|
case "create_attempt": {
|
|
return "An attempt was created for the run";
|
|
}
|
|
case "import": {
|
|
return "A task file was imported";
|
|
}
|
|
case "lazy_payload": {
|
|
return "The payload was initialized lazily";
|
|
}
|
|
case "pod_scheduled": {
|
|
return "The Kubernetes pod was scheduled to run";
|
|
}
|
|
case "Triggered": {
|
|
return "The run was triggered";
|
|
}
|
|
case "Dequeued": {
|
|
return "The run was dequeued from the queue";
|
|
}
|
|
case "Started": {
|
|
return "The run began executing";
|
|
}
|
|
case "Finished": {
|
|
return "The run completed execution";
|
|
}
|
|
case "Expired": {
|
|
return "The run expired before it could be started";
|
|
}
|
|
default: {
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|