1
- import { and , asc , eq , inArray , sql } from "drizzle-orm" ;
1
+ import { and , asc , eq , inArray , not , sql } from "drizzle-orm" ;
2
+ import { partition } from "lodash" ;
2
3
import { NextRequest , NextResponse } from "next/server" ;
3
4
4
5
import { searchSpans } from "@/lib/clickhouse/spans" ;
5
6
import { SpanSearchType } from "@/lib/clickhouse/types" ;
6
7
import { TimeRange } from "@/lib/clickhouse/utils" ;
7
8
import { db } from "@/lib/db/drizzle" ;
8
- import { events , spans } from "@/lib/db/migrations/schema" ;
9
+ import { events , labelClasses , labels , spans } from "@/lib/db/migrations/schema" ;
10
+ import { FilterDef , filtersToSql } from "@/lib/db/modifiers" ;
11
+
9
12
export async function GET (
10
13
req : NextRequest ,
11
14
props : { params : Promise < { projectId : string ; traceId : string } > }
@@ -15,6 +18,63 @@ export async function GET(
15
18
const traceId = params . traceId ;
16
19
const searchQuery = req . nextUrl . searchParams . get ( "search" ) ;
17
20
const searchType = req . nextUrl . searchParams . getAll ( "searchIn" ) ;
21
+
22
+ const urlParamFilters = ( ( ) => {
23
+ try {
24
+ const rawFilters = req . nextUrl . searchParams . getAll ( "filter" ) . map ( ( f ) => JSON . parse ( f ) as FilterDef ) ;
25
+ return Array . isArray ( rawFilters ) ? rawFilters : [ ] ;
26
+ } catch {
27
+ return [ ] ;
28
+ }
29
+ } ) ( ) ;
30
+
31
+ const [ filters , statusFilters ] = partition ( urlParamFilters , ( f ) => f . column !== "status" ) ;
32
+ const [ otherFilters , tagsFilters ] = partition ( filters , ( f ) => f . column !== "tags" ) ;
33
+
34
+ const statusSqlFilters = statusFilters . map ( ( filter ) => {
35
+ if ( filter . value === "success" ) {
36
+ return filter . operator === "eq" ? sql `status IS NULL` : sql `status IS NOT NULL` ;
37
+ } else if ( filter . value === "error" ) {
38
+ return filter . operator === "eq" ? sql `status = 'error'` : sql `status != 'error' OR status IS NULL` ;
39
+ }
40
+ return sql `1=1` ;
41
+ } ) ;
42
+
43
+ const tagsSqlFilters = tagsFilters . map ( ( filter ) => {
44
+ const name = filter . value ;
45
+ const inArrayFilter = inArray (
46
+ spans . spanId ,
47
+ db
48
+ . select ( { span_id : spans . spanId } )
49
+ . from ( spans )
50
+ . innerJoin ( labels , eq ( spans . spanId , labels . spanId ) )
51
+ . innerJoin ( labelClasses , eq ( labels . classId , labelClasses . id ) )
52
+ . where ( and ( eq ( labelClasses . name , name ) ) )
53
+ ) ;
54
+ return filter . operator === "eq" ? inArrayFilter : not ( inArrayFilter ) ;
55
+ } ) ;
56
+
57
+ const processedFilters = otherFilters . map ( ( filter ) => {
58
+ if ( filter . column === "path" ) {
59
+ filter . column = "(attributes ->> 'lmnr.span.path')" ;
60
+ } else if ( filter . column === "tokens" ) {
61
+ filter . column = "(attributes ->> 'llm.usage.total_tokens')::int8" ;
62
+ } else if ( filter . column === "cost" ) {
63
+ filter . column = "(attributes ->> 'gen_ai.usage.cost')::float8" ;
64
+ } else if ( filter . column === "model" ) {
65
+ filter . column = "(attributes ->> 'gen_ai.request.model')" ;
66
+ }
67
+ return filter ;
68
+ } ) ;
69
+
70
+ const sqlFilters = filtersToSql (
71
+ processedFilters ,
72
+ [ new RegExp ( / ^ \( a t t r i b u t e s \s * - > > \s * ' [ a - z A - Z _ \. ] + ' \) (?: : : i n t 8 | : : f l o a t 8 ) ? $ / ) ] ,
73
+ {
74
+ latency : sql < number > `EXTRACT(EPOCH FROM (end_time - start_time))` ,
75
+ }
76
+ ) ;
77
+
18
78
let searchSpanIds : string [ ] = [ ] ;
19
79
if ( searchQuery ) {
20
80
const timeRange = { pastHours : "all" } as TimeRange ;
@@ -32,7 +92,7 @@ export async function GET(
32
92
const spanEventsQuery = db . $with ( "span_events" ) . as (
33
93
db
34
94
. select ( {
35
- spanId : events . spanId ,
95
+ eventSpanId : sql ` events.span_id` . as ( "eventSpanId" ) ,
36
96
projectId : events . projectId ,
37
97
events : sql `jsonb_agg(jsonb_build_object(
38
98
'id', events.id,
@@ -78,12 +138,15 @@ export async function GET(
78
138
. from ( spans )
79
139
. leftJoin (
80
140
spanEventsQuery ,
81
- and ( eq ( spans . spanId , spanEventsQuery . spanId ) , eq ( spans . projectId , spanEventsQuery . projectId ) )
141
+ and ( eq ( spans . spanId , spanEventsQuery . eventSpanId ) , eq ( spans . projectId , spanEventsQuery . projectId ) )
82
142
)
83
143
. where (
84
144
and (
85
145
eq ( spans . traceId , traceId ) ,
86
146
eq ( spans . projectId , projectId ) ,
147
+ ...sqlFilters ,
148
+ ...statusSqlFilters ,
149
+ ...tagsSqlFilters ,
87
150
...( searchQuery !== null ? [ inArray ( spans . spanId , searchSpanIds ) ] : [ ] )
88
151
)
89
152
)
@@ -94,7 +157,7 @@ export async function GET(
94
157
return NextResponse . json (
95
158
spanItems . map ( ( span ) => ( {
96
159
...span ,
97
- parentSpanId : searchSpanIds . length > 0 ? null : span . parentSpanId ,
160
+ parentSpanId : searchSpanIds . length > 0 || urlParamFilters . length > 0 ? null : span . parentSpanId ,
98
161
} ) )
99
162
) ;
100
163
}
0 commit comments