@litemetrics/node 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -153,6 +153,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
153
153
  title Nullable(String),
154
154
  event_name Nullable(String),
155
155
  properties Nullable(String),
156
+ event_source LowCardinality(Nullable(String)),
157
+ event_subtype LowCardinality(Nullable(String)),
158
+ page_path Nullable(String),
159
+ target_url_path Nullable(String),
160
+ element_selector Nullable(String),
161
+ element_text Nullable(String),
162
+ scroll_depth_pct Nullable(UInt8),
156
163
  user_id Nullable(String),
157
164
  traits Nullable(String),
158
165
  country LowCardinality(Nullable(String)),
@@ -184,6 +191,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
184
191
  name String,
185
192
  domain Nullable(String),
186
193
  allowed_origins Nullable(String),
194
+ conversion_events Nullable(String),
187
195
  created_at DateTime64(3),
188
196
  updated_at DateTime64(3),
189
197
  version UInt64,
@@ -196,6 +204,39 @@ function toCHDateTime(d) {
196
204
  const iso = typeof d === "string" ? d : d.toISOString();
197
205
  return iso.replace("T", " ").replace("Z", "");
198
206
  }
207
+ function buildFilterConditions(filters) {
208
+ if (!filters) return { conditions: [], params: {} };
209
+ const map = {
210
+ "geo.country": "country",
211
+ "geo.city": "city",
212
+ "geo.region": "region",
213
+ "language": "language",
214
+ "device.type": "device_type",
215
+ "device.browser": "browser",
216
+ "device.os": "os",
217
+ "utm.source": "utm_source",
218
+ "utm.medium": "utm_medium",
219
+ "utm.campaign": "utm_campaign",
220
+ "utm.term": "utm_term",
221
+ "utm.content": "utm_content",
222
+ "referrer": "referrer",
223
+ "event_source": "event_source",
224
+ "event_subtype": "event_subtype",
225
+ "page_path": "page_path",
226
+ "target_url_path": "target_url_path",
227
+ "event_name": "event_name",
228
+ "type": "type"
229
+ };
230
+ const conditions = [];
231
+ const params = {};
232
+ for (const [key, value] of Object.entries(filters)) {
233
+ if (!value || !map[key]) continue;
234
+ const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
235
+ conditions.push(`${map[key]} = {${paramKey}:String}`);
236
+ params[paramKey] = value;
237
+ }
238
+ return { conditions, params };
239
+ }
199
240
  var ClickHouseAdapter = class {
200
241
  client;
201
242
  constructor(url) {
@@ -209,6 +250,16 @@ var ClickHouseAdapter = class {
209
250
  async init() {
210
251
  await this.client.command({ query: CREATE_EVENTS_TABLE });
211
252
  await this.client.command({ query: CREATE_SITES_TABLE });
253
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_source LowCardinality(Nullable(String))` });
254
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_subtype LowCardinality(Nullable(String))` });
255
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS page_path Nullable(String)` });
256
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS target_url_path Nullable(String)` });
257
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_selector Nullable(String)` });
258
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_text Nullable(String)` });
259
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS scroll_depth_pct Nullable(UInt8)` });
260
+ await this.client.command({
261
+ query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
262
+ });
212
263
  }
213
264
  async close() {
214
265
  await this.client.close();
@@ -227,6 +278,13 @@ var ClickHouseAdapter = class {
227
278
  title: e.title ?? null,
228
279
  event_name: e.name ?? null,
229
280
  properties: e.properties ? JSON.stringify(e.properties) : null,
281
+ event_source: e.eventSource ?? null,
282
+ event_subtype: e.eventSubtype ?? null,
283
+ page_path: e.pagePath ?? null,
284
+ target_url_path: e.targetUrlPath ?? null,
285
+ element_selector: e.elementSelector ?? null,
286
+ element_text: e.elementText ?? null,
287
+ scroll_depth_pct: e.scrollDepthPct ?? null,
230
288
  user_id: e.userId ?? null,
231
289
  traits: e.traits ? JSON.stringify(e.traits) : null,
232
290
  country: e.geo?.country ?? null,
@@ -263,6 +321,8 @@ var ClickHouseAdapter = class {
263
321
  to: toCHDateTime(dateRange.to),
264
322
  limit
265
323
  };
324
+ const filter = buildFilterConditions(q.filters);
325
+ const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
266
326
  let data = [];
267
327
  let total = 0;
268
328
  switch (q.metric) {
@@ -272,8 +332,8 @@ var ClickHouseAdapter = class {
272
332
  WHERE site_id = {siteId:String}
273
333
  AND timestamp >= {from:String}
274
334
  AND timestamp <= {to:String}
275
- AND type = 'pageview'`,
276
- params
335
+ AND type = 'pageview'${filterSql}`,
336
+ { ...params, ...filter.params }
277
337
  );
278
338
  total = Number(rows[0]?.value ?? 0);
279
339
  data = [{ key: "pageviews", value: total }];
@@ -284,8 +344,8 @@ var ClickHouseAdapter = class {
284
344
  `SELECT uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
285
345
  WHERE site_id = {siteId:String}
286
346
  AND timestamp >= {from:String}
287
- AND timestamp <= {to:String}`,
288
- params
347
+ AND timestamp <= {to:String}${filterSql}`,
348
+ { ...params, ...filter.params }
289
349
  );
290
350
  total = Number(rows[0]?.value ?? 0);
291
351
  data = [{ key: "visitors", value: total }];
@@ -296,8 +356,8 @@ var ClickHouseAdapter = class {
296
356
  `SELECT uniq(session_id) AS value FROM ${EVENTS_TABLE}
297
357
  WHERE site_id = {siteId:String}
298
358
  AND timestamp >= {from:String}
299
- AND timestamp <= {to:String}`,
300
- params
359
+ AND timestamp <= {to:String}${filterSql}`,
360
+ { ...params, ...filter.params }
301
361
  );
302
362
  total = Number(rows[0]?.value ?? 0);
303
363
  data = [{ key: "sessions", value: total }];
@@ -309,13 +369,33 @@ var ClickHouseAdapter = class {
309
369
  WHERE site_id = {siteId:String}
310
370
  AND timestamp >= {from:String}
311
371
  AND timestamp <= {to:String}
312
- AND type = 'event'`,
313
- params
372
+ AND type = 'event'${filterSql}`,
373
+ { ...params, ...filter.params }
314
374
  );
315
375
  total = Number(rows[0]?.value ?? 0);
316
376
  data = [{ key: "events", value: total }];
317
377
  break;
318
378
  }
379
+ case "conversions": {
380
+ const conversionEvents = q.conversionEvents ?? [];
381
+ if (conversionEvents.length === 0) {
382
+ total = 0;
383
+ data = [{ key: "conversions", value: 0 }];
384
+ break;
385
+ }
386
+ const rows = await this.queryRows(
387
+ `SELECT count() AS value FROM ${EVENTS_TABLE}
388
+ WHERE site_id = {siteId:String}
389
+ AND timestamp >= {from:String}
390
+ AND timestamp <= {to:String}
391
+ AND type = 'event'
392
+ AND event_name IN {eventNames:Array(String)}${filterSql}`,
393
+ { ...params, eventNames: conversionEvents, ...filter.params }
394
+ );
395
+ total = Number(rows[0]?.value ?? 0);
396
+ data = [{ key: "conversions", value: total }];
397
+ break;
398
+ }
319
399
  case "top_pages": {
320
400
  const rows = await this.queryRows(
321
401
  `SELECT url AS key, count() AS value FROM ${EVENTS_TABLE}
@@ -323,11 +403,11 @@ var ClickHouseAdapter = class {
323
403
  AND timestamp >= {from:String}
324
404
  AND timestamp <= {to:String}
325
405
  AND type = 'pageview'
326
- AND url IS NOT NULL
406
+ AND url IS NOT NULL${filterSql}
327
407
  GROUP BY url
328
408
  ORDER BY value DESC
329
409
  LIMIT {limit:UInt32}`,
330
- params
410
+ { ...params, ...filter.params }
331
411
  );
332
412
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
333
413
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -341,11 +421,11 @@ var ClickHouseAdapter = class {
341
421
  AND timestamp <= {to:String}
342
422
  AND type = 'pageview'
343
423
  AND referrer IS NOT NULL
344
- AND referrer != ''
424
+ AND referrer != ''${filterSql}
345
425
  GROUP BY referrer
346
426
  ORDER BY value DESC
347
427
  LIMIT {limit:UInt32}`,
348
- params
428
+ { ...params, ...filter.params }
349
429
  );
350
430
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
351
431
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -357,11 +437,11 @@ var ClickHouseAdapter = class {
357
437
  WHERE site_id = {siteId:String}
358
438
  AND timestamp >= {from:String}
359
439
  AND timestamp <= {to:String}
360
- AND country IS NOT NULL
440
+ AND country IS NOT NULL${filterSql}
361
441
  GROUP BY country
362
442
  ORDER BY value DESC
363
443
  LIMIT {limit:UInt32}`,
364
- params
444
+ { ...params, ...filter.params }
365
445
  );
366
446
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
367
447
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -373,11 +453,11 @@ var ClickHouseAdapter = class {
373
453
  WHERE site_id = {siteId:String}
374
454
  AND timestamp >= {from:String}
375
455
  AND timestamp <= {to:String}
376
- AND city IS NOT NULL
456
+ AND city IS NOT NULL${filterSql}
377
457
  GROUP BY city
378
458
  ORDER BY value DESC
379
459
  LIMIT {limit:UInt32}`,
380
- params
460
+ { ...params, ...filter.params }
381
461
  );
382
462
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
383
463
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -391,10 +471,132 @@ var ClickHouseAdapter = class {
391
471
  AND timestamp <= {to:String}
392
472
  AND type = 'event'
393
473
  AND event_name IS NOT NULL
474
+ ${filterSql}
394
475
  GROUP BY event_name
395
476
  ORDER BY value DESC
396
477
  LIMIT {limit:UInt32}`,
397
- params
478
+ { ...params, ...filter.params }
479
+ );
480
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
481
+ total = data.reduce((sum, d) => sum + d.value, 0);
482
+ break;
483
+ }
484
+ case "top_conversions": {
485
+ const conversionEvents = q.conversionEvents ?? [];
486
+ if (conversionEvents.length === 0) {
487
+ total = 0;
488
+ data = [];
489
+ break;
490
+ }
491
+ const rows = await this.queryRows(
492
+ `SELECT event_name AS key, count() AS value FROM ${EVENTS_TABLE}
493
+ WHERE site_id = {siteId:String}
494
+ AND timestamp >= {from:String}
495
+ AND timestamp <= {to:String}
496
+ AND type = 'event'
497
+ AND event_name IN {eventNames:Array(String)}
498
+ ${filterSql}
499
+ GROUP BY event_name
500
+ ORDER BY value DESC
501
+ LIMIT {limit:UInt32}`,
502
+ { ...params, eventNames: conversionEvents, ...filter.params }
503
+ );
504
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
505
+ total = data.reduce((sum, d) => sum + d.value, 0);
506
+ break;
507
+ }
508
+ case "top_exit_pages": {
509
+ const rows = await this.queryRows(
510
+ `SELECT url AS key, count() AS value FROM (
511
+ SELECT session_id, argMax(url, timestamp) AS url
512
+ FROM ${EVENTS_TABLE}
513
+ WHERE site_id = {siteId:String}
514
+ AND timestamp >= {from:String}
515
+ AND timestamp <= {to:String}
516
+ AND type = 'pageview'
517
+ AND url IS NOT NULL${filterSql}
518
+ GROUP BY session_id
519
+ )
520
+ GROUP BY url
521
+ ORDER BY value DESC
522
+ LIMIT {limit:UInt32}`,
523
+ { ...params, ...filter.params }
524
+ );
525
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
526
+ total = data.reduce((sum, d) => sum + d.value, 0);
527
+ break;
528
+ }
529
+ case "top_transitions": {
530
+ const rows = await this.queryRows(
531
+ `SELECT concat(prev_url, ' \u2192 ', url) AS key, count() AS value FROM (
532
+ SELECT session_id, url,
533
+ lag(url) OVER (PARTITION BY session_id ORDER BY timestamp) AS prev_url
534
+ FROM ${EVENTS_TABLE}
535
+ WHERE site_id = {siteId:String}
536
+ AND timestamp >= {from:String}
537
+ AND timestamp <= {to:String}
538
+ AND type = 'pageview'
539
+ AND url IS NOT NULL${filterSql}
540
+ )
541
+ WHERE prev_url IS NOT NULL
542
+ GROUP BY key
543
+ ORDER BY value DESC
544
+ LIMIT {limit:UInt32}`,
545
+ { ...params, ...filter.params }
546
+ );
547
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
548
+ total = data.reduce((sum, d) => sum + d.value, 0);
549
+ break;
550
+ }
551
+ case "top_scroll_pages": {
552
+ const rows = await this.queryRows(
553
+ `SELECT page_path AS key, count() AS value FROM ${EVENTS_TABLE}
554
+ WHERE site_id = {siteId:String}
555
+ AND timestamp >= {from:String}
556
+ AND timestamp <= {to:String}
557
+ AND type = 'event'
558
+ AND event_subtype = 'scroll_depth'
559
+ AND page_path IS NOT NULL${filterSql}
560
+ GROUP BY page_path
561
+ ORDER BY value DESC
562
+ LIMIT {limit:UInt32}`,
563
+ { ...params, ...filter.params }
564
+ );
565
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
566
+ total = data.reduce((sum, d) => sum + d.value, 0);
567
+ break;
568
+ }
569
+ case "top_button_clicks": {
570
+ const rows = await this.queryRows(
571
+ `SELECT ifNull(element_text, element_selector) AS key, count() AS value FROM ${EVENTS_TABLE}
572
+ WHERE site_id = {siteId:String}
573
+ AND timestamp >= {from:String}
574
+ AND timestamp <= {to:String}
575
+ AND type = 'event'
576
+ AND event_subtype = 'button_click'
577
+ AND (element_text IS NOT NULL OR element_selector IS NOT NULL)${filterSql}
578
+ GROUP BY key
579
+ ORDER BY value DESC
580
+ LIMIT {limit:UInt32}`,
581
+ { ...params, ...filter.params }
582
+ );
583
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
584
+ total = data.reduce((sum, d) => sum + d.value, 0);
585
+ break;
586
+ }
587
+ case "top_link_targets": {
588
+ const rows = await this.queryRows(
589
+ `SELECT target_url_path AS key, count() AS value FROM ${EVENTS_TABLE}
590
+ WHERE site_id = {siteId:String}
591
+ AND timestamp >= {from:String}
592
+ AND timestamp <= {to:String}
593
+ AND type = 'event'
594
+ AND event_subtype IN ('link_click','outbound_click')
595
+ AND target_url_path IS NOT NULL${filterSql}
596
+ GROUP BY target_url_path
597
+ ORDER BY value DESC
598
+ LIMIT {limit:UInt32}`,
599
+ { ...params, ...filter.params }
398
600
  );
399
601
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
400
602
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -407,10 +609,11 @@ var ClickHouseAdapter = class {
407
609
  AND timestamp >= {from:String}
408
610
  AND timestamp <= {to:String}
409
611
  AND device_type IS NOT NULL
612
+ ${filterSql}
410
613
  GROUP BY device_type
411
614
  ORDER BY value DESC
412
615
  LIMIT {limit:UInt32}`,
413
- params
616
+ { ...params, ...filter.params }
414
617
  );
415
618
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
416
619
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -423,10 +626,11 @@ var ClickHouseAdapter = class {
423
626
  AND timestamp >= {from:String}
424
627
  AND timestamp <= {to:String}
425
628
  AND browser IS NOT NULL
629
+ ${filterSql}
426
630
  GROUP BY browser
427
631
  ORDER BY value DESC
428
632
  LIMIT {limit:UInt32}`,
429
- params
633
+ { ...params, ...filter.params }
430
634
  );
431
635
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
432
636
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -439,10 +643,11 @@ var ClickHouseAdapter = class {
439
643
  AND timestamp >= {from:String}
440
644
  AND timestamp <= {to:String}
441
645
  AND os IS NOT NULL
646
+ ${filterSql}
442
647
  GROUP BY os
443
648
  ORDER BY value DESC
444
649
  LIMIT {limit:UInt32}`,
445
- params
650
+ { ...params, ...filter.params }
446
651
  );
447
652
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
448
653
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -450,7 +655,7 @@ var ClickHouseAdapter = class {
450
655
  }
451
656
  }
452
657
  const result = { metric: q.metric, period, data, total };
453
- if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
658
+ if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
454
659
  const prevRange = previousPeriodRange(dateRange);
455
660
  const prevResult = await this.query({
456
661
  ...q,
@@ -480,7 +685,12 @@ var ClickHouseAdapter = class {
480
685
  const granularity = params.granularity ?? autoGranularity(period);
481
686
  const bucketFn = this.granularityToClickHouseFunc(granularity);
482
687
  const dateFormat = granularityToDateFormat(granularity);
688
+ const filter = buildFilterConditions(params.filters);
689
+ const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
483
690
  const typeFilter = params.metric === "pageviews" ? `AND type = 'pageview'` : "";
691
+ const eventsFilter = params.metric === "events" ? `AND type = 'event'` : "";
692
+ const conversionsFilter = params.metric === "conversions" ? `AND type = 'event' AND event_name IN {eventNames:Array(String)}` : "";
693
+ const extraFilters = [typeFilter, eventsFilter, conversionsFilter, filterSql].filter(Boolean).join(" ");
484
694
  let sql;
485
695
  if (params.metric === "visitors" || params.metric === "sessions") {
486
696
  const field = params.metric === "visitors" ? "visitor_id" : "session_id";
@@ -490,7 +700,7 @@ var ClickHouseAdapter = class {
490
700
  WHERE site_id = {siteId:String}
491
701
  AND timestamp >= {from:String}
492
702
  AND timestamp <= {to:String}
493
- ${typeFilter}
703
+ ${extraFilters}
494
704
  GROUP BY bucket
495
705
  ORDER BY bucket ASC
496
706
  `;
@@ -501,7 +711,7 @@ var ClickHouseAdapter = class {
501
711
  WHERE site_id = {siteId:String}
502
712
  AND timestamp >= {from:String}
503
713
  AND timestamp <= {to:String}
504
- ${typeFilter}
714
+ ${extraFilters}
505
715
  GROUP BY bucket
506
716
  ORDER BY bucket ASC
507
717
  `;
@@ -509,7 +719,9 @@ var ClickHouseAdapter = class {
509
719
  const rows = await this.queryRows(sql, {
510
720
  siteId: params.siteId,
511
721
  from: toCHDateTime(dateRange.from),
512
- to: toCHDateTime(dateRange.to)
722
+ to: toCHDateTime(dateRange.to),
723
+ eventNames: params.conversionEvents ?? [],
724
+ ...filter.params
513
725
  });
514
726
  const mappedRows = rows.map((r) => ({
515
727
  _id: this.convertClickHouseBucket(r.bucket, granularity),
@@ -627,6 +839,14 @@ var ClickHouseAdapter = class {
627
839
  conditions.push(`event_name = {eventName:String}`);
628
840
  queryParams.eventName = params.eventName;
629
841
  }
842
+ if (params.eventSource) {
843
+ conditions.push(`event_source = {eventSource:String}`);
844
+ queryParams.eventSource = params.eventSource;
845
+ }
846
+ if (params.eventNames && params.eventNames.length > 0) {
847
+ conditions.push(`event_name IN {eventNames:Array(String)}`);
848
+ queryParams.eventNames = params.eventNames;
849
+ }
630
850
  if (params.visitorId) {
631
851
  conditions.push(`visitor_id = {visitorId:String}`);
632
852
  queryParams.visitorId = params.visitorId;
@@ -649,7 +869,9 @@ var ClickHouseAdapter = class {
649
869
  const [events, countRows] = await Promise.all([
650
870
  this.queryRows(
651
871
  `SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
652
- event_name, properties, user_id, traits, country, city, region,
872
+ event_name, properties, event_source, event_subtype, page_path, target_url_path,
873
+ element_selector, element_text, scroll_depth_pct,
874
+ user_id, traits, country, city, region,
653
875
  device_type, browser, os, language,
654
876
  utm_source, utm_medium, utm_campaign, utm_term, utm_content
655
877
  FROM ${EVENTS_TABLE}
@@ -694,13 +916,22 @@ var ClickHouseAdapter = class {
694
916
  countIf(type = 'pageview') AS totalPageviews,
695
917
  uniq(session_id) AS totalSessions,
696
918
  anyLast(url) AS lastUrl,
919
+ anyLast(referrer) AS referrer,
697
920
  anyLast(device_type) AS device_type,
698
921
  anyLast(browser) AS browser,
699
922
  anyLast(os) AS os,
700
923
  anyLast(country) AS country,
701
924
  anyLast(city) AS city,
702
925
  anyLast(region) AS region,
703
- anyLast(language) AS language
926
+ anyLast(language) AS language,
927
+ anyLast(timezone) AS timezone,
928
+ anyLast(screen_width) AS screen_width,
929
+ anyLast(screen_height) AS screen_height,
930
+ anyLast(utm_source) AS utm_source,
931
+ anyLast(utm_medium) AS utm_medium,
932
+ anyLast(utm_campaign) AS utm_campaign,
933
+ anyLast(utm_term) AS utm_term,
934
+ anyLast(utm_content) AS utm_content
704
935
  FROM ${EVENTS_TABLE}
705
936
  WHERE ${where}
706
937
  GROUP BY visitor_id
@@ -728,9 +959,19 @@ var ClickHouseAdapter = class {
728
959
  totalPageviews: Number(u.totalPageviews),
729
960
  totalSessions: Number(u.totalSessions),
730
961
  lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
962
+ referrer: u.referrer ? String(u.referrer) : void 0,
731
963
  device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
732
964
  geo: u.country ? { country: String(u.country), city: u.city ? String(u.city) : void 0, region: u.region ? String(u.region) : void 0 } : void 0,
733
- language: u.language ? String(u.language) : void 0
965
+ language: u.language ? String(u.language) : void 0,
966
+ timezone: u.timezone ? String(u.timezone) : void 0,
967
+ screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
968
+ utm: u.utm_source ? {
969
+ source: String(u.utm_source),
970
+ medium: u.utm_medium ? String(u.utm_medium) : void 0,
971
+ campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
972
+ term: u.utm_term ? String(u.utm_term) : void 0,
973
+ content: u.utm_content ? String(u.utm_content) : void 0
974
+ } : void 0
734
975
  }));
735
976
  return {
736
977
  users,
@@ -758,6 +999,7 @@ var ClickHouseAdapter = class {
758
999
  name: data.name,
759
1000
  domain: data.domain,
760
1001
  allowedOrigins: data.allowedOrigins,
1002
+ conversionEvents: data.conversionEvents,
761
1003
  createdAt: nowISO,
762
1004
  updatedAt: nowISO
763
1005
  };
@@ -769,6 +1011,7 @@ var ClickHouseAdapter = class {
769
1011
  name: site.name,
770
1012
  domain: site.domain ?? null,
771
1013
  allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
1014
+ conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
772
1015
  created_at: nowCH,
773
1016
  updated_at: nowCH,
774
1017
  version: 1,
@@ -780,7 +1023,7 @@ var ClickHouseAdapter = class {
780
1023
  }
781
1024
  async getSite(siteId) {
782
1025
  const rows = await this.queryRows(
783
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
1026
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
784
1027
  FROM ${SITES_TABLE} FINAL
785
1028
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
786
1029
  { siteId }
@@ -789,7 +1032,7 @@ var ClickHouseAdapter = class {
789
1032
  }
790
1033
  async getSiteBySecret(secretKey) {
791
1034
  const rows = await this.queryRows(
792
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
1035
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
793
1036
  FROM ${SITES_TABLE} FINAL
794
1037
  WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
795
1038
  { secretKey }
@@ -798,7 +1041,7 @@ var ClickHouseAdapter = class {
798
1041
  }
799
1042
  async listSites() {
800
1043
  const rows = await this.queryRows(
801
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
1044
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
802
1045
  FROM ${SITES_TABLE} FINAL
803
1046
  WHERE is_deleted = 0
804
1047
  ORDER BY created_at DESC`,
@@ -808,7 +1051,7 @@ var ClickHouseAdapter = class {
808
1051
  }
809
1052
  async updateSite(siteId, data) {
810
1053
  const currentRows = await this.queryRows(
811
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at, version
1054
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
812
1055
  FROM ${SITES_TABLE} FINAL
813
1056
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
814
1057
  { siteId }
@@ -822,6 +1065,7 @@ var ClickHouseAdapter = class {
822
1065
  const newName = data.name !== void 0 ? data.name : String(current.name);
823
1066
  const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
824
1067
  const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
1068
+ const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
825
1069
  await this.client.insert({
826
1070
  table: SITES_TABLE,
827
1071
  values: [{
@@ -830,6 +1074,7 @@ var ClickHouseAdapter = class {
830
1074
  name: newName,
831
1075
  domain: newDomain,
832
1076
  allowed_origins: newOrigins,
1077
+ conversion_events: newConversions,
833
1078
  created_at: toCHDateTime(String(current.created_at)),
834
1079
  updated_at: nowCH,
835
1080
  version: newVersion,
@@ -843,13 +1088,14 @@ var ClickHouseAdapter = class {
843
1088
  name: newName,
844
1089
  domain: newDomain ?? void 0,
845
1090
  allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
1091
+ conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
846
1092
  createdAt: String(current.created_at),
847
1093
  updatedAt: nowISO
848
1094
  };
849
1095
  }
850
1096
  async deleteSite(siteId) {
851
1097
  const currentRows = await this.queryRows(
852
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
1098
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
853
1099
  FROM ${SITES_TABLE} FINAL
854
1100
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
855
1101
  { siteId }
@@ -865,6 +1111,7 @@ var ClickHouseAdapter = class {
865
1111
  name: String(current.name),
866
1112
  domain: current.domain ? String(current.domain) : null,
867
1113
  allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
1114
+ conversion_events: current.conversion_events ? String(current.conversion_events) : null,
868
1115
  created_at: toCHDateTime(String(current.created_at)),
869
1116
  updated_at: nowCH,
870
1117
  version: Number(current.version) + 1,
@@ -876,7 +1123,7 @@ var ClickHouseAdapter = class {
876
1123
  }
877
1124
  async regenerateSecret(siteId) {
878
1125
  const currentRows = await this.queryRows(
879
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
1126
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
880
1127
  FROM ${SITES_TABLE} FINAL
881
1128
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
882
1129
  { siteId }
@@ -895,6 +1142,7 @@ var ClickHouseAdapter = class {
895
1142
  name: String(current.name),
896
1143
  domain: current.domain ? String(current.domain) : null,
897
1144
  allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
1145
+ conversion_events: current.conversion_events ? String(current.conversion_events) : null,
898
1146
  created_at: toCHDateTime(String(current.created_at)),
899
1147
  updated_at: nowCH,
900
1148
  version: Number(current.version) + 1,
@@ -908,6 +1156,7 @@ var ClickHouseAdapter = class {
908
1156
  name: String(current.name),
909
1157
  domain: current.domain ? String(current.domain) : void 0,
910
1158
  allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
1159
+ conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
911
1160
  createdAt: String(current.created_at),
912
1161
  updatedAt: nowISO
913
1162
  };
@@ -928,6 +1177,7 @@ var ClickHouseAdapter = class {
928
1177
  name: String(row.name),
929
1178
  domain: row.domain ? String(row.domain) : void 0,
930
1179
  allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
1180
+ conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
931
1181
  createdAt: new Date(String(row.created_at)).toISOString(),
932
1182
  updatedAt: new Date(String(row.updated_at)).toISOString()
933
1183
  };
@@ -944,6 +1194,13 @@ var ClickHouseAdapter = class {
944
1194
  title: row.title ? String(row.title) : void 0,
945
1195
  name: row.event_name ? String(row.event_name) : void 0,
946
1196
  properties: this.parseJSON(row.properties),
1197
+ eventSource: row.event_source ? String(row.event_source) : void 0,
1198
+ eventSubtype: row.event_subtype ? String(row.event_subtype) : void 0,
1199
+ pagePath: row.page_path ? String(row.page_path) : void 0,
1200
+ targetUrlPath: row.target_url_path ? String(row.target_url_path) : void 0,
1201
+ elementSelector: row.element_selector ? String(row.element_selector) : void 0,
1202
+ elementText: row.element_text ? String(row.element_text) : void 0,
1203
+ scrollDepthPct: row.scroll_depth_pct !== null && row.scroll_depth_pct !== void 0 ? Number(row.scroll_depth_pct) : void 0,
947
1204
  userId: row.user_id ? String(row.user_id) : void 0,
948
1205
  traits: this.parseJSON(row.traits),
949
1206
  geo: row.country ? {
@@ -980,6 +1237,36 @@ var ClickHouseAdapter = class {
980
1237
  import { MongoClient } from "mongodb";
981
1238
  var EVENTS_COLLECTION = "litemetrics_events";
982
1239
  var SITES_COLLECTION = "litemetrics_sites";
1240
+ function buildFilterMatch(filters) {
1241
+ if (!filters) return {};
1242
+ const map = {
1243
+ "geo.country": "country",
1244
+ "geo.city": "city",
1245
+ "geo.region": "region",
1246
+ "language": "language",
1247
+ "device.type": "device_type",
1248
+ "device.browser": "browser",
1249
+ "device.os": "os",
1250
+ "utm.source": "utm_source",
1251
+ "utm.medium": "utm_medium",
1252
+ "utm.campaign": "utm_campaign",
1253
+ "utm.term": "utm_term",
1254
+ "utm.content": "utm_content",
1255
+ "referrer": "referrer",
1256
+ "event_source": "event_source",
1257
+ "event_subtype": "event_subtype",
1258
+ "page_path": "page_path",
1259
+ "target_url_path": "target_url_path",
1260
+ "event_name": "event_name",
1261
+ "type": "type"
1262
+ };
1263
+ const match = {};
1264
+ for (const [key, value] of Object.entries(filters)) {
1265
+ if (!value || !map[key]) continue;
1266
+ match[map[key]] = value;
1267
+ }
1268
+ return match;
1269
+ }
983
1270
  var MongoDBAdapter = class {
984
1271
  client;
985
1272
  db;
@@ -1015,6 +1302,13 @@ var MongoDBAdapter = class {
1015
1302
  title: e.title ?? null,
1016
1303
  event_name: e.name ?? null,
1017
1304
  properties: e.properties ?? null,
1305
+ event_source: e.eventSource ?? null,
1306
+ event_subtype: e.eventSubtype ?? null,
1307
+ page_path: e.pagePath ?? null,
1308
+ target_url_path: e.targetUrlPath ?? null,
1309
+ element_selector: e.elementSelector ?? null,
1310
+ element_text: e.elementText ?? null,
1311
+ scroll_depth_pct: e.scrollDepthPct ?? null,
1018
1312
  user_id: e.userId ?? null,
1019
1313
  traits: e.traits ?? null,
1020
1314
  country: e.geo?.country ?? null,
@@ -1045,12 +1339,13 @@ var MongoDBAdapter = class {
1045
1339
  site_id: siteId,
1046
1340
  timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
1047
1341
  };
1342
+ const filterMatch = buildFilterMatch(q.filters);
1048
1343
  let data = [];
1049
1344
  let total = 0;
1050
1345
  switch (q.metric) {
1051
1346
  case "pageviews": {
1052
1347
  const [result2] = await this.collection.aggregate([
1053
- { $match: { ...baseMatch, type: "pageview" } },
1348
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview" } },
1054
1349
  { $count: "count" }
1055
1350
  ]).toArray();
1056
1351
  total = result2?.count ?? 0;
@@ -1059,7 +1354,7 @@ var MongoDBAdapter = class {
1059
1354
  }
1060
1355
  case "visitors": {
1061
1356
  const [result2] = await this.collection.aggregate([
1062
- { $match: baseMatch },
1357
+ { $match: { ...baseMatch, ...filterMatch } },
1063
1358
  { $group: { _id: "$visitor_id" } },
1064
1359
  { $count: "count" }
1065
1360
  ]).toArray();
@@ -1069,7 +1364,7 @@ var MongoDBAdapter = class {
1069
1364
  }
1070
1365
  case "sessions": {
1071
1366
  const [result2] = await this.collection.aggregate([
1072
- { $match: baseMatch },
1367
+ { $match: { ...baseMatch, ...filterMatch } },
1073
1368
  { $group: { _id: "$session_id" } },
1074
1369
  { $count: "count" }
1075
1370
  ]).toArray();
@@ -1079,16 +1374,31 @@ var MongoDBAdapter = class {
1079
1374
  }
1080
1375
  case "events": {
1081
1376
  const [result2] = await this.collection.aggregate([
1082
- { $match: { ...baseMatch, type: "event" } },
1377
+ { $match: { ...baseMatch, ...filterMatch, type: "event" } },
1083
1378
  { $count: "count" }
1084
1379
  ]).toArray();
1085
1380
  total = result2?.count ?? 0;
1086
1381
  data = [{ key: "events", value: total }];
1087
1382
  break;
1088
1383
  }
1384
+ case "conversions": {
1385
+ const conversionEvents = q.conversionEvents ?? [];
1386
+ if (conversionEvents.length === 0) {
1387
+ total = 0;
1388
+ data = [{ key: "conversions", value: 0 }];
1389
+ break;
1390
+ }
1391
+ const [result2] = await this.collection.aggregate([
1392
+ { $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
1393
+ { $count: "count" }
1394
+ ]).toArray();
1395
+ total = result2?.count ?? 0;
1396
+ data = [{ key: "conversions", value: total }];
1397
+ break;
1398
+ }
1089
1399
  case "top_pages": {
1090
1400
  const rows = await this.collection.aggregate([
1091
- { $match: { ...baseMatch, type: "pageview", url: { $ne: null } } },
1401
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
1092
1402
  { $group: { _id: "$url", value: { $sum: 1 } } },
1093
1403
  { $sort: { value: -1 } },
1094
1404
  { $limit: limit }
@@ -1099,7 +1409,7 @@ var MongoDBAdapter = class {
1099
1409
  }
1100
1410
  case "top_referrers": {
1101
1411
  const rows = await this.collection.aggregate([
1102
- { $match: { ...baseMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
1412
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
1103
1413
  { $group: { _id: "$referrer", value: { $sum: 1 } } },
1104
1414
  { $sort: { value: -1 } },
1105
1415
  { $limit: limit }
@@ -1110,7 +1420,7 @@ var MongoDBAdapter = class {
1110
1420
  }
1111
1421
  case "top_countries": {
1112
1422
  const rows = await this.collection.aggregate([
1113
- { $match: { ...baseMatch, country: { $ne: null } } },
1423
+ { $match: { ...baseMatch, ...filterMatch, country: { $ne: null } } },
1114
1424
  { $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
1115
1425
  { $project: { _id: 1, value: { $size: "$value" } } },
1116
1426
  { $sort: { value: -1 } },
@@ -1122,7 +1432,7 @@ var MongoDBAdapter = class {
1122
1432
  }
1123
1433
  case "top_cities": {
1124
1434
  const rows = await this.collection.aggregate([
1125
- { $match: { ...baseMatch, city: { $ne: null } } },
1435
+ { $match: { ...baseMatch, ...filterMatch, city: { $ne: null } } },
1126
1436
  { $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
1127
1437
  { $project: { _id: 1, value: { $size: "$value" } } },
1128
1438
  { $sort: { value: -1 } },
@@ -1134,7 +1444,7 @@ var MongoDBAdapter = class {
1134
1444
  }
1135
1445
  case "top_events": {
1136
1446
  const rows = await this.collection.aggregate([
1137
- { $match: { ...baseMatch, type: "event", event_name: { $ne: null } } },
1447
+ { $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $ne: null } } },
1138
1448
  { $group: { _id: "$event_name", value: { $sum: 1 } } },
1139
1449
  { $sort: { value: -1 } },
1140
1450
  { $limit: limit }
@@ -1143,9 +1453,109 @@ var MongoDBAdapter = class {
1143
1453
  total = data.reduce((sum, d) => sum + d.value, 0);
1144
1454
  break;
1145
1455
  }
1456
+ case "top_conversions": {
1457
+ const conversionEvents = q.conversionEvents ?? [];
1458
+ if (conversionEvents.length === 0) {
1459
+ total = 0;
1460
+ data = [];
1461
+ break;
1462
+ }
1463
+ const rows = await this.collection.aggregate([
1464
+ { $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
1465
+ { $group: { _id: "$event_name", value: { $sum: 1 } } },
1466
+ { $sort: { value: -1 } },
1467
+ { $limit: limit }
1468
+ ]).toArray();
1469
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1470
+ total = data.reduce((sum, d) => sum + d.value, 0);
1471
+ break;
1472
+ }
1473
+ case "top_exit_pages": {
1474
+ const rows = await this.collection.aggregate([
1475
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
1476
+ { $sort: { timestamp: 1 } },
1477
+ { $group: { _id: "$session_id", url: { $last: "$url" } } },
1478
+ { $group: { _id: "$url", value: { $sum: 1 } } },
1479
+ { $sort: { value: -1 } },
1480
+ { $limit: limit }
1481
+ ]).toArray();
1482
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1483
+ total = data.reduce((sum, d) => sum + d.value, 0);
1484
+ break;
1485
+ }
1486
+ case "top_transitions": {
1487
+ const rows = await this.collection.aggregate([
1488
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
1489
+ {
1490
+ $setWindowFields: {
1491
+ partitionBy: "$session_id",
1492
+ sortBy: { timestamp: 1 },
1493
+ output: {
1494
+ prev_url: { $shift: { output: "$url", by: -1 } }
1495
+ }
1496
+ }
1497
+ },
1498
+ { $match: { prev_url: { $ne: null } } },
1499
+ { $group: { _id: { $concat: ["$prev_url", " \u2192 ", "$url"] }, value: { $sum: 1 } } },
1500
+ { $sort: { value: -1 } },
1501
+ { $limit: limit }
1502
+ ]).toArray();
1503
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1504
+ total = data.reduce((sum, d) => sum + d.value, 0);
1505
+ break;
1506
+ }
1507
+ case "top_scroll_pages": {
1508
+ const rows = await this.collection.aggregate([
1509
+ { $match: { ...baseMatch, ...filterMatch, type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } } },
1510
+ { $group: { _id: "$page_path", value: { $sum: 1 } } },
1511
+ { $sort: { value: -1 } },
1512
+ { $limit: limit }
1513
+ ]).toArray();
1514
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1515
+ total = data.reduce((sum, d) => sum + d.value, 0);
1516
+ break;
1517
+ }
1518
+ case "top_button_clicks": {
1519
+ const rows = await this.collection.aggregate([
1520
+ {
1521
+ $match: {
1522
+ ...baseMatch,
1523
+ ...filterMatch,
1524
+ type: "event",
1525
+ event_subtype: "button_click",
1526
+ $or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
1527
+ }
1528
+ },
1529
+ { $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
1530
+ { $sort: { value: -1 } },
1531
+ { $limit: limit }
1532
+ ]).toArray();
1533
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1534
+ total = data.reduce((sum, d) => sum + d.value, 0);
1535
+ break;
1536
+ }
1537
+ case "top_link_targets": {
1538
+ const rows = await this.collection.aggregate([
1539
+ {
1540
+ $match: {
1541
+ ...baseMatch,
1542
+ ...filterMatch,
1543
+ type: "event",
1544
+ event_subtype: { $in: ["link_click", "outbound_click"] },
1545
+ target_url_path: { $ne: null }
1546
+ }
1547
+ },
1548
+ { $group: { _id: "$target_url_path", value: { $sum: 1 } } },
1549
+ { $sort: { value: -1 } },
1550
+ { $limit: limit }
1551
+ ]).toArray();
1552
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1553
+ total = data.reduce((sum, d) => sum + d.value, 0);
1554
+ break;
1555
+ }
1146
1556
  case "top_devices": {
1147
1557
  const rows = await this.collection.aggregate([
1148
- { $match: { ...baseMatch, device_type: { $ne: null } } },
1558
+ { $match: { ...baseMatch, ...filterMatch, device_type: { $ne: null } } },
1149
1559
  { $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
1150
1560
  { $project: { _id: 1, value: { $size: "$value" } } },
1151
1561
  { $sort: { value: -1 } },
@@ -1157,7 +1567,7 @@ var MongoDBAdapter = class {
1157
1567
  }
1158
1568
  case "top_browsers": {
1159
1569
  const rows = await this.collection.aggregate([
1160
- { $match: { ...baseMatch, browser: { $ne: null } } },
1570
+ { $match: { ...baseMatch, ...filterMatch, browser: { $ne: null } } },
1161
1571
  { $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
1162
1572
  { $project: { _id: 1, value: { $size: "$value" } } },
1163
1573
  { $sort: { value: -1 } },
@@ -1169,7 +1579,7 @@ var MongoDBAdapter = class {
1169
1579
  }
1170
1580
  case "top_os": {
1171
1581
  const rows = await this.collection.aggregate([
1172
- { $match: { ...baseMatch, os: { $ne: null } } },
1582
+ { $match: { ...baseMatch, ...filterMatch, os: { $ne: null } } },
1173
1583
  { $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
1174
1584
  { $project: { _id: 1, value: { $size: "$value" } } },
1175
1585
  { $sort: { value: -1 } },
@@ -1181,7 +1591,7 @@ var MongoDBAdapter = class {
1181
1591
  }
1182
1592
  }
1183
1593
  const result = { metric: q.metric, period, data, total };
1184
- if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
1594
+ if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
1185
1595
  const prevRange = previousPeriodRange(dateRange);
1186
1596
  const prevResult = await this.query({
1187
1597
  ...q,
@@ -1213,15 +1623,34 @@ var MongoDBAdapter = class {
1213
1623
  site_id: params.siteId,
1214
1624
  timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
1215
1625
  };
1626
+ const filterMatch = buildFilterMatch(params.filters);
1216
1627
  if (params.metric === "pageviews") {
1217
1628
  baseMatch.type = "pageview";
1218
1629
  }
1630
+ if (params.metric === "events") {
1631
+ baseMatch.type = "event";
1632
+ }
1633
+ if (params.metric === "conversions") {
1634
+ baseMatch.type = "event";
1635
+ const conversionEvents = params.conversionEvents ?? [];
1636
+ if (conversionEvents.length === 0) {
1637
+ const data2 = fillBuckets(
1638
+ new Date(dateRange.from),
1639
+ new Date(dateRange.to),
1640
+ granularity,
1641
+ granularityToDateFormat(granularity),
1642
+ []
1643
+ );
1644
+ return { metric: params.metric, granularity, data: data2 };
1645
+ }
1646
+ baseMatch.event_name = { $in: conversionEvents };
1647
+ }
1219
1648
  const dateFormat = granularityToDateFormat(granularity);
1220
1649
  let pipeline;
1221
1650
  if (params.metric === "visitors" || params.metric === "sessions") {
1222
1651
  const groupField = params.metric === "visitors" ? "$visitor_id" : "$session_id";
1223
1652
  pipeline = [
1224
- { $match: baseMatch },
1653
+ { $match: { ...baseMatch, ...filterMatch } },
1225
1654
  {
1226
1655
  $group: {
1227
1656
  _id: {
@@ -1240,7 +1669,7 @@ var MongoDBAdapter = class {
1240
1669
  ];
1241
1670
  } else {
1242
1671
  pipeline = [
1243
- { $match: baseMatch },
1672
+ { $match: { ...baseMatch, ...filterMatch } },
1244
1673
  {
1245
1674
  $group: {
1246
1675
  _id: { $dateToString: { format: dateFormat, date: "$timestamp" } },
@@ -1321,7 +1750,12 @@ var MongoDBAdapter = class {
1321
1750
  const offset = params.offset ?? 0;
1322
1751
  const match = { site_id: params.siteId };
1323
1752
  if (params.type) match.type = params.type;
1324
- if (params.eventName) match.event_name = params.eventName;
1753
+ if (params.eventName) {
1754
+ match.event_name = params.eventName;
1755
+ } else if (params.eventNames && params.eventNames.length > 0) {
1756
+ match.event_name = { $in: params.eventNames };
1757
+ }
1758
+ if (params.eventSource) match.event_source = params.eventSource;
1325
1759
  if (params.visitorId) match.visitor_id = params.visitorId;
1326
1760
  if (params.userId) match.user_id = params.userId;
1327
1761
  if (params.period || params.dateFrom) {
@@ -1356,6 +1790,7 @@ var MongoDBAdapter = class {
1356
1790
  }
1357
1791
  const pipeline = [
1358
1792
  { $match: match },
1793
+ { $sort: { timestamp: 1 } },
1359
1794
  {
1360
1795
  $group: {
1361
1796
  _id: "$visitor_id",
@@ -1367,13 +1802,22 @@ var MongoDBAdapter = class {
1367
1802
  totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
1368
1803
  sessions: { $addToSet: "$session_id" },
1369
1804
  lastUrl: { $last: "$url" },
1805
+ referrer: { $last: "$referrer" },
1370
1806
  device_type: { $last: "$device_type" },
1371
1807
  browser: { $last: "$browser" },
1372
1808
  os: { $last: "$os" },
1373
1809
  country: { $last: "$country" },
1374
1810
  city: { $last: "$city" },
1375
1811
  region: { $last: "$region" },
1376
- language: { $last: "$language" }
1812
+ language: { $last: "$language" },
1813
+ timezone: { $last: "$timezone" },
1814
+ screen_width: { $last: "$screen_width" },
1815
+ screen_height: { $last: "$screen_height" },
1816
+ utm_source: { $last: "$utm_source" },
1817
+ utm_medium: { $last: "$utm_medium" },
1818
+ utm_campaign: { $last: "$utm_campaign" },
1819
+ utm_term: { $last: "$utm_term" },
1820
+ utm_content: { $last: "$utm_content" }
1377
1821
  }
1378
1822
  },
1379
1823
  { $sort: { lastSeen: -1 } },
@@ -1395,9 +1839,19 @@ var MongoDBAdapter = class {
1395
1839
  totalPageviews: u.totalPageviews,
1396
1840
  totalSessions: u.sessions.length,
1397
1841
  lastUrl: u.lastUrl ?? void 0,
1842
+ referrer: u.referrer ?? void 0,
1398
1843
  device: u.device_type ? { type: u.device_type, browser: u.browser ?? "", os: u.os ?? "" } : void 0,
1399
1844
  geo: u.country ? { country: u.country, city: u.city ?? void 0, region: u.region ?? void 0 } : void 0,
1400
- language: u.language ?? void 0
1845
+ language: u.language ?? void 0,
1846
+ timezone: u.timezone ?? void 0,
1847
+ screen: u.screen_width || u.screen_height ? { width: u.screen_width ?? 0, height: u.screen_height ?? 0 } : void 0,
1848
+ utm: u.utm_source ? {
1849
+ source: u.utm_source ?? void 0,
1850
+ medium: u.utm_medium ?? void 0,
1851
+ campaign: u.utm_campaign ?? void 0,
1852
+ term: u.utm_term ?? void 0,
1853
+ content: u.utm_content ?? void 0
1854
+ } : void 0
1401
1855
  }));
1402
1856
  return {
1403
1857
  users,
@@ -1426,6 +1880,13 @@ var MongoDBAdapter = class {
1426
1880
  title: doc.title ?? void 0,
1427
1881
  name: doc.event_name ?? void 0,
1428
1882
  properties: doc.properties ?? void 0,
1883
+ eventSource: doc.event_source ? doc.event_source : void 0,
1884
+ eventSubtype: doc.event_subtype ? doc.event_subtype : void 0,
1885
+ pagePath: doc.page_path ?? void 0,
1886
+ targetUrlPath: doc.target_url_path ?? void 0,
1887
+ elementSelector: doc.element_selector ?? void 0,
1888
+ elementText: doc.element_text ?? void 0,
1889
+ scrollDepthPct: doc.scroll_depth_pct ?? void 0,
1429
1890
  userId: doc.user_id ?? void 0,
1430
1891
  traits: doc.traits ?? void 0,
1431
1892
  geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
@@ -1449,6 +1910,7 @@ var MongoDBAdapter = class {
1449
1910
  name: data.name,
1450
1911
  domain: data.domain ?? null,
1451
1912
  allowed_origins: data.allowedOrigins ?? null,
1913
+ conversion_events: data.conversionEvents ?? null,
1452
1914
  created_at: now,
1453
1915
  updated_at: now
1454
1916
  };
@@ -1472,6 +1934,7 @@ var MongoDBAdapter = class {
1472
1934
  if (data.name !== void 0) updates.name = data.name;
1473
1935
  if (data.domain !== void 0) updates.domain = data.domain || null;
1474
1936
  if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
1937
+ if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
1475
1938
  const result = await this.sites.findOneAndUpdate(
1476
1939
  { site_id: siteId },
1477
1940
  { $set: updates },
@@ -1502,6 +1965,7 @@ var MongoDBAdapter = class {
1502
1965
  name: doc.name,
1503
1966
  domain: doc.domain ?? void 0,
1504
1967
  allowedOrigins: doc.allowed_origins ?? void 0,
1968
+ conversionEvents: doc.conversion_events ?? void 0,
1505
1969
  createdAt: doc.created_at.toISOString(),
1506
1970
  updatedAt: doc.updated_at.toISOString()
1507
1971
  };
@@ -1755,6 +2219,7 @@ async function createCollector(config) {
1755
2219
  if (allowed) {
1756
2220
  res.setHeader?.("Access-Control-Allow-Origin", origin || "*");
1757
2221
  res.setHeader?.("Access-Control-Allow-Methods", methods);
2222
+ res.setHeader?.("Access-Control-Allow-Credentials", "true");
1758
2223
  const headers = ["Content-Type", extraHeaders].filter(Boolean).join(", ");
1759
2224
  res.setHeader?.("Access-Control-Allow-Headers", headers);
1760
2225
  }
@@ -1785,7 +2250,14 @@ async function createCollector(config) {
1785
2250
  }
1786
2251
  function handler() {
1787
2252
  return async (req, res) => {
1788
- if (setCors(req, res, "POST, OPTIONS")) return;
2253
+ res.setHeader?.("Access-Control-Allow-Origin", "*");
2254
+ res.setHeader?.("Access-Control-Allow-Methods", "POST, OPTIONS");
2255
+ res.setHeader?.("Access-Control-Allow-Headers", "Content-Type");
2256
+ if (req.method === "OPTIONS") {
2257
+ res.writeHead?.(204);
2258
+ res.end?.();
2259
+ return;
2260
+ }
1789
2261
  if (req.method !== "POST") {
1790
2262
  sendJson(res, 405, { ok: false, error: "Method not allowed" });
1791
2263
  return;
@@ -1864,8 +2336,13 @@ async function createCollector(config) {
1864
2336
  period: params.period,
1865
2337
  dateFrom: params.dateFrom,
1866
2338
  dateTo: params.dateTo,
1867
- granularity: q.granularity
2339
+ granularity: q.granularity,
2340
+ filters: q.filters ? JSON.parse(q.filters) : void 0
1868
2341
  };
2342
+ if (tsParams.metric === "conversions") {
2343
+ const site = await db.getSite(params.siteId);
2344
+ tsParams.conversionEvents = site?.conversionEvents ?? [];
2345
+ }
1869
2346
  const result2 = await db.queryTimeSeries(tsParams);
1870
2347
  sendJson(res, 200, result2);
1871
2348
  return;
@@ -1881,7 +2358,15 @@ async function createCollector(config) {
1881
2358
  sendJson(res, 200, result2);
1882
2359
  return;
1883
2360
  }
1884
- const result = await db.query(params);
2361
+ const isConversionMetric = params.metric === "conversions" || params.metric === "top_conversions";
2362
+ let result;
2363
+ if (isConversionMetric) {
2364
+ const site = await db.getSite(params.siteId);
2365
+ const conversionEvents = site?.conversionEvents ?? [];
2366
+ result = await db.query({ ...params, conversionEvents });
2367
+ } else {
2368
+ result = await db.query(params);
2369
+ }
1885
2370
  sendJson(res, 200, result);
1886
2371
  } catch (err) {
1887
2372
  sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : "Internal error" });
@@ -1978,10 +2463,13 @@ async function createCollector(config) {
1978
2463
  sendJson(res, 401, { ok: false, error: "Invalid or missing secret key" });
1979
2464
  return;
1980
2465
  }
2466
+ const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1981
2467
  const params = {
1982
2468
  siteId: q.siteId,
1983
2469
  type: q.type,
1984
2470
  eventName: q.eventName,
2471
+ eventNames,
2472
+ eventSource: q.eventSource,
1985
2473
  visitorId: q.visitorId,
1986
2474
  userId: q.userId,
1987
2475
  period: q.period,
@@ -2021,9 +2509,13 @@ async function createCollector(config) {
2021
2509
  const visitorId = usersIdx >= 0 ? pathSegments[usersIdx + 1] : void 0;
2022
2510
  const action = usersIdx >= 0 ? pathSegments[usersIdx + 2] : void 0;
2023
2511
  if (visitorId && action === "events") {
2512
+ const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2024
2513
  const params2 = {
2025
2514
  siteId: q.siteId,
2026
2515
  type: q.type,
2516
+ eventName: q.eventName,
2517
+ eventNames,
2518
+ eventSource: q.eventSource,
2027
2519
  period: q.period,
2028
2520
  dateFrom: q.dateFrom,
2029
2521
  dateTo: q.dateTo,
@@ -2130,7 +2622,8 @@ function createAdapter(config) {
2130
2622
  }
2131
2623
  }
2132
2624
  async function parseBody(req) {
2133
- if (req.body) return req.body;
2625
+ if (req.body && typeof req.body === "object") return req.body;
2626
+ if (typeof req.body === "string") return JSON.parse(req.body);
2134
2627
  return new Promise((resolve, reject) => {
2135
2628
  let data = "";
2136
2629
  req.on("data", (chunk) => {