Skip to content

Commit 0ca3f8d

Browse files
vachmaraizadoesdev
andauthored
fix: custom events on funnels and session (#167)
* fix: custom events funnels and sessions * fix: funnels UI --------- Co-authored-by: iza <[email protected]>
1 parent 472a061 commit 0ca3f8d

File tree

4 files changed

+139
-58
lines changed

4 files changed

+139
-58
lines changed

apps/api/src/query/builders/sessions.ts

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -136,29 +136,60 @@ export const SessionsBuilders: Record<string, SimpleQueryConfig> = {
136136
ORDER BY first_visit DESC
137137
LIMIT {limit:Int32} OFFSET {offset:Int32}
138138
),
139-
session_events AS (
139+
all_events AS (
140140
SELECT
141+
e.id,
141142
e.session_id,
143+
e.time,
144+
e.event_name,
145+
e.path,
146+
CASE
147+
WHEN e.event_name NOT IN ('screen_view', 'page_exit', 'web_vitals', 'link_out')
148+
AND e.properties IS NOT NULL
149+
AND e.properties != '{}'
150+
THEN CAST(e.properties AS String)
151+
ELSE NULL
152+
END as properties
153+
FROM analytics.events e
154+
INNER JOIN session_list sl ON e.session_id = sl.session_id
155+
WHERE e.client_id = {websiteId:String}
156+
157+
UNION ALL
158+
159+
SELECT
160+
ce.id,
161+
ce.session_id,
162+
ce.timestamp as time,
163+
ce.event_name,
164+
'' as path,
165+
CASE
166+
WHEN ce.properties IS NOT NULL
167+
AND ce.properties != '{}'
168+
THEN CAST(ce.properties AS String)
169+
ELSE NULL
170+
END as properties
171+
FROM analytics.custom_events ce
172+
INNER JOIN session_list sl ON ce.session_id = sl.session_id
173+
WHERE ce.client_id = {websiteId:String}
174+
),
175+
session_events AS (
176+
SELECT
177+
session_id,
142178
groupArray(
143179
tuple(
144-
e.id,
145-
e.time,
146-
e.event_name,
147-
e.path,
148-
CASE
149-
WHEN e.event_name NOT IN ('screen_view', 'page_exit', 'web_vitals', 'link_out')
150-
AND e.properties IS NOT NULL
151-
AND e.properties != '{}'
152-
THEN CAST(e.properties AS String)
153-
ELSE NULL
154-
END
180+
id,
181+
time,
182+
event_name,
183+
path,
184+
properties
155185
)
156186
) as events
157-
FROM analytics.events e
158-
INNER JOIN session_list sl ON e.session_id = sl.session_id
159-
WHERE e.client_id = {websiteId:String}
160-
${combinedWhereClause}
161-
GROUP BY e.session_id
187+
FROM (
188+
SELECT * FROM all_events
189+
ORDER BY time ASC
190+
)
191+
${combinedWhereClause}
192+
GROUP BY session_id
162193
)
163194
SELECT
164195
sl.session_id,
@@ -174,7 +205,7 @@ export const SessionsBuilders: Record<string, SimpleQueryConfig> = {
174205
COALESCE(se.events, []) as events
175206
FROM session_list sl
176207
LEFT JOIN session_events se ON sl.session_id = se.session_id
177-
${combinedWhereClause ? `WHERE ${combinedWhereClause.replace('AND ', '')}` : ''}
208+
${combinedWhereClause}
178209
ORDER BY sl.first_visit DESC
179210
`,
180211
params: {
@@ -210,4 +241,4 @@ export const SessionsBuilders: Record<string, SimpleQueryConfig> = {
210241
timeField: 'time',
211242
customizable: true,
212243
} satisfies SimpleQueryConfig,
213-
};
244+
};

apps/api/src/query/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,5 +288,5 @@ export function buildWhereClause(conditions?: string[]): string {
288288
const safeClauses = conditions.filter(
289289
(clause) => !UNSAFE_CLAUSE_REGEX.test(clause)
290290
);
291-
return `AND (${safeClauses.join(' AND ')})`;
291+
return `WHERE (${safeClauses.join(' AND ')})`;
292292
}

apps/dashboard/app/(main)/websites/[id]/funnels/_components/funnel-analytics.tsx

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,40 +56,32 @@ export function FunnelAnalytics({
5656
return referrer || null;
5757
}, [selectedReferrer, referrerAnalytics]);
5858

59-
// Use selected referrer data if available, otherwise use main analytics data
6059
const displayData = selectedReferrerData
6160
? {
6261
total_users_entered: selectedReferrerData.total_users,
6362
total_users_completed: selectedReferrerData.completed_users,
6463
overall_conversion_rate: selectedReferrerData.conversion_rate,
65-
avg_completion_time: 0, // Not available in referrer analytics
64+
avg_completion_time: 0,
6665
avg_completion_time_formatted: '0s',
6766
biggest_dropoff_step: 1,
6867
biggest_dropoff_rate: 100 - selectedReferrerData.conversion_rate,
69-
steps_analytics: [
70-
{
71-
step_number: 1,
72-
step_name: 'Landing Page',
73-
users: selectedReferrerData.total_users,
74-
total_users: selectedReferrerData.total_users,
75-
conversion_rate: 100,
76-
dropoffs: 0,
77-
dropoff_rate: 0,
78-
avg_time_to_complete: 0,
79-
},
80-
{
81-
step_number: 2,
82-
step_name: 'Completed',
83-
users: selectedReferrerData.completed_users,
84-
total_users: selectedReferrerData.total_users,
85-
conversion_rate: selectedReferrerData.conversion_rate,
86-
dropoffs:
87-
selectedReferrerData.total_users -
88-
selectedReferrerData.completed_users,
89-
dropoff_rate: 100 - selectedReferrerData.conversion_rate,
90-
avg_time_to_complete: 0,
91-
},
92-
],
68+
steps_analytics: data?.steps_analytics?.map((step, index) => ({
69+
...step,
70+
users: index === 0
71+
? selectedReferrerData.total_users
72+
: selectedReferrerData.completed_users,
73+
total_users: selectedReferrerData.total_users,
74+
conversion_rate: index === 0
75+
? 100
76+
: selectedReferrerData.conversion_rate,
77+
dropoffs: index === 0
78+
? 0
79+
: selectedReferrerData.total_users - selectedReferrerData.completed_users,
80+
dropoff_rate: index === 0
81+
? 0
82+
: 100 - selectedReferrerData.conversion_rate,
83+
avg_time_to_complete: 0,
84+
})) || [],
9385
}
9486
: data;
9587

packages/rpc/src/lib/analytics-utils.ts

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,76 @@ const buildStepQuery = (
111111
const whereCondition = buildWhereCondition(step, params);
112112
const referrerSelect = includeReferrer ? ', any(referrer) as referrer' : '';
113113

114+
// For PAGE_VIEW, only query analytics.events
115+
if (step.type === 'PAGE_VIEW') {
116+
return `
117+
SELECT
118+
${stepIndex + 1} as step_number,
119+
{${stepNameKey}:String} as step_name,
120+
session_id,
121+
MIN(time) as first_occurrence${referrerSelect}
122+
FROM analytics.events
123+
WHERE client_id = {websiteId:String}
124+
AND time >= parseDateTimeBestEffort({startDate:String})
125+
AND time <= parseDateTimeBestEffort({endDate:String})
126+
AND ${whereCondition}${filterConditions}
127+
GROUP BY session_id`;
128+
}
129+
130+
// For custom EVENT, query both analytics.events and analytics.custom_events
131+
const targetKey = `target_${step.step_number - 1}`;
132+
const referrerSelectCustom = includeReferrer ? ", '' as referrer" : '';
133+
114134
return `
135+
WITH filtered_sessions AS (
136+
SELECT DISTINCT session_id
137+
FROM analytics.events
138+
WHERE client_id = {websiteId:String}
139+
AND time >= parseDateTimeBestEffort({startDate:String})
140+
AND time <= parseDateTimeBestEffort({endDate:String})
141+
AND event_name = {${targetKey}:String}${filterConditions}
142+
),
143+
session_referrers AS (
144+
SELECT
145+
session_id,
146+
argMin(referrer, time) as session_referrer
147+
FROM analytics.events
148+
WHERE client_id = {websiteId:String}
149+
AND time >= parseDateTimeBestEffort({startDate:String})
150+
AND time <= parseDateTimeBestEffort({endDate:String})
151+
AND event_name = 'screen_view'
152+
AND referrer != ''
153+
GROUP BY session_id
154+
)
115155
SELECT
116156
${stepIndex + 1} as step_number,
117157
{${stepNameKey}:String} as step_name,
118158
session_id,
119-
MIN(time) as first_occurrence${referrerSelect}
120-
FROM analytics.events
121-
WHERE client_id = {websiteId:String}
122-
AND time >= parseDateTimeBestEffort({startDate:String})
123-
AND time <= parseDateTimeBestEffort({endDate:String})
124-
AND ${whereCondition}${filterConditions}
125-
GROUP BY session_id`;
159+
MIN(first_occurrence) as first_occurrence${includeReferrer ? ', COALESCE(sr.session_referrer, \'\') as referrer' : ''}
160+
FROM (
161+
SELECT
162+
session_id,
163+
time as first_occurrence
164+
FROM analytics.events
165+
WHERE client_id = {websiteId:String}
166+
AND time >= parseDateTimeBestEffort({startDate:String})
167+
AND time <= parseDateTimeBestEffort({endDate:String})
168+
AND event_name = {${targetKey}:String}${filterConditions}
169+
170+
UNION ALL
171+
172+
SELECT
173+
ce.session_id,
174+
ce.timestamp as first_occurrence
175+
FROM analytics.custom_events ce
176+
INNER JOIN filtered_sessions fs ON ce.session_id = fs.session_id
177+
WHERE ce.client_id = {websiteId:String}
178+
AND ce.timestamp >= parseDateTimeBestEffort({startDate:String})
179+
AND ce.timestamp <= parseDateTimeBestEffort({endDate:String})
180+
AND ce.event_name = {${targetKey}:String}
181+
)${includeReferrer ? `
182+
LEFT JOIN session_referrers sr ON session_id = sr.session_id` : ''}
183+
GROUP BY session_id${includeReferrer ? ', sr.session_referrer' : ''}`;
126184
};
127185

128186
const processSessionEvents = (
@@ -184,7 +242,7 @@ const calculateStepCounts = (
184242
): Map<number, Set<string>> => {
185243
const stepCounts = new Map<number, Set<string>>();
186244

187-
for (const [sessionId, events] of sessionEvents) {
245+
for (const [sessionId, events] of Array.from(sessionEvents.entries())) {
188246
events.sort((a, b) => a.first_occurrence - b.first_occurrence);
189247
let currentStep = 1;
190248

@@ -638,7 +696,7 @@ const calculateReferrerStepCounts = (
638696
): Map<number, Set<string>> => {
639697
const stepCounts = new Map<number, Set<string>>();
640698

641-
for (const sessionId of group.sessionIds) {
699+
for (const sessionId of Array.from(group.sessionIds)) {
642700
const events = sessionEvents
643701
.get(sessionId)
644702
?.sort((a, b) => a.first_occurrence - b.first_occurrence);
@@ -827,7 +885,7 @@ export const processFunnelAnalyticsByReferrer = async (
827885
}
828886
>();
829887

830-
for (const [sessionId, events] of sessionEvents) {
888+
for (const [sessionId, events] of Array.from(sessionEvents.entries())) {
831889
if (events.length > 0) {
832890
const referrer = events[0].referrer || 'Direct';
833891
const parsed = parseReferrer(referrer);
@@ -842,7 +900,7 @@ export const processFunnelAnalyticsByReferrer = async (
842900

843901
const referrerAnalytics: ReferrerAnalytics[] = [];
844902

845-
for (const [groupKey, group] of referrerGroups) {
903+
for (const [groupKey, group] of Array.from(referrerGroups.entries())) {
846904
const analytics = processReferrerGroup(
847905
groupKey,
848906
group,
@@ -857,4 +915,4 @@ export const processFunnelAnalyticsByReferrer = async (
857915
const referrer_analytics = aggregateReferrerAnalytics(referrerAnalytics);
858916

859917
return { referrer_analytics };
860-
};
918+
};

0 commit comments

Comments
 (0)