@@ -28,9 +28,7 @@ export class FunnelService {
2828
2929 /**
3030 * Returns the grouping strategy for the funnel.
31- * Note: windowFunnel is ALWAYS computed per session_id first to handle
32- * identity changes mid-session (anonymous → logged-in).
33- * The returned group is used for the final aggregation step.
31+ * Determines whether windowFunnel is computed per session_id or profile_id.
3432 */
3533 getFunnelGroup ( group ?: string ) : 'profile_id' | 'session_id' {
3634 return group === 'profile_id' ? 'profile_id' : 'session_id' ;
@@ -46,10 +44,11 @@ export class FunnelService {
4644 }
4745
4846 /**
49- * Builds the session-level funnel CTE.
50- * IMPORTANT: windowFunnel is ALWAYS computed per session_id first.
51- * This ensures identity changes mid-session (anonymous → logged-in) don't break the funnel.
52- * The profile_id is extracted from the last event in the session using argMax.
47+ * Builds the funnel CTE.
48+ * - When group === 'session_id': windowFunnel is computed per session_id.
49+ * profile_id is resolved via argMax to handle identity changes mid-session.
50+ * - When group === 'profile_id': windowFunnel is computed directly per profile_id.
51+ * This correctly handles cross-session funnel completions.
5352 */
5453 buildFunnelCte ( {
5554 projectId,
@@ -60,6 +59,7 @@ export class FunnelService {
6059 timezone,
6160 additionalSelects = [ ] ,
6261 additionalGroupBy = [ ] ,
62+ group = 'session_id' ,
6363 } : {
6464 projectId : string ;
6565 startDate : string ;
@@ -69,14 +69,18 @@ export class FunnelService {
6969 timezone : string ;
7070 additionalSelects ?: string [ ] ;
7171 additionalGroupBy ?: string [ ] ;
72+ group ?: 'session_id' | 'profile_id' ;
7273 } ) {
7374 const funnels = this . getFunnelConditions ( eventSeries ) ;
75+ const primaryKey = group === 'profile_id' ? 'profile_id' : 'session_id' ;
7476
7577 return clix ( this . client , timezone )
7678 . select ( [
77- 'session_id' ,
79+ primaryKey ,
7880 `windowFunnel(${ funnelWindowMilliseconds } , 'strict_increase')(toUInt64(toUnixTimestamp64Milli(created_at)), ${ funnels . join ( ', ' ) } ) AS level` ,
79- 'argMax(profile_id, created_at) AS profile_id' ,
81+ ...( group === 'session_id'
82+ ? [ 'argMax(profile_id, created_at) AS profile_id' ]
83+ : [ ] ) ,
8084 ...additionalSelects ,
8185 ] )
8286 . from ( TABLE_NAMES . events , false )
@@ -90,7 +94,7 @@ export class FunnelService {
9094 'IN' ,
9195 eventSeries . map ( ( e ) => e . name ) ,
9296 )
93- . groupBy ( [ 'session_id' , ...additionalGroupBy ] ) ;
97+ . groupBy ( [ primaryKey , ...additionalGroupBy ] ) ;
9498 }
9599
96100 buildSessionsCte ( {
@@ -248,6 +252,7 @@ export class FunnelService {
248252 timezone,
249253 additionalSelects : breakdownSelects ,
250254 additionalGroupBy : breakdownGroupBy ,
255+ group,
251256 } ) ;
252257
253258 if ( anyFilterOnProfile || anyBreakdownOnProfile ) {
@@ -276,25 +281,12 @@ export class FunnelService {
276281 const funnelQuery = clix ( this . client , timezone ) ;
277282 funnelQuery . with ( 'session_funnel' , funnelCte ) ;
278283
279- if ( group === 'profile_id' ) {
280- // For profile grouping: re-aggregate by profile_id, taking MAX level per profile.
281- // This ensures a user who completed the funnel across multiple sessions
282- // (or with identity change) is counted correctly.
283- const breakdownAggregates =
284- breakdowns . length > 0
285- ? `, ${ breakdowns . map ( ( _ , index ) => `any(b_${ index } ) AS b_${ index } ` ) . join ( ', ' ) } `
286- : '' ;
287- funnelQuery . with (
288- 'funnel' ,
289- `SELECT profile_id, max(level) AS level${ breakdownAggregates } FROM (SELECT * FROM session_funnel WHERE level != 0) GROUP BY profile_id` ,
290- ) ;
291- } else {
292- // For session grouping: filter out level = 0 inside the CTE
293- funnelQuery . with (
294- 'funnel' ,
295- 'SELECT * FROM session_funnel WHERE level != 0' ,
296- ) ;
297- }
284+ // windowFunnel is computed per the primary key (profile_id or session_id),
285+ // so we just filter out level=0 rows — no re-aggregation needed.
286+ funnelQuery . with (
287+ 'funnel' ,
288+ 'SELECT * FROM session_funnel WHERE level != 0' ,
289+ ) ;
298290
299291 funnelQuery
300292 . select < {
0 commit comments