diff --git a/apps/webapp/app/utils/taskEvent.ts b/apps/webapp/app/utils/taskEvent.ts new file mode 100644 index 000000000..fd0f85736 --- /dev/null +++ b/apps/webapp/app/utils/taskEvent.ts @@ -0,0 +1,210 @@ +import { Attributes } from "@opentelemetry/api"; +import { SpanEvents, TaskEventStyle, unflattenAttributes } from "@trigger.dev/core/v3"; +import { Prisma } from "@trigger.dev/database"; +import type { PreparedEvent, QueriedEvent, SpanSummary } from "~/v3/eventRepository.server"; + +export function prepareTrace(events: QueriedEvent[]) { + let preparedEvents: Array = []; + let rootSpanId: string | undefined; + const eventsBySpanId = new Map(); + + for (const event of events) { + preparedEvents.push(prepareEvent(event)); + + if (!rootSpanId && !event.parentId) { + rootSpanId = event.spanId; + } + } + + for (const event of preparedEvents) { + const existingEvent = eventsBySpanId.get(event.spanId); + + if (!existingEvent) { + eventsBySpanId.set(event.spanId, event); + continue; + } + + if (event.isCancelled || !event.isPartial) { + eventsBySpanId.set(event.spanId, event); + } + } + + preparedEvents = Array.from(eventsBySpanId.values()); + + const spansBySpanId = new Map(); + + const spans = preparedEvents.map((event) => { + const ancestorCancelled = isAncestorCancelled(eventsBySpanId, event.spanId); + const duration = calculateDurationIfAncestorIsCancelled( + eventsBySpanId, + event.spanId, + event.duration + ); + + const span = { + recordId: event.id, + id: event.spanId, + parentId: event.parentId ?? undefined, + runId: event.runId, + idempotencyKey: event.idempotencyKey, + data: { + message: event.message, + style: event.style, + duration, + isError: event.isError, + isPartial: ancestorCancelled ? false : event.isPartial, + isCancelled: event.isCancelled === true ? true : event.isPartial && ancestorCancelled, + startTime: getDateFromNanoseconds(event.startTime), + level: event.level, + events: event.events, + environmentType: event.environmentType, + }, + }; + + spansBySpanId.set(event.spanId, span); + + return span; + }); + + if (!rootSpanId) { + return; + } + + const rootSpan = spansBySpanId.get(rootSpanId); + + if (!rootSpan) { + return; + } + + return { + rootSpan, + spans, + }; +} + +export function prepareEvent(event: QueriedEvent): PreparedEvent { + return { + ...event, + duration: Number(event.duration), + events: parseEventsField(event.events), + style: parseStyleField(event.style), + }; +} + +function parseEventsField(events: Prisma.JsonValue): SpanEvents { + const unsafe = events + ? (events as any[]).map((e) => ({ + ...e, + properties: unflattenAttributes(e.properties as Attributes), + })) + : undefined; + + return unsafe as SpanEvents; +} + +function parseStyleField(style: Prisma.JsonValue): TaskEventStyle { + const unsafe = unflattenAttributes(style as Attributes); + + if (!unsafe) { + return {}; + } + + if (typeof unsafe === "object") { + return Object.assign( + { + icon: undefined, + variant: undefined, + }, + unsafe + ) as TaskEventStyle; + } + + return {}; +} + +export function isAncestorCancelled(events: Map, spanId: string) { + const event = events.get(spanId); + + if (!event) { + return false; + } + + if (event.isCancelled) { + return true; + } + + if (event.parentId) { + return isAncestorCancelled(events, event.parentId); + } + + return false; +} + +function calculateDurationIfAncestorIsCancelled( + events: Map, + spanId: string, + defaultDuration: number +) { + const event = events.get(spanId); + + if (!event) { + return defaultDuration; + } + + if (event.isCancelled) { + return defaultDuration; + } + + if (!event.isPartial) { + return defaultDuration; + } + + if (event.parentId) { + const cancelledAncestor = findFirstCancelledAncestor(events, event.parentId); + + if (cancelledAncestor) { + // We need to get the cancellation time from the cancellation span event + const cancellationEvent = cancelledAncestor.events.find( + (event) => event.name === "cancellation" + ); + + if (cancellationEvent) { + return calculateDurationFromStart(event.startTime, cancellationEvent.time); + } + } + } + + return defaultDuration; +} + +export function calculateDurationFromStart(startTime: bigint, endTime: Date = new Date()) { + const $endtime = typeof endTime === "string" ? new Date(endTime) : endTime; + + return Number(BigInt($endtime.getTime() * 1_000_000) - startTime); +} + +function findFirstCancelledAncestor(events: Map, spanId: string) { + const event = events.get(spanId); + + if (!event) { + return; + } + + if (event.isCancelled) { + return event; + } + + if (event.parentId) { + return findFirstCancelledAncestor(events, event.parentId); + } + + return; +} + +export function getDateFromNanoseconds(nanoseconds: bigint) { + return new Date(Number(nanoseconds) / 1_000_000); +} + +export function getNowInNanoseconds(): bigint { + return BigInt(new Date().getTime() * 1_000_000); +} diff --git a/apps/webapp/app/v3/eventRepository.server.ts b/apps/webapp/app/v3/eventRepository.server.ts index 989ba2b27..60c249b60 100644 --- a/apps/webapp/app/v3/eventRepository.server.ts +++ b/apps/webapp/app/v3/eventRepository.server.ts @@ -32,6 +32,14 @@ import { logger } from "~/services/logger.server"; import { singleton } from "~/utils/singleton"; import { DynamicFlushScheduler } from "./dynamicFlushScheduler.server"; import { startActiveSpan } from "./tracer.server"; +import { + calculateDurationFromStart, + getDateFromNanoseconds, + getNowInNanoseconds, + isAncestorCancelled, + prepareEvent, + prepareTrace, +} from "~/utils/taskEvent"; const MAX_FLUSH_DEPTH = 5; @@ -413,82 +421,7 @@ export class EventRepository { take: env.MAXIMUM_TRACE_SUMMARY_VIEW_COUNT, }); - let preparedEvents: Array = []; - let rootSpanId: string | undefined; - const eventsBySpanId = new Map(); - - for (const event of events) { - preparedEvents.push(prepareEvent(event)); - - if (!rootSpanId && !event.parentId) { - rootSpanId = event.spanId; - } - } - - for (const event of preparedEvents) { - const existingEvent = eventsBySpanId.get(event.spanId); - - if (!existingEvent) { - eventsBySpanId.set(event.spanId, event); - continue; - } - - if (event.isCancelled || !event.isPartial) { - eventsBySpanId.set(event.spanId, event); - } - } - - preparedEvents = Array.from(eventsBySpanId.values()); - - const spansBySpanId = new Map(); - - const spans = preparedEvents.map((event) => { - const ancestorCancelled = isAncestorCancelled(eventsBySpanId, event.spanId); - const duration = calculateDurationIfAncestorIsCancelled( - eventsBySpanId, - event.spanId, - event.duration - ); - - const span = { - recordId: event.id, - id: event.spanId, - parentId: event.parentId ?? undefined, - runId: event.runId, - idempotencyKey: event.idempotencyKey, - data: { - message: event.message, - style: event.style, - duration, - isError: event.isError, - isPartial: ancestorCancelled ? false : event.isPartial, - isCancelled: event.isCancelled === true ? true : event.isPartial && ancestorCancelled, - startTime: getDateFromNanoseconds(event.startTime), - level: event.level, - events: event.events, - environmentType: event.environmentType, - }, - }; - - spansBySpanId.set(event.spanId, span); - - return span; - }); - - if (!rootSpanId) { - return; - } - - const rootSpan = spansBySpanId.get(rootSpanId); - - if (!rootSpan) { - return; - } - - return { - rootSpan, - spans, - }; + return prepareTrace(events); }); } @@ -1240,119 +1173,6 @@ function parseTraceparent(traceparent?: string): { traceId: string; spanId: stri return { traceId, spanId }; } -function prepareEvent(event: QueriedEvent): PreparedEvent { - return { - ...event, - duration: Number(event.duration), - events: parseEventsField(event.events), - style: parseStyleField(event.style), - }; -} - -function parseEventsField(events: Prisma.JsonValue): SpanEvents { - const unsafe = events - ? (events as any[]).map((e) => ({ - ...e, - properties: unflattenAttributes(e.properties as Attributes), - })) - : undefined; - - return unsafe as SpanEvents; -} - -function parseStyleField(style: Prisma.JsonValue): TaskEventStyle { - const unsafe = unflattenAttributes(style as Attributes); - - if (!unsafe) { - return {}; - } - - if (typeof unsafe === "object") { - return Object.assign( - { - icon: undefined, - variant: undefined, - }, - unsafe - ) as TaskEventStyle; - } - - return {}; -} - -function isAncestorCancelled(events: Map, spanId: string) { - const event = events.get(spanId); - - if (!event) { - return false; - } - - if (event.isCancelled) { - return true; - } - - if (event.parentId) { - return isAncestorCancelled(events, event.parentId); - } - - return false; -} - -function calculateDurationIfAncestorIsCancelled( - events: Map, - spanId: string, - defaultDuration: number -) { - const event = events.get(spanId); - - if (!event) { - return defaultDuration; - } - - if (event.isCancelled) { - return defaultDuration; - } - - if (!event.isPartial) { - return defaultDuration; - } - - if (event.parentId) { - const cancelledAncestor = findFirstCancelledAncestor(events, event.parentId); - - if (cancelledAncestor) { - // We need to get the cancellation time from the cancellation span event - const cancellationEvent = cancelledAncestor.events.find( - (event) => event.name === "cancellation" - ); - - if (cancellationEvent) { - return calculateDurationFromStart(event.startTime, cancellationEvent.time); - } - } - } - - return defaultDuration; -} - -function findFirstCancelledAncestor(events: Map, spanId: string) { - const event = events.get(spanId); - - if (!event) { - return; - } - - if (event.isCancelled) { - return event; - } - - if (event.parentId) { - return findFirstCancelledAncestor(events, event.parentId); - } - - return; -} - // Prioritize spans with the same id, keeping the completed spans over partial spans // Completed spans are either !isPartial or isCancelled function removeDuplicateEvents(events: PreparedEvent[]) { @@ -1470,20 +1290,6 @@ function filteredAttributes(attributes: Attributes, prefix: string): Attributes return result; } -function calculateDurationFromStart(startTime: bigint, endTime: Date = new Date()) { - const $endtime = typeof endTime === "string" ? new Date(endTime) : endTime; - - return Number(BigInt($endtime.getTime() * 1_000_000) - startTime); -} - -function getNowInNanoseconds(): bigint { - return BigInt(new Date().getTime() * 1_000_000); -} - -export function getDateFromNanoseconds(nanoseconds: bigint) { - return new Date(Number(nanoseconds) / 1_000_000); -} - function rehydrateJson(json: Prisma.JsonValue): any { if (json === null) { return undefined;