@@ -33,7 +33,7 @@ import { createRedisClient, RedisClient, RedisWithClusterOptions } from "~/redis
3333import { logger } from "~/services/logger.server" ;
3434import { singleton } from "~/utils/singleton" ;
3535import { DynamicFlushScheduler } from "./dynamicFlushScheduler.server" ;
36- import { TaskEventStore , TaskEventStoreTable } from "./taskEventStore.server" ;
36+ import { DetailedTraceEvent , TaskEventStore , TaskEventStoreTable } from "./taskEventStore.server" ;
3737import { startActiveSpan } from "./tracer.server" ;
3838import { startSpan } from "./tracing.server" ;
3939
@@ -146,6 +146,12 @@ export type PreparedEvent = Omit<QueriedEvent, "events" | "style" | "duration">
146146 style : TaskEventStyle ;
147147} ;
148148
149+ export type PreparedDetailedEvent = Omit < DetailedTraceEvent , "events" | "style" | "duration" > & {
150+ duration : number ;
151+ events : SpanEvents ;
152+ style : TaskEventStyle ;
153+ } ;
154+
149155export type RunPreparedEvent = PreparedEvent & {
150156 taskSlug ?: string ;
151157} ;
@@ -186,6 +192,36 @@ export type SpanSummary = {
186192
187193export type TraceSummary = { rootSpan : SpanSummary ; spans : Array < SpanSummary > } ;
188194
195+ export type SpanDetailedSummary = {
196+ id : string ;
197+ parentId : string | undefined ;
198+ message : string ;
199+ data : {
200+ runId : string ;
201+ taskSlug ?: string ;
202+ taskPath ?: string ;
203+ events : SpanEvents ;
204+ startTime : Date ;
205+ duration : number ;
206+ isError : boolean ;
207+ isPartial : boolean ;
208+ isCancelled : boolean ;
209+ level : NonNullable < CreatableEvent [ "level" ] > ;
210+ environmentType : CreatableEventEnvironmentType ;
211+ workerVersion ?: string ;
212+ queueName ?: string ;
213+ machinePreset ?: string ;
214+ properties ?: Attributes ;
215+ output ?: Attributes ;
216+ } ;
217+ children : Array < SpanDetailedSummary > ;
218+ } ;
219+
220+ export type TraceDetailedSummary = {
221+ traceId : string ;
222+ rootSpan : SpanDetailedSummary ;
223+ } ;
224+
189225export type UpdateEventOptions = {
190226 attributes : TraceAttributes ;
191227 endTime ?: Date ;
@@ -589,6 +625,121 @@ export class EventRepository {
589625 } ) ;
590626 }
591627
628+ public async getTraceDetailedSummary (
629+ storeTable : TaskEventStoreTable ,
630+ traceId : string ,
631+ startCreatedAt : Date ,
632+ endCreatedAt ?: Date ,
633+ options ?: { includeDebugLogs ?: boolean }
634+ ) : Promise < TraceDetailedSummary | undefined > {
635+ return await startActiveSpan ( "getTraceDetailedSummary" , async ( span ) => {
636+ const events = await this . taskEventStore . findDetailedTraceEvents (
637+ storeTable ,
638+ traceId ,
639+ startCreatedAt ,
640+ endCreatedAt ,
641+ { includeDebugLogs : options ?. includeDebugLogs }
642+ ) ;
643+
644+ let preparedEvents : Array < PreparedDetailedEvent > = [ ] ;
645+ let rootSpanId : string | undefined ;
646+ const eventsBySpanId = new Map < string , PreparedDetailedEvent > ( ) ;
647+
648+ for ( const event of events ) {
649+ preparedEvents . push ( prepareDetailedEvent ( event ) ) ;
650+
651+ if ( ! rootSpanId && ! event . parentId ) {
652+ rootSpanId = event . spanId ;
653+ }
654+ }
655+
656+ for ( const event of preparedEvents ) {
657+ const existingEvent = eventsBySpanId . get ( event . spanId ) ;
658+
659+ if ( ! existingEvent ) {
660+ eventsBySpanId . set ( event . spanId , event ) ;
661+ continue ;
662+ }
663+
664+ if ( event . isCancelled || ! event . isPartial ) {
665+ eventsBySpanId . set ( event . spanId , event ) ;
666+ }
667+ }
668+
669+ preparedEvents = Array . from ( eventsBySpanId . values ( ) ) ;
670+
671+ if ( ! rootSpanId ) {
672+ return ;
673+ }
674+
675+ // Build hierarchical structure
676+ const spanDetailedSummaryMap = new Map < string , SpanDetailedSummary > ( ) ;
677+
678+ // First pass: create all span detailed summaries
679+ for ( const event of preparedEvents ) {
680+ const ancestorCancelled = isAncestorCancelled ( eventsBySpanId , event . spanId ) ;
681+ const duration = calculateDurationIfAncestorIsCancelled (
682+ eventsBySpanId ,
683+ event . spanId ,
684+ event . duration
685+ ) ;
686+
687+ const output = event . output ? ( event . output as Attributes ) : undefined ;
688+ const properties = event . properties
689+ ? removePrivateProperties ( event . properties as Attributes )
690+ : { } ;
691+
692+ const spanDetailedSummary : SpanDetailedSummary = {
693+ id : event . spanId ,
694+ parentId : event . parentId ?? undefined ,
695+ message : event . message ,
696+ data : {
697+ runId : event . runId ,
698+ taskSlug : event . taskSlug ?? undefined ,
699+ taskPath : event . taskPath ?? undefined ,
700+ events : event . events ?. filter ( ( e ) => ! e . name . startsWith ( "trigger.dev" ) ) ,
701+ startTime : getDateFromNanoseconds ( event . startTime ) ,
702+ duration : nanosecondsToMilliseconds ( duration ) ,
703+ isError : event . isError ,
704+ isPartial : ancestorCancelled ? false : event . isPartial ,
705+ isCancelled : event . isCancelled === true ? true : event . isPartial && ancestorCancelled ,
706+ level : event . level ,
707+ environmentType : event . environmentType ,
708+ workerVersion : event . workerVersion ?? undefined ,
709+ queueName : event . queueName ?? undefined ,
710+ machinePreset : event . machinePreset ?? undefined ,
711+ properties,
712+ output,
713+ } ,
714+ children : [ ] ,
715+ } ;
716+
717+ spanDetailedSummaryMap . set ( event . spanId , spanDetailedSummary ) ;
718+ }
719+
720+ // Second pass: build parent-child relationships
721+ for ( const spanSummary of spanDetailedSummaryMap . values ( ) ) {
722+ if ( spanSummary . parentId ) {
723+ const parent = spanDetailedSummaryMap . get ( spanSummary . parentId ) ;
724+ if ( parent ) {
725+ parent . children . push ( spanSummary ) ;
726+ }
727+ }
728+ }
729+
730+ const rootSpan = spanDetailedSummaryMap . get ( rootSpanId ) ;
731+
732+ if ( ! rootSpan ) {
733+ return ;
734+ }
735+
736+ return {
737+ traceId,
738+ rootSpan,
739+ } ;
740+ } ) ;
741+ }
742+
592743 public async getRunEvents (
593744 storeTable : TaskEventStoreTable ,
594745 runId : string ,
@@ -1517,6 +1668,15 @@ function prepareEvent(event: QueriedEvent): PreparedEvent {
15171668 } ;
15181669}
15191670
1671+ function prepareDetailedEvent ( event : DetailedTraceEvent ) : PreparedDetailedEvent {
1672+ return {
1673+ ...event ,
1674+ duration : Number ( event . duration ) ,
1675+ events : parseEventsField ( event . events ) ,
1676+ style : parseStyleField ( event . style ) ,
1677+ } ;
1678+ }
1679+
15201680function parseEventsField ( events : Prisma . JsonValue ) : SpanEvents {
15211681 const unsafe = events
15221682 ? ( events as any [ ] ) . map ( ( e ) => ( {
@@ -1548,7 +1708,10 @@ function parseStyleField(style: Prisma.JsonValue): TaskEventStyle {
15481708 return { } ;
15491709}
15501710
1551- function isAncestorCancelled ( events : Map < string , PreparedEvent > , spanId : string ) {
1711+ function isAncestorCancelled (
1712+ events : Map < string , { isCancelled : boolean ; parentId : string | null } > ,
1713+ spanId : string
1714+ ) {
15521715 const event = events . get ( spanId ) ;
15531716
15541717 if ( ! event ) {
@@ -1567,7 +1730,16 @@ function isAncestorCancelled(events: Map<string, PreparedEvent>, spanId: string)
15671730}
15681731
15691732function calculateDurationIfAncestorIsCancelled (
1570- events : Map < string , PreparedEvent > ,
1733+ events : Map <
1734+ string ,
1735+ {
1736+ isCancelled : boolean ;
1737+ parentId : string | null ;
1738+ isPartial : boolean ;
1739+ startTime : bigint ;
1740+ events : SpanEvents ;
1741+ }
1742+ > ,
15711743 spanId : string ,
15721744 defaultDuration : number
15731745) {
@@ -1603,7 +1775,19 @@ function calculateDurationIfAncestorIsCancelled(
16031775 return defaultDuration ;
16041776}
16051777
1606- function findFirstCancelledAncestor ( events : Map < string , PreparedEvent > , spanId : string ) {
1778+ function findFirstCancelledAncestor (
1779+ events : Map <
1780+ string ,
1781+ {
1782+ isCancelled : boolean ;
1783+ parentId : string | null ;
1784+ isPartial : boolean ;
1785+ startTime : bigint ;
1786+ events : SpanEvents ;
1787+ }
1788+ > ,
1789+ spanId : string
1790+ ) {
16071791 const event = events . get ( spanId ) ;
16081792
16091793 if ( ! event ) {
@@ -1711,6 +1895,10 @@ export function getDateFromNanoseconds(nanoseconds: bigint) {
17111895 return new Date ( Number ( nanoseconds ) / 1_000_000 ) ;
17121896}
17131897
1898+ function nanosecondsToMilliseconds ( nanoseconds : bigint | number ) : number {
1899+ return Number ( nanoseconds ) / 1_000_000 ;
1900+ }
1901+
17141902function rehydrateJson ( json : Prisma . JsonValue ) : any {
17151903 if ( json === null ) {
17161904 return undefined ;
0 commit comments