1+ import { trace } from "@opentelemetry/api" ;
12import { json } from "@remix-run/server-runtime" ;
3+ import type { RetrieveRunTracePageResponseBody } from "@trigger.dev/core/v3" ;
24import { BatchId } from "@trigger.dev/core/v3/isomorphic" ;
35import { z } from "zod" ;
46import { $replica } from "~/db.server" ;
7+ import { env } from "~/env.server" ;
58import { anyResource , createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server" ;
9+ import { clampToEmergencySpanCap } from "~/v3/eventRepository/emergencySpanCap.server" ;
610import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server" ;
711import { getTraceInsertedAtEnd } from "~/v3/eventRepository/traceInsertedAtBound" ;
12+ import { getRunTracePage } from "~/v3/eventRepository/runTracePage.server" ;
13+ import { FEATURE_FLAG } from "~/v3/featureFlags" ;
14+ import { makeFlag } from "~/v3/featureFlags.server" ;
15+ import { toPageSpan } from "~/v3/eventRepository/tracePage" ;
16+ import {
17+ DEFAULT_TRACE_PAGE_SIZE ,
18+ parseTracePageRequest ,
19+ type TracePageRequest ,
20+ } from "~/v3/eventRepository/tracePageRequest" ;
821import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server" ;
922import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server" ;
1023import { buildSyntheticTraceBody } from "~/v3/mollifier/syntheticApiResponses.server" ;
@@ -78,7 +91,22 @@ export const loader = createLoaderApiRoute(
7891 } ,
7992 } ,
8093 } ,
81- async ( { resource : resolved , authentication } ) => {
94+ async ( { resource : resolved , authentication, request } ) => {
95+ const searchParams = new URL ( request . url ) . searchParams ;
96+
97+ if (
98+ hasPageParams ( searchParams ) &&
99+ ( await isTracePagingEnabled ( authentication . environment . organization . featureFlags ) )
100+ ) {
101+ const pageRequest = parseTracePageRequest ( searchParams , DEFAULT_TRACE_PAGE_SIZE ) ;
102+ if ( pageRequest . isErr ( ) ) {
103+ return json ( { error : PAGE_REQUEST_ERRORS [ pageRequest . error ] } , { status : 400 } ) ;
104+ }
105+ if ( pageRequest . value ) {
106+ return tracePageResponse ( resolved , authentication . environment , pageRequest . value ) ;
107+ }
108+ }
109+
82110 if ( resolved . source === "buffer" ) {
83111 // Buffered runs have no events ingested yet — the drainer hasn't
84112 // materialised the PG row and the worker hasn't started executing.
@@ -104,6 +132,11 @@ export const loader = createLoaderApiRoute(
104132 { insertedAtEnd : getTraceInsertedAtEnd ( run ) }
105133 ) ;
106134
135+ trace . getActiveSpan ( ) ?. setAttributes ( {
136+ "run.depth" : run . depth ,
137+ "trace.truncated" : traceSummary ?. isTruncated ?? false ,
138+ } ) ;
139+
107140 if ( ! traceSummary ) {
108141 return json ( { error : "Trace not found" } , { status : 404 } ) ;
109142 }
@@ -116,3 +149,88 @@ export const loader = createLoaderApiRoute(
116149 ) ;
117150 }
118151) ;
152+
153+ function hasPageParams ( searchParams : URLSearchParams ) : boolean {
154+ return searchParams . has ( "page[size]" ) || searchParams . has ( "page[after]" ) ;
155+ }
156+
157+ // Off: page parameters are ignored and the request gets today's tree response.
158+ function isTracePagingEnabled ( orgFeatureFlags : unknown ) : Promise < boolean > {
159+ return makeFlag ( ) ( {
160+ key : FEATURE_FLAG . publicTracePagingEnabled ,
161+ defaultValue : false ,
162+ overrides : ( orgFeatureFlags as Record < string , unknown > ) ?? { } ,
163+ } ) ;
164+ }
165+
166+ const PAGE_REQUEST_ERRORS = {
167+ invalid_page_size : "page[size] must be a positive integer" ,
168+ invalid_cursor : "page[after] is not a valid cursor; pass back pagination.next unchanged" ,
169+ } as const ;
170+
171+ const CHILD_RUN_ERROR =
172+ "Paging is only supported for runs at the root of their trace. Request this run's trace without page parameters." ;
173+
174+ // The same synthetic root span the unpaged response returns for a run still in the trigger buffer.
175+ function bufferedRunPage (
176+ run : Parameters < typeof buildSyntheticTraceBody > [ 0 ]
177+ ) : RetrieveRunTracePageResponseBody {
178+ if ( ! run . spanId ) {
179+ return { data : [ ] , attemptFailures : [ ] , pagination : { } } ;
180+ }
181+
182+ const { rootSpan } = buildSyntheticTraceBody ( run ) . trace ;
183+ return { data : [ toPageSpan ( rootSpan ) ] , attemptFailures : [ ] , pagination : { } } ;
184+ }
185+
186+ async function tracePageResponse (
187+ resolved : ResolvedRun ,
188+ environment : { id : string ; organization : { id : string } } ,
189+ pageRequest : TracePageRequest
190+ ) {
191+ // Paging reads the whole trace, so the run's span must be the trace root (no parent run or span).
192+ if ( resolved . run . parentTaskRunId || resolved . run . parentSpanId ) {
193+ return json ( { error : CHILD_RUN_ERROR } , { status : 400 } ) ;
194+ }
195+
196+ if ( resolved . source === "buffer" ) {
197+ return json ( bufferedRunPage ( resolved . run ) , { status : 200 } ) ;
198+ }
199+
200+ const capped = env . TRACE_VIEW_EMERGENCY_SPAN_CAP !== undefined ;
201+ if ( capped && pageRequest . after ) {
202+ return json (
203+ { error : "Trace paging is temporarily limited to the first page" } ,
204+ { status : 503 , headers : { "Retry-After" : "60" , "x-should-retry" : "false" } }
205+ ) ;
206+ }
207+
208+ const run = resolved . run ;
209+ const page = await getRunTracePage ( {
210+ repository : await getEventRepositoryForStore ( run . taskEventStore , environment . organization . id ) ,
211+ storeTable : getTaskEventStoreTableForRun ( run ) ,
212+ environmentId : environment . id ,
213+ run,
214+ pageRequest : { ...pageRequest , limit : clampToEmergencySpanCap ( pageRequest . limit ) } ,
215+ // Like the dashboard, stop paging under the emergency cap instead of reading the whole trace.
216+ stopAfterPage : capped ,
217+ } ) ;
218+
219+ if ( page . isOk ( ) ) {
220+ return json ( page . value , { status : 200 } ) ;
221+ }
222+
223+ switch ( page . error ) {
224+ case "store_unavailable" :
225+ // The SDK retries 5xx; tell it not to, so a struggling store isn't hit harder.
226+ return json (
227+ { error : "The trace store is temporarily unavailable" } ,
228+ { status : 503 , headers : { "Retry-After" : "5" , "x-should-retry" : "false" } }
229+ ) ;
230+ case "paging_unsupported" :
231+ return json (
232+ { error : "Trace paging isn't supported by this deployment's trace store" } ,
233+ { status : 501 }
234+ ) ;
235+ }
236+ }
0 commit comments