11import { z } from "zod" ;
2- import {
3- type ClickHouse ,
4- type TimeGranularity ,
5- detectTimeGranularity ,
6- granularityToInterval ,
7- granularityToStepMs ,
8- } from "@internal/clickhouse" ;
2+ import { type ClickHouse , msToClickHouseInterval } from "@internal/clickhouse" ;
3+ import { TimeGranularity } from "~/utils/timeGranularity" ;
4+
5+ const errorGroupGranularity = new TimeGranularity ( [
6+ { max : "1h" , granularity : "1m" } ,
7+ { max : "1d" , granularity : "30m" } ,
8+ { max : "1w" , granularity : "8h" } ,
9+ { max : "31d" , granularity : "1d" } ,
10+ { max : "45d" , granularity : "1w" } ,
11+ { max : "Infinity" , granularity : "30d" } ,
12+ ] ) ;
913import { type PrismaClientOrTransaction } from "@trigger.dev/database" ;
10- import { type Direction } from "~/components/ListPagination" ;
1114import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server" ;
1215import { ServiceValidationError } from "~/v3/services/baseService.server" ;
1316import { BasePresenter } from "~/presenters/v3/basePresenter.server" ;
17+ import {
18+ NextRunListPresenter ,
19+ type NextRunList ,
20+ } from "~/presenters/v3/NextRunListPresenter.server" ;
1421
1522export type ErrorGroupOptions = {
1623 userId ?: string ;
1724 projectId : string ;
1825 fingerprint : string ;
19- // pagination
20- direction ?: Direction ;
21- cursor ?: string ;
22- pageSize ?: number ;
26+ runsPageSize ?: number ;
2327} ;
2428
2529export const ErrorGroupOptionsSchema = z . object ( {
2630 userId : z . string ( ) . optional ( ) ,
2731 projectId : z . string ( ) ,
2832 fingerprint : z . string ( ) ,
29- direction : z . enum ( [ "forward" , "backward" ] ) . optional ( ) ,
30- cursor : z . string ( ) . optional ( ) ,
31- pageSize : z . number ( ) . int ( ) . positive ( ) . max ( 1000 ) . optional ( ) ,
33+ runsPageSize : z . number ( ) . int ( ) . positive ( ) . max ( 1000 ) . optional ( ) ,
3234} ) ;
3335
34- const DEFAULT_PAGE_SIZE = 50 ;
36+ const DEFAULT_RUNS_PAGE_SIZE = 25 ;
3537
3638export type ErrorGroupDetail = Awaited < ReturnType < ErrorGroupPresenter [ "call" ] > > ;
37- export type ErrorInstance = ErrorGroupDetail [ "instances" ] [ 0 ] ;
38-
39- // Cursor for error instances pagination
40- type ErrorInstanceCursor = {
41- createdAt : string ;
42- runId : string ;
43- } ;
44-
45- const ErrorInstanceCursorSchema = z . object ( {
46- createdAt : z . string ( ) ,
47- runId : z . string ( ) ,
48- } ) ;
49-
50- function encodeCursor ( cursor : ErrorInstanceCursor ) : string {
51- return Buffer . from ( JSON . stringify ( cursor ) ) . toString ( "base64" ) ;
52- }
53-
54- function decodeCursor ( cursor : string ) : ErrorInstanceCursor | null {
55- try {
56- const decoded = Buffer . from ( cursor , "base64" ) . toString ( "utf-8" ) ;
57- const parsed = JSON . parse ( decoded ) ;
58- const validated = ErrorInstanceCursorSchema . safeParse ( parsed ) ;
59- if ( ! validated . success ) {
60- return null ;
61- }
62- return validated . data as ErrorInstanceCursor ;
63- } catch {
64- return null ;
65- }
66- }
6739
6840function parseClickHouseDateTime ( value : string ) : Date {
6941 const asNum = Number ( value ) ;
@@ -77,7 +49,6 @@ export type ErrorGroupSummary = {
7749 fingerprint : string ;
7850 errorType : string ;
7951 errorMessage : string ;
80- stackTrace ?: string ;
8152 taskIdentifier : string ;
8253 count : number ;
8354 firstSeen : Date ;
@@ -90,6 +61,7 @@ export type ErrorGroupActivity = ErrorGroupOccurrences["data"];
9061export class ErrorGroupPresenter extends BasePresenter {
9162 constructor (
9263 private readonly replica : PrismaClientOrTransaction ,
64+ private readonly logsClickhouse : ClickHouse ,
9365 private readonly clickhouse : ClickHouse
9466 ) {
9567 super ( undefined , replica ) ;
@@ -98,67 +70,32 @@ export class ErrorGroupPresenter extends BasePresenter {
9870 public async call (
9971 organizationId : string ,
10072 environmentId : string ,
101- { userId, projectId, fingerprint, cursor, pageSize = DEFAULT_PAGE_SIZE } : ErrorGroupOptions
73+ {
74+ userId,
75+ projectId,
76+ fingerprint,
77+ runsPageSize = DEFAULT_RUNS_PAGE_SIZE ,
78+ } : ErrorGroupOptions
10279 ) {
10380 const displayableEnvironment = await findDisplayableEnvironment ( environmentId , userId ) ;
10481
10582 if ( ! displayableEnvironment ) {
10683 throw new ServiceValidationError ( "No environment found" ) ;
10784 }
10885
109- // Run summary (aggregated) and instances queries in parallel
110- const [ summary , instancesResult ] = await Promise . all ( [
86+ const [ summary , runList ] = await Promise . all ( [
11187 this . getSummary ( organizationId , projectId , environmentId , fingerprint ) ,
112- this . getInstances ( organizationId , projectId , environmentId , fingerprint , cursor , pageSize ) ,
88+ this . getRunList ( organizationId , environmentId , {
89+ userId,
90+ projectId,
91+ fingerprint,
92+ pageSize : runsPageSize ,
93+ } ) ,
11394 ] ) ;
11495
115- // Get stack trace from the most recent instance
116- let stackTrace : string | undefined ;
117- if ( instancesResult . instances . length > 0 ) {
118- const firstInstance = instancesResult . instances [ 0 ] ;
119- try {
120- const errorData = JSON . parse ( firstInstance . error_text ) as Record < string , unknown > ;
121- stackTrace = ( errorData . stack || errorData . stacktrace ) as string | undefined ;
122- } catch {
123- // no stack trace available
124- }
125- }
126-
127- // Build error group combining aggregated summary with instance stack trace
128- let errorGroup : ErrorGroupSummary | undefined ;
129- if ( summary ) {
130- errorGroup = {
131- ...summary ,
132- stackTrace,
133- } ;
134- }
135-
136- // Transform instances
137- const transformedInstances = instancesResult . instances . map ( ( instance ) => {
138- let parsedError : any ;
139- try {
140- parsedError = JSON . parse ( instance . error_text ) ;
141- } catch {
142- parsedError = { message : instance . error_text } ;
143- }
144-
145- return {
146- runId : instance . run_id ,
147- friendlyId : instance . friendly_id ,
148- taskIdentifier : instance . task_identifier ,
149- createdAt : new Date ( parseInt ( instance . created_at ) * 1000 ) ,
150- status : instance . status ,
151- error : parsedError ,
152- traceId : instance . trace_id ,
153- taskVersion : instance . task_version ,
154- } ;
155- } ) ;
156-
15796 return {
158- errorGroup,
159- instances : transformedInstances ,
160- runFriendlyIds : transformedInstances . map ( ( i ) => i . friendlyId ) ,
161- pagination : instancesResult . pagination ,
97+ errorGroup : summary ,
98+ runList,
16299 } ;
163100 }
164101
@@ -174,14 +111,12 @@ export class ErrorGroupPresenter extends BasePresenter {
174111 from : Date ,
175112 to : Date
176113 ) : Promise < {
177- granularity : TimeGranularity ;
178114 data : Array < { date : Date ; count : number } > ;
179115 } > {
180- const granularity = detectTimeGranularity ( from , to ) ;
181- const intervalExpr = granularityToInterval ( granularity ) ;
182- const stepMs = granularityToStepMs ( granularity ) ;
116+ const granularityMs = errorGroupGranularity . getTimeGranularityMs ( from , to ) ;
117+ const intervalExpr = msToClickHouseInterval ( granularityMs ) ;
183118
184- const queryBuilder = this . clickhouse . errors . createOccurrencesQueryBuilder ( intervalExpr ) ;
119+ const queryBuilder = this . logsClickhouse . errors . createOccurrencesQueryBuilder ( intervalExpr ) ;
185120
186121 queryBuilder . where ( "organization_id = {organizationId: String}" , { organizationId } ) ;
187122 queryBuilder . where ( "project_id = {projectId: String}" , { projectId } ) ;
@@ -205,9 +140,9 @@ export class ErrorGroupPresenter extends BasePresenter {
205140
206141 // Build time buckets covering the full range
207142 const buckets : number [ ] = [ ] ;
208- const startEpoch = Math . floor ( from . getTime ( ) / stepMs ) * ( stepMs / 1000 ) ;
143+ const startEpoch = Math . floor ( from . getTime ( ) / granularityMs ) * ( granularityMs / 1000 ) ;
209144 const endEpoch = Math . ceil ( to . getTime ( ) / 1000 ) ;
210- for ( let epoch = startEpoch ; epoch <= endEpoch ; epoch += stepMs / 1000 ) {
145+ for ( let epoch = startEpoch ; epoch <= endEpoch ; epoch += granularityMs / 1000 ) {
211146 buckets . push ( epoch ) ;
212147 }
213148
@@ -217,7 +152,6 @@ export class ErrorGroupPresenter extends BasePresenter {
217152 }
218153
219154 return {
220- granularity,
221155 data : buckets . map ( ( epoch ) => ( {
222156 date : new Date ( epoch * 1000 ) ,
223157 count : byBucket . get ( epoch ) ?? 0 ,
@@ -230,8 +164,8 @@ export class ErrorGroupPresenter extends BasePresenter {
230164 projectId : string ,
231165 environmentId : string ,
232166 fingerprint : string
233- ) : Promise < Omit < ErrorGroupSummary , "stackTrace" > | undefined > {
234- const queryBuilder = this . clickhouse . errors . listQueryBuilder ( ) ;
167+ ) : Promise < ErrorGroupSummary | undefined > {
168+ const queryBuilder = this . logsClickhouse . errors . listQueryBuilder ( ) ;
235169
236170 queryBuilder . where ( "organization_id = {organizationId: String}" , { organizationId } ) ;
237171 queryBuilder . where ( "project_id = {projectId: String}" , { projectId } ) ;
@@ -263,63 +197,29 @@ export class ErrorGroupPresenter extends BasePresenter {
263197 } ;
264198 }
265199
266- private async getInstances (
200+ private async getRunList (
267201 organizationId : string ,
268- projectId : string ,
269202 environmentId : string ,
270- fingerprint : string ,
271- cursor : string | undefined ,
272- pageSize : number
273- ) {
274- const queryBuilder = this . clickhouse . errors . instancesQueryBuilder ( ) ;
275-
276- queryBuilder . where ( "organization_id = {organizationId: String}" , { organizationId } ) ;
277- queryBuilder . where ( "project_id = {projectId: String}" , { projectId } ) ;
278- queryBuilder . where ( "environment_id = {environmentId: String}" , { environmentId } ) ;
279- queryBuilder . where ( "error_fingerprint = {errorFingerprint: String}" , {
280- errorFingerprint : fingerprint ,
281- } ) ;
282- queryBuilder . where ( "_is_deleted = 0" ) ;
283-
284- const decodedCursor = cursor ? decodeCursor ( cursor ) : null ;
285- if ( decodedCursor ) {
286- queryBuilder . where (
287- `(created_at < {cursorCreatedAt: String} OR (created_at = {cursorCreatedAt: String} AND run_id < {cursorRunId: String}))` ,
288- {
289- cursorCreatedAt : decodedCursor . createdAt ,
290- cursorRunId : decodedCursor . runId ,
291- }
292- ) ;
293- }
294-
295- queryBuilder . orderBy ( "created_at DESC, run_id DESC" ) ;
296- queryBuilder . limit ( pageSize + 1 ) ;
297-
298- const [ queryError , records ] = await queryBuilder . execute ( ) ;
299-
300- if ( queryError ) {
301- throw queryError ;
203+ options : {
204+ userId ?: string ;
205+ projectId : string ;
206+ fingerprint : string ;
207+ pageSize : number ;
302208 }
209+ ) : Promise < NextRunList | undefined > {
210+ const runListPresenter = new NextRunListPresenter ( this . replica , this . clickhouse ) ;
211+
212+ const result = await runListPresenter . call ( organizationId , environmentId , {
213+ userId : options . userId ,
214+ projectId : options . projectId ,
215+ errorFingerprint : options . fingerprint ,
216+ pageSize : options . pageSize ,
217+ } ) ;
303218
304- const results = records || [ ] ;
305- const hasMore = results . length > pageSize ;
306- const instances = results . slice ( 0 , pageSize ) ;
307-
308- let nextCursor : string | undefined ;
309- if ( hasMore && instances . length > 0 ) {
310- const lastInstance = instances [ instances . length - 1 ] ;
311- nextCursor = encodeCursor ( {
312- createdAt : lastInstance . created_at ,
313- runId : lastInstance . run_id ,
314- } ) ;
219+ if ( result . runs . length === 0 ) {
220+ return undefined ;
315221 }
316222
317- return {
318- instances,
319- pagination : {
320- hasMore,
321- nextCursor,
322- } ,
323- } ;
223+ return result ;
324224 }
325225}
0 commit comments