Skip to content

Commit 038234d

Browse files
committed
feat: users v1
1 parent f148c13 commit 038234d

File tree

10 files changed

+1275
-99
lines changed

10 files changed

+1275
-99
lines changed

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

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const ProfilesBuilders: Record<string, SimpleQueryConfig> = {
4040
client_id = {websiteId:String}
4141
AND time >= parseDateTimeBestEffort({startDate:String})
4242
AND time <= parseDateTimeBestEffort({endDate:String})
43-
${combinedWhereClause}
43+
${combinedWhereClause}
4444
GROUP BY anonymous_id
4545
ORDER BY last_visit DESC
4646
LIMIT {limit:Int32} OFFSET {offset:Int32}
@@ -79,7 +79,7 @@ export const ProfilesBuilders: Record<string, SimpleQueryConfig> = {
7979
FROM analytics.events e
8080
INNER JOIN visitor_profiles vp ON e.anonymous_id = vp.visitor_id
8181
WHERE e.client_id = {websiteId:String}
82-
${combinedWhereClause}
82+
${combinedWhereClause}
8383
GROUP BY vp.visitor_id, e.session_id
8484
ORDER BY vp.visitor_id, session_start DESC
8585
)
@@ -127,6 +127,142 @@ export const ProfilesBuilders: Record<string, SimpleQueryConfig> = {
127127
},
128128
},
129129

130+
profile_detail: {
131+
customSql: (
132+
websiteId: string,
133+
startDate: string,
134+
endDate: string,
135+
filters?: Filter[],
136+
_granularity?: unknown,
137+
_limit?: number,
138+
_offset?: number,
139+
_timezone?: string,
140+
_filterConditions?: string[],
141+
_filterParams?: Record<string, Filter['value']>
142+
) => {
143+
const visitorId = filters?.find((f) => f.field === 'anonymous_id')?.value;
144+
145+
if (!visitorId || typeof visitorId !== 'string') {
146+
throw new Error(
147+
'anonymous_id filter is required for profile_detail query'
148+
);
149+
}
150+
151+
return {
152+
sql: `
153+
WITH user_profile AS (
154+
SELECT
155+
anonymous_id as visitor_id,
156+
MIN(time) as first_visit,
157+
MAX(time) as last_visit,
158+
COUNT(DISTINCT session_id) as total_sessions,
159+
COUNT(*) as total_pageviews,
160+
SUM(CASE WHEN time_on_page > 0 THEN time_on_page ELSE 0 END) as total_duration,
161+
formatReadableTimeDelta(SUM(CASE WHEN time_on_page > 0 THEN time_on_page ELSE 0 END)) as total_duration_formatted,
162+
any(device_type) as device,
163+
any(browser_name) as browser,
164+
any(os_name) as os,
165+
any(country) as country,
166+
any(region) as region
167+
FROM analytics.events
168+
WHERE
169+
client_id = {websiteId:String}
170+
AND anonymous_id = {visitorId:String}
171+
AND time >= parseDateTimeBestEffort({startDate:String})
172+
AND time <= parseDateTimeBestEffort({endDate:String})
173+
GROUP BY anonymous_id
174+
),
175+
user_sessions AS (
176+
SELECT
177+
e.session_id,
178+
CONCAT('Session ', ROW_NUMBER() OVER (ORDER BY MIN(e.time))) as session_name,
179+
MIN(e.time) as first_visit,
180+
MAX(e.time) as last_visit,
181+
LEAST(dateDiff('second', MIN(e.time), MAX(e.time)), 28800) as duration,
182+
formatReadableTimeDelta(LEAST(dateDiff('second', MIN(e.time), MAX(e.time)), 28800)) as duration_formatted,
183+
COUNT(DISTINCT e.path) as page_views,
184+
COUNT(DISTINCT e.path) as unique_pages,
185+
any(e.device_type) as device,
186+
any(e.browser_name) as browser,
187+
any(e.os_name) as os,
188+
any(e.country) as country,
189+
any(e.region) as region,
190+
any(e.referrer) as referrer,
191+
groupArray(
192+
tuple(
193+
e.id,
194+
e.time,
195+
e.event_name,
196+
e.path,
197+
CASE
198+
WHEN e.event_name NOT IN ('screen_view', 'page_exit', 'web_vitals', 'link_out')
199+
AND e.properties IS NOT NULL
200+
AND e.properties != '{}'
201+
THEN CAST(e.properties AS String)
202+
ELSE NULL
203+
END,
204+
NULL,
205+
NULL
206+
)
207+
) as events
208+
FROM analytics.events e
209+
WHERE
210+
e.client_id = {websiteId:String}
211+
AND e.anonymous_id = {visitorId:String}
212+
AND e.time >= parseDateTimeBestEffort({startDate:String})
213+
AND e.time <= parseDateTimeBestEffort({endDate:String})
214+
GROUP BY e.session_id
215+
ORDER BY first_visit DESC
216+
)
217+
SELECT
218+
up.visitor_id,
219+
up.first_visit,
220+
up.last_visit,
221+
up.total_sessions,
222+
up.total_pageviews,
223+
up.total_duration,
224+
up.total_duration_formatted,
225+
up.device,
226+
up.browser,
227+
up.os,
228+
up.country,
229+
up.region,
230+
groupArray(
231+
tuple(
232+
us.session_id,
233+
us.session_name,
234+
us.first_visit,
235+
us.last_visit,
236+
us.duration,
237+
us.duration_formatted,
238+
us.page_views,
239+
us.unique_pages,
240+
us.device,
241+
us.browser,
242+
us.os,
243+
us.country,
244+
us.region,
245+
us.referrer,
246+
us.events
247+
)
248+
) as sessions
249+
FROM user_profile up
250+
LEFT JOIN user_sessions us ON 1=1
251+
GROUP BY
252+
up.visitor_id, up.first_visit, up.last_visit, up.total_sessions,
253+
up.total_pageviews, up.total_duration, up.total_duration_formatted,
254+
up.device, up.browser, up.os, up.country, up.region
255+
`,
256+
params: {
257+
websiteId,
258+
visitorId,
259+
startDate,
260+
endDate: `${endDate} 23:59:59`,
261+
},
262+
};
263+
},
264+
},
265+
130266
profile_metrics: {
131267
table: Analytics.events,
132268
fields: [
@@ -238,7 +374,7 @@ export const ProfilesBuilders: Record<string, SimpleQueryConfig> = {
238374
AND time >= parseDateTimeBestEffort({startDate:String})
239375
AND time <= parseDateTimeBestEffort({endDate:String})
240376
AND event_name = 'screen_view'
241-
${combinedWhereClause}
377+
${combinedWhereClause}
242378
GROUP BY anonymous_id
243379
HAVING session_count > 1
244380
ORDER BY session_count DESC

apps/basket/src/routes/basket.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -519,28 +519,10 @@ async function insertTrackEvent(
519519
dom_interactive: validatePerformanceMetric(trackData.dom_interactive),
520520
ttfb: validatePerformanceMetric(trackData.ttfb),
521521
connection_time: validatePerformanceMetric(trackData.connection_time),
522-
request_time: validatePerformanceMetric(trackData.request_time),
523522
render_time: validatePerformanceMetric(trackData.render_time),
524523
redirect_time: validatePerformanceMetric(trackData.redirect_time),
525524
domain_lookup_time: validatePerformanceMetric(trackData.domain_lookup_time),
526525

527-
fcp: validatePerformanceMetric(trackData.fcp),
528-
lcp: validatePerformanceMetric(trackData.lcp),
529-
cls: validatePerformanceMetric(trackData.cls),
530-
fid: validatePerformanceMetric(trackData.fid),
531-
inp: validatePerformanceMetric(trackData.inp),
532-
533-
href: trackData.href,
534-
text: trackData.text,
535-
value: trackData.value,
536-
537-
error_message: undefined,
538-
error_filename: undefined,
539-
error_lineno: undefined,
540-
error_colno: undefined,
541-
error_stack: undefined,
542-
error_type: undefined,
543-
544526
properties: trackData.properties
545527
? JSON.stringify(trackData.properties)
546528
: '{}',
@@ -850,7 +832,6 @@ const app = new Elysia()
850832
}
851833

852834
if (eventType === 'outgoing_link') {
853-
// Check for bots before processing outgoing link events
854835
const botError = await checkForBot(
855836
request,
856837
body,

apps/dashboard/app/(main)/websites/[id]/layout.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ export default function WebsiteLayout({ children }: WebsiteLayoutProps) {
3333
pathname.includes('/map') ||
3434
pathname.includes('/flags') ||
3535
pathname.includes('/databunny') ||
36-
pathname.includes('/settings');
36+
pathname.includes('/settings') ||
37+
pathname.includes('/users');
3738

38-
const noPadding = pathname.includes('/settings');
39+
const noPadding =
40+
pathname.includes('/settings') || pathname.includes('/users');
3941

4042
const handleRefresh = async () => {
4143
setIsRefreshing(true);

0 commit comments

Comments
 (0)