@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.cjs CHANGED
@@ -192,6 +192,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
192
192
  title Nullable(String),
193
193
  event_name Nullable(String),
194
194
  properties Nullable(String),
195
+ event_source LowCardinality(Nullable(String)),
196
+ event_subtype LowCardinality(Nullable(String)),
197
+ page_path Nullable(String),
198
+ target_url_path Nullable(String),
199
+ element_selector Nullable(String),
200
+ element_text Nullable(String),
201
+ scroll_depth_pct Nullable(UInt8),
195
202
  user_id Nullable(String),
196
203
  traits Nullable(String),
197
204
  country LowCardinality(Nullable(String)),
@@ -223,6 +230,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
223
230
  name String,
224
231
  domain Nullable(String),
225
232
  allowed_origins Nullable(String),
233
+ conversion_events Nullable(String),
226
234
  created_at DateTime64(3),
227
235
  updated_at DateTime64(3),
228
236
  version UInt64,
@@ -235,6 +243,39 @@ function toCHDateTime(d) {
235
243
  const iso = typeof d === "string" ? d : d.toISOString();
236
244
  return iso.replace("T", " ").replace("Z", "");
237
245
  }
246
+ function buildFilterConditions(filters) {
247
+ if (!filters) return { conditions: [], params: {} };
248
+ const map = {
249
+ "geo.country": "country",
250
+ "geo.city": "city",
251
+ "geo.region": "region",
252
+ "language": "language",
253
+ "device.type": "device_type",
254
+ "device.browser": "browser",
255
+ "device.os": "os",
256
+ "utm.source": "utm_source",
257
+ "utm.medium": "utm_medium",
258
+ "utm.campaign": "utm_campaign",
259
+ "utm.term": "utm_term",
260
+ "utm.content": "utm_content",
261
+ "referrer": "referrer",
262
+ "event_source": "event_source",
263
+ "event_subtype": "event_subtype",
264
+ "page_path": "page_path",
265
+ "target_url_path": "target_url_path",
266
+ "event_name": "event_name",
267
+ "type": "type"
268
+ };
269
+ const conditions = [];
270
+ const params = {};
271
+ for (const [key, value] of Object.entries(filters)) {
272
+ if (!value || !map[key]) continue;
273
+ const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
274
+ conditions.push(`${map[key]} = {${paramKey}:String}`);
275
+ params[paramKey] = value;
276
+ }
277
+ return { conditions, params };
278
+ }
238
279
  var ClickHouseAdapter = class {
239
280
  client;
240
281
  constructor(url) {
@@ -248,6 +289,16 @@ var ClickHouseAdapter = class {
248
289
  async init() {
249
290
  await this.client.command({ query: CREATE_EVENTS_TABLE });
250
291
  await this.client.command({ query: CREATE_SITES_TABLE });
292
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_source LowCardinality(Nullable(String))` });
293
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_subtype LowCardinality(Nullable(String))` });
294
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS page_path Nullable(String)` });
295
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS target_url_path Nullable(String)` });
296
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_selector Nullable(String)` });
297
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_text Nullable(String)` });
298
+ await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS scroll_depth_pct Nullable(UInt8)` });
299
+ await this.client.command({
300
+ query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
301
+ });
251
302
  }
252
303
  async close() {
253
304
  await this.client.close();
@@ -266,6 +317,13 @@ var ClickHouseAdapter = class {
266
317
  title: e.title ?? null,
267
318
  event_name: e.name ?? null,
268
319
  properties: e.properties ? JSON.stringify(e.properties) : null,
320
+ event_source: e.eventSource ?? null,
321
+ event_subtype: e.eventSubtype ?? null,
322
+ page_path: e.pagePath ?? null,
323
+ target_url_path: e.targetUrlPath ?? null,
324
+ element_selector: e.elementSelector ?? null,
325
+ element_text: e.elementText ?? null,
326
+ scroll_depth_pct: e.scrollDepthPct ?? null,
269
327
  user_id: e.userId ?? null,
270
328
  traits: e.traits ? JSON.stringify(e.traits) : null,
271
329
  country: e.geo?.country ?? null,
@@ -302,6 +360,8 @@ var ClickHouseAdapter = class {
302
360
  to: toCHDateTime(dateRange.to),
303
361
  limit
304
362
  };
363
+ const filter = buildFilterConditions(q.filters);
364
+ const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
305
365
  let data = [];
306
366
  let total = 0;
307
367
  switch (q.metric) {
@@ -311,8 +371,8 @@ var ClickHouseAdapter = class {
311
371
  WHERE site_id = {siteId:String}
312
372
  AND timestamp >= {from:String}
313
373
  AND timestamp <= {to:String}
314
- AND type = 'pageview'`,
315
- params
374
+ AND type = 'pageview'${filterSql}`,
375
+ { ...params, ...filter.params }
316
376
  );
317
377
  total = Number(rows[0]?.value ?? 0);
318
378
  data = [{ key: "pageviews", value: total }];
@@ -323,8 +383,8 @@ var ClickHouseAdapter = class {
323
383
  `SELECT uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
324
384
  WHERE site_id = {siteId:String}
325
385
  AND timestamp >= {from:String}
326
- AND timestamp <= {to:String}`,
327
- params
386
+ AND timestamp <= {to:String}${filterSql}`,
387
+ { ...params, ...filter.params }
328
388
  );
329
389
  total = Number(rows[0]?.value ?? 0);
330
390
  data = [{ key: "visitors", value: total }];
@@ -335,8 +395,8 @@ var ClickHouseAdapter = class {
335
395
  `SELECT uniq(session_id) AS value FROM ${EVENTS_TABLE}
336
396
  WHERE site_id = {siteId:String}
337
397
  AND timestamp >= {from:String}
338
- AND timestamp <= {to:String}`,
339
- params
398
+ AND timestamp <= {to:String}${filterSql}`,
399
+ { ...params, ...filter.params }
340
400
  );
341
401
  total = Number(rows[0]?.value ?? 0);
342
402
  data = [{ key: "sessions", value: total }];
@@ -348,13 +408,33 @@ var ClickHouseAdapter = class {
348
408
  WHERE site_id = {siteId:String}
349
409
  AND timestamp >= {from:String}
350
410
  AND timestamp <= {to:String}
351
- AND type = 'event'`,
352
- params
411
+ AND type = 'event'${filterSql}`,
412
+ { ...params, ...filter.params }
353
413
  );
354
414
  total = Number(rows[0]?.value ?? 0);
355
415
  data = [{ key: "events", value: total }];
356
416
  break;
357
417
  }
418
+ case "conversions": {
419
+ const conversionEvents = q.conversionEvents ?? [];
420
+ if (conversionEvents.length === 0) {
421
+ total = 0;
422
+ data = [{ key: "conversions", value: 0 }];
423
+ break;
424
+ }
425
+ const rows = await this.queryRows(
426
+ `SELECT count() AS value FROM ${EVENTS_TABLE}
427
+ WHERE site_id = {siteId:String}
428
+ AND timestamp >= {from:String}
429
+ AND timestamp <= {to:String}
430
+ AND type = 'event'
431
+ AND event_name IN {eventNames:Array(String)}${filterSql}`,
432
+ { ...params, eventNames: conversionEvents, ...filter.params }
433
+ );
434
+ total = Number(rows[0]?.value ?? 0);
435
+ data = [{ key: "conversions", value: total }];
436
+ break;
437
+ }
358
438
  case "top_pages": {
359
439
  const rows = await this.queryRows(
360
440
  `SELECT url AS key, count() AS value FROM ${EVENTS_TABLE}
@@ -362,11 +442,11 @@ var ClickHouseAdapter = class {
362
442
  AND timestamp >= {from:String}
363
443
  AND timestamp <= {to:String}
364
444
  AND type = 'pageview'
365
- AND url IS NOT NULL
445
+ AND url IS NOT NULL${filterSql}
366
446
  GROUP BY url
367
447
  ORDER BY value DESC
368
448
  LIMIT {limit:UInt32}`,
369
- params
449
+ { ...params, ...filter.params }
370
450
  );
371
451
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
372
452
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -380,11 +460,11 @@ var ClickHouseAdapter = class {
380
460
  AND timestamp <= {to:String}
381
461
  AND type = 'pageview'
382
462
  AND referrer IS NOT NULL
383
- AND referrer != ''
463
+ AND referrer != ''${filterSql}
384
464
  GROUP BY referrer
385
465
  ORDER BY value DESC
386
466
  LIMIT {limit:UInt32}`,
387
- params
467
+ { ...params, ...filter.params }
388
468
  );
389
469
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
390
470
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -396,11 +476,11 @@ var ClickHouseAdapter = class {
396
476
  WHERE site_id = {siteId:String}
397
477
  AND timestamp >= {from:String}
398
478
  AND timestamp <= {to:String}
399
- AND country IS NOT NULL
479
+ AND country IS NOT NULL${filterSql}
400
480
  GROUP BY country
401
481
  ORDER BY value DESC
402
482
  LIMIT {limit:UInt32}`,
403
- params
483
+ { ...params, ...filter.params }
404
484
  );
405
485
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
406
486
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -412,11 +492,11 @@ var ClickHouseAdapter = class {
412
492
  WHERE site_id = {siteId:String}
413
493
  AND timestamp >= {from:String}
414
494
  AND timestamp <= {to:String}
415
- AND city IS NOT NULL
495
+ AND city IS NOT NULL${filterSql}
416
496
  GROUP BY city
417
497
  ORDER BY value DESC
418
498
  LIMIT {limit:UInt32}`,
419
- params
499
+ { ...params, ...filter.params }
420
500
  );
421
501
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
422
502
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -430,10 +510,132 @@ var ClickHouseAdapter = class {
430
510
  AND timestamp <= {to:String}
431
511
  AND type = 'event'
432
512
  AND event_name IS NOT NULL
513
+ ${filterSql}
433
514
  GROUP BY event_name
434
515
  ORDER BY value DESC
435
516
  LIMIT {limit:UInt32}`,
436
- params
517
+ { ...params, ...filter.params }
518
+ );
519
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
520
+ total = data.reduce((sum, d) => sum + d.value, 0);
521
+ break;
522
+ }
523
+ case "top_conversions": {
524
+ const conversionEvents = q.conversionEvents ?? [];
525
+ if (conversionEvents.length === 0) {
526
+ total = 0;
527
+ data = [];
528
+ break;
529
+ }
530
+ const rows = await this.queryRows(
531
+ `SELECT event_name AS key, count() AS value FROM ${EVENTS_TABLE}
532
+ WHERE site_id = {siteId:String}
533
+ AND timestamp >= {from:String}
534
+ AND timestamp <= {to:String}
535
+ AND type = 'event'
536
+ AND event_name IN {eventNames:Array(String)}
537
+ ${filterSql}
538
+ GROUP BY event_name
539
+ ORDER BY value DESC
540
+ LIMIT {limit:UInt32}`,
541
+ { ...params, eventNames: conversionEvents, ...filter.params }
542
+ );
543
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
544
+ total = data.reduce((sum, d) => sum + d.value, 0);
545
+ break;
546
+ }
547
+ case "top_exit_pages": {
548
+ const rows = await this.queryRows(
549
+ `SELECT url AS key, count() AS value FROM (
550
+ SELECT session_id, argMax(url, timestamp) AS url
551
+ FROM ${EVENTS_TABLE}
552
+ WHERE site_id = {siteId:String}
553
+ AND timestamp >= {from:String}
554
+ AND timestamp <= {to:String}
555
+ AND type = 'pageview'
556
+ AND url IS NOT NULL${filterSql}
557
+ GROUP BY session_id
558
+ )
559
+ GROUP BY url
560
+ ORDER BY value DESC
561
+ LIMIT {limit:UInt32}`,
562
+ { ...params, ...filter.params }
563
+ );
564
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
565
+ total = data.reduce((sum, d) => sum + d.value, 0);
566
+ break;
567
+ }
568
+ case "top_transitions": {
569
+ const rows = await this.queryRows(
570
+ `SELECT concat(prev_url, ' \u2192 ', url) AS key, count() AS value FROM (
571
+ SELECT session_id, url,
572
+ lag(url) OVER (PARTITION BY session_id ORDER BY timestamp) AS prev_url
573
+ FROM ${EVENTS_TABLE}
574
+ WHERE site_id = {siteId:String}
575
+ AND timestamp >= {from:String}
576
+ AND timestamp <= {to:String}
577
+ AND type = 'pageview'
578
+ AND url IS NOT NULL${filterSql}
579
+ )
580
+ WHERE prev_url IS NOT NULL
581
+ GROUP BY key
582
+ ORDER BY value DESC
583
+ LIMIT {limit:UInt32}`,
584
+ { ...params, ...filter.params }
585
+ );
586
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
587
+ total = data.reduce((sum, d) => sum + d.value, 0);
588
+ break;
589
+ }
590
+ case "top_scroll_pages": {
591
+ const rows = await this.queryRows(
592
+ `SELECT page_path AS key, count() AS value FROM ${EVENTS_TABLE}
593
+ WHERE site_id = {siteId:String}
594
+ AND timestamp >= {from:String}
595
+ AND timestamp <= {to:String}
596
+ AND type = 'event'
597
+ AND event_subtype = 'scroll_depth'
598
+ AND page_path IS NOT NULL${filterSql}
599
+ GROUP BY page_path
600
+ ORDER BY value DESC
601
+ LIMIT {limit:UInt32}`,
602
+ { ...params, ...filter.params }
603
+ );
604
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
605
+ total = data.reduce((sum, d) => sum + d.value, 0);
606
+ break;
607
+ }
608
+ case "top_button_clicks": {
609
+ const rows = await this.queryRows(
610
+ `SELECT ifNull(element_text, element_selector) AS key, count() AS value FROM ${EVENTS_TABLE}
611
+ WHERE site_id = {siteId:String}
612
+ AND timestamp >= {from:String}
613
+ AND timestamp <= {to:String}
614
+ AND type = 'event'
615
+ AND event_subtype = 'button_click'
616
+ AND (element_text IS NOT NULL OR element_selector IS NOT NULL)${filterSql}
617
+ GROUP BY key
618
+ ORDER BY value DESC
619
+ LIMIT {limit:UInt32}`,
620
+ { ...params, ...filter.params }
621
+ );
622
+ data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
623
+ total = data.reduce((sum, d) => sum + d.value, 0);
624
+ break;
625
+ }
626
+ case "top_link_targets": {
627
+ const rows = await this.queryRows(
628
+ `SELECT target_url_path AS key, count() AS value FROM ${EVENTS_TABLE}
629
+ WHERE site_id = {siteId:String}
630
+ AND timestamp >= {from:String}
631
+ AND timestamp <= {to:String}
632
+ AND type = 'event'
633
+ AND event_subtype IN ('link_click','outbound_click')
634
+ AND target_url_path IS NOT NULL${filterSql}
635
+ GROUP BY target_url_path
636
+ ORDER BY value DESC
637
+ LIMIT {limit:UInt32}`,
638
+ { ...params, ...filter.params }
437
639
  );
438
640
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
439
641
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -446,10 +648,11 @@ var ClickHouseAdapter = class {
446
648
  AND timestamp >= {from:String}
447
649
  AND timestamp <= {to:String}
448
650
  AND device_type IS NOT NULL
651
+ ${filterSql}
449
652
  GROUP BY device_type
450
653
  ORDER BY value DESC
451
654
  LIMIT {limit:UInt32}`,
452
- params
655
+ { ...params, ...filter.params }
453
656
  );
454
657
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
455
658
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -462,10 +665,11 @@ var ClickHouseAdapter = class {
462
665
  AND timestamp >= {from:String}
463
666
  AND timestamp <= {to:String}
464
667
  AND browser IS NOT NULL
668
+ ${filterSql}
465
669
  GROUP BY browser
466
670
  ORDER BY value DESC
467
671
  LIMIT {limit:UInt32}`,
468
- params
672
+ { ...params, ...filter.params }
469
673
  );
470
674
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
471
675
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -478,10 +682,11 @@ var ClickHouseAdapter = class {
478
682
  AND timestamp >= {from:String}
479
683
  AND timestamp <= {to:String}
480
684
  AND os IS NOT NULL
685
+ ${filterSql}
481
686
  GROUP BY os
482
687
  ORDER BY value DESC
483
688
  LIMIT {limit:UInt32}`,
484
- params
689
+ { ...params, ...filter.params }
485
690
  );
486
691
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
487
692
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -489,7 +694,7 @@ var ClickHouseAdapter = class {
489
694
  }
490
695
  }
491
696
  const result = { metric: q.metric, period, data, total };
492
- if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
697
+ if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
493
698
  const prevRange = previousPeriodRange(dateRange);
494
699
  const prevResult = await this.query({
495
700
  ...q,
@@ -519,7 +724,12 @@ var ClickHouseAdapter = class {
519
724
  const granularity = params.granularity ?? autoGranularity(period);
520
725
  const bucketFn = this.granularityToClickHouseFunc(granularity);
521
726
  const dateFormat = granularityToDateFormat(granularity);
727
+ const filter = buildFilterConditions(params.filters);
728
+ const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
522
729
  const typeFilter = params.metric === "pageviews" ? `AND type = 'pageview'` : "";
730
+ const eventsFilter = params.metric === "events" ? `AND type = 'event'` : "";
731
+ const conversionsFilter = params.metric === "conversions" ? `AND type = 'event' AND event_name IN {eventNames:Array(String)}` : "";
732
+ const extraFilters = [typeFilter, eventsFilter, conversionsFilter, filterSql].filter(Boolean).join(" ");
523
733
  let sql;
524
734
  if (params.metric === "visitors" || params.metric === "sessions") {
525
735
  const field = params.metric === "visitors" ? "visitor_id" : "session_id";
@@ -529,7 +739,7 @@ var ClickHouseAdapter = class {
529
739
  WHERE site_id = {siteId:String}
530
740
  AND timestamp >= {from:String}
531
741
  AND timestamp <= {to:String}
532
- ${typeFilter}
742
+ ${extraFilters}
533
743
  GROUP BY bucket
534
744
  ORDER BY bucket ASC
535
745
  `;
@@ -540,7 +750,7 @@ var ClickHouseAdapter = class {
540
750
  WHERE site_id = {siteId:String}
541
751
  AND timestamp >= {from:String}
542
752
  AND timestamp <= {to:String}
543
- ${typeFilter}
753
+ ${extraFilters}
544
754
  GROUP BY bucket
545
755
  ORDER BY bucket ASC
546
756
  `;
@@ -548,7 +758,9 @@ var ClickHouseAdapter = class {
548
758
  const rows = await this.queryRows(sql, {
549
759
  siteId: params.siteId,
550
760
  from: toCHDateTime(dateRange.from),
551
- to: toCHDateTime(dateRange.to)
761
+ to: toCHDateTime(dateRange.to),
762
+ eventNames: params.conversionEvents ?? [],
763
+ ...filter.params
552
764
  });
553
765
  const mappedRows = rows.map((r) => ({
554
766
  _id: this.convertClickHouseBucket(r.bucket, granularity),
@@ -666,6 +878,14 @@ var ClickHouseAdapter = class {
666
878
  conditions.push(`event_name = {eventName:String}`);
667
879
  queryParams.eventName = params.eventName;
668
880
  }
881
+ if (params.eventSource) {
882
+ conditions.push(`event_source = {eventSource:String}`);
883
+ queryParams.eventSource = params.eventSource;
884
+ }
885
+ if (params.eventNames && params.eventNames.length > 0) {
886
+ conditions.push(`event_name IN {eventNames:Array(String)}`);
887
+ queryParams.eventNames = params.eventNames;
888
+ }
669
889
  if (params.visitorId) {
670
890
  conditions.push(`visitor_id = {visitorId:String}`);
671
891
  queryParams.visitorId = params.visitorId;
@@ -688,7 +908,9 @@ var ClickHouseAdapter = class {
688
908
  const [events, countRows] = await Promise.all([
689
909
  this.queryRows(
690
910
  `SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
691
- event_name, properties, user_id, traits, country, city, region,
911
+ event_name, properties, event_source, event_subtype, page_path, target_url_path,
912
+ element_selector, element_text, scroll_depth_pct,
913
+ user_id, traits, country, city, region,
692
914
  device_type, browser, os, language,
693
915
  utm_source, utm_medium, utm_campaign, utm_term, utm_content
694
916
  FROM ${EVENTS_TABLE}
@@ -733,13 +955,22 @@ var ClickHouseAdapter = class {
733
955
  countIf(type = 'pageview') AS totalPageviews,
734
956
  uniq(session_id) AS totalSessions,
735
957
  anyLast(url) AS lastUrl,
958
+ anyLast(referrer) AS referrer,
736
959
  anyLast(device_type) AS device_type,
737
960
  anyLast(browser) AS browser,
738
961
  anyLast(os) AS os,
739
962
  anyLast(country) AS country,
740
963
  anyLast(city) AS city,
741
964
  anyLast(region) AS region,
742
- anyLast(language) AS language
965
+ anyLast(language) AS language,
966
+ anyLast(timezone) AS timezone,
967
+ anyLast(screen_width) AS screen_width,
968
+ anyLast(screen_height) AS screen_height,
969
+ anyLast(utm_source) AS utm_source,
970
+ anyLast(utm_medium) AS utm_medium,
971
+ anyLast(utm_campaign) AS utm_campaign,
972
+ anyLast(utm_term) AS utm_term,
973
+ anyLast(utm_content) AS utm_content
743
974
  FROM ${EVENTS_TABLE}
744
975
  WHERE ${where}
745
976
  GROUP BY visitor_id
@@ -767,9 +998,19 @@ var ClickHouseAdapter = class {
767
998
  totalPageviews: Number(u.totalPageviews),
768
999
  totalSessions: Number(u.totalSessions),
769
1000
  lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
1001
+ referrer: u.referrer ? String(u.referrer) : void 0,
770
1002
  device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
771
1003
  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,
772
- language: u.language ? String(u.language) : void 0
1004
+ language: u.language ? String(u.language) : void 0,
1005
+ timezone: u.timezone ? String(u.timezone) : void 0,
1006
+ screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
1007
+ utm: u.utm_source ? {
1008
+ source: String(u.utm_source),
1009
+ medium: u.utm_medium ? String(u.utm_medium) : void 0,
1010
+ campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
1011
+ term: u.utm_term ? String(u.utm_term) : void 0,
1012
+ content: u.utm_content ? String(u.utm_content) : void 0
1013
+ } : void 0
773
1014
  }));
774
1015
  return {
775
1016
  users,
@@ -797,6 +1038,7 @@ var ClickHouseAdapter = class {
797
1038
  name: data.name,
798
1039
  domain: data.domain,
799
1040
  allowedOrigins: data.allowedOrigins,
1041
+ conversionEvents: data.conversionEvents,
800
1042
  createdAt: nowISO,
801
1043
  updatedAt: nowISO
802
1044
  };
@@ -808,6 +1050,7 @@ var ClickHouseAdapter = class {
808
1050
  name: site.name,
809
1051
  domain: site.domain ?? null,
810
1052
  allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
1053
+ conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
811
1054
  created_at: nowCH,
812
1055
  updated_at: nowCH,
813
1056
  version: 1,
@@ -819,7 +1062,7 @@ var ClickHouseAdapter = class {
819
1062
  }
820
1063
  async getSite(siteId) {
821
1064
  const rows = await this.queryRows(
822
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
1065
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
823
1066
  FROM ${SITES_TABLE} FINAL
824
1067
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
825
1068
  { siteId }
@@ -828,7 +1071,7 @@ var ClickHouseAdapter = class {
828
1071
  }
829
1072
  async getSiteBySecret(secretKey) {
830
1073
  const rows = await this.queryRows(
831
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
1074
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
832
1075
  FROM ${SITES_TABLE} FINAL
833
1076
  WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
834
1077
  { secretKey }
@@ -837,7 +1080,7 @@ var ClickHouseAdapter = class {
837
1080
  }
838
1081
  async listSites() {
839
1082
  const rows = await this.queryRows(
840
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
1083
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
841
1084
  FROM ${SITES_TABLE} FINAL
842
1085
  WHERE is_deleted = 0
843
1086
  ORDER BY created_at DESC`,
@@ -847,7 +1090,7 @@ var ClickHouseAdapter = class {
847
1090
  }
848
1091
  async updateSite(siteId, data) {
849
1092
  const currentRows = await this.queryRows(
850
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at, version
1093
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
851
1094
  FROM ${SITES_TABLE} FINAL
852
1095
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
853
1096
  { siteId }
@@ -861,6 +1104,7 @@ var ClickHouseAdapter = class {
861
1104
  const newName = data.name !== void 0 ? data.name : String(current.name);
862
1105
  const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
863
1106
  const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
1107
+ const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
864
1108
  await this.client.insert({
865
1109
  table: SITES_TABLE,
866
1110
  values: [{
@@ -869,6 +1113,7 @@ var ClickHouseAdapter = class {
869
1113
  name: newName,
870
1114
  domain: newDomain,
871
1115
  allowed_origins: newOrigins,
1116
+ conversion_events: newConversions,
872
1117
  created_at: toCHDateTime(String(current.created_at)),
873
1118
  updated_at: nowCH,
874
1119
  version: newVersion,
@@ -882,13 +1127,14 @@ var ClickHouseAdapter = class {
882
1127
  name: newName,
883
1128
  domain: newDomain ?? void 0,
884
1129
  allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
1130
+ conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
885
1131
  createdAt: String(current.created_at),
886
1132
  updatedAt: nowISO
887
1133
  };
888
1134
  }
889
1135
  async deleteSite(siteId) {
890
1136
  const currentRows = await this.queryRows(
891
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
1137
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
892
1138
  FROM ${SITES_TABLE} FINAL
893
1139
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
894
1140
  { siteId }
@@ -904,6 +1150,7 @@ var ClickHouseAdapter = class {
904
1150
  name: String(current.name),
905
1151
  domain: current.domain ? String(current.domain) : null,
906
1152
  allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
1153
+ conversion_events: current.conversion_events ? String(current.conversion_events) : null,
907
1154
  created_at: toCHDateTime(String(current.created_at)),
908
1155
  updated_at: nowCH,
909
1156
  version: Number(current.version) + 1,
@@ -915,7 +1162,7 @@ var ClickHouseAdapter = class {
915
1162
  }
916
1163
  async regenerateSecret(siteId) {
917
1164
  const currentRows = await this.queryRows(
918
- `SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
1165
+ `SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
919
1166
  FROM ${SITES_TABLE} FINAL
920
1167
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
921
1168
  { siteId }
@@ -934,6 +1181,7 @@ var ClickHouseAdapter = class {
934
1181
  name: String(current.name),
935
1182
  domain: current.domain ? String(current.domain) : null,
936
1183
  allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
1184
+ conversion_events: current.conversion_events ? String(current.conversion_events) : null,
937
1185
  created_at: toCHDateTime(String(current.created_at)),
938
1186
  updated_at: nowCH,
939
1187
  version: Number(current.version) + 1,
@@ -947,6 +1195,7 @@ var ClickHouseAdapter = class {
947
1195
  name: String(current.name),
948
1196
  domain: current.domain ? String(current.domain) : void 0,
949
1197
  allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
1198
+ conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
950
1199
  createdAt: String(current.created_at),
951
1200
  updatedAt: nowISO
952
1201
  };
@@ -967,6 +1216,7 @@ var ClickHouseAdapter = class {
967
1216
  name: String(row.name),
968
1217
  domain: row.domain ? String(row.domain) : void 0,
969
1218
  allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
1219
+ conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
970
1220
  createdAt: new Date(String(row.created_at)).toISOString(),
971
1221
  updatedAt: new Date(String(row.updated_at)).toISOString()
972
1222
  };
@@ -983,6 +1233,13 @@ var ClickHouseAdapter = class {
983
1233
  title: row.title ? String(row.title) : void 0,
984
1234
  name: row.event_name ? String(row.event_name) : void 0,
985
1235
  properties: this.parseJSON(row.properties),
1236
+ eventSource: row.event_source ? String(row.event_source) : void 0,
1237
+ eventSubtype: row.event_subtype ? String(row.event_subtype) : void 0,
1238
+ pagePath: row.page_path ? String(row.page_path) : void 0,
1239
+ targetUrlPath: row.target_url_path ? String(row.target_url_path) : void 0,
1240
+ elementSelector: row.element_selector ? String(row.element_selector) : void 0,
1241
+ elementText: row.element_text ? String(row.element_text) : void 0,
1242
+ scrollDepthPct: row.scroll_depth_pct !== null && row.scroll_depth_pct !== void 0 ? Number(row.scroll_depth_pct) : void 0,
986
1243
  userId: row.user_id ? String(row.user_id) : void 0,
987
1244
  traits: this.parseJSON(row.traits),
988
1245
  geo: row.country ? {
@@ -1019,6 +1276,36 @@ var ClickHouseAdapter = class {
1019
1276
  var import_mongodb = require("mongodb");
1020
1277
  var EVENTS_COLLECTION = "litemetrics_events";
1021
1278
  var SITES_COLLECTION = "litemetrics_sites";
1279
+ function buildFilterMatch(filters) {
1280
+ if (!filters) return {};
1281
+ const map = {
1282
+ "geo.country": "country",
1283
+ "geo.city": "city",
1284
+ "geo.region": "region",
1285
+ "language": "language",
1286
+ "device.type": "device_type",
1287
+ "device.browser": "browser",
1288
+ "device.os": "os",
1289
+ "utm.source": "utm_source",
1290
+ "utm.medium": "utm_medium",
1291
+ "utm.campaign": "utm_campaign",
1292
+ "utm.term": "utm_term",
1293
+ "utm.content": "utm_content",
1294
+ "referrer": "referrer",
1295
+ "event_source": "event_source",
1296
+ "event_subtype": "event_subtype",
1297
+ "page_path": "page_path",
1298
+ "target_url_path": "target_url_path",
1299
+ "event_name": "event_name",
1300
+ "type": "type"
1301
+ };
1302
+ const match = {};
1303
+ for (const [key, value] of Object.entries(filters)) {
1304
+ if (!value || !map[key]) continue;
1305
+ match[map[key]] = value;
1306
+ }
1307
+ return match;
1308
+ }
1022
1309
  var MongoDBAdapter = class {
1023
1310
  client;
1024
1311
  db;
@@ -1054,6 +1341,13 @@ var MongoDBAdapter = class {
1054
1341
  title: e.title ?? null,
1055
1342
  event_name: e.name ?? null,
1056
1343
  properties: e.properties ?? null,
1344
+ event_source: e.eventSource ?? null,
1345
+ event_subtype: e.eventSubtype ?? null,
1346
+ page_path: e.pagePath ?? null,
1347
+ target_url_path: e.targetUrlPath ?? null,
1348
+ element_selector: e.elementSelector ?? null,
1349
+ element_text: e.elementText ?? null,
1350
+ scroll_depth_pct: e.scrollDepthPct ?? null,
1057
1351
  user_id: e.userId ?? null,
1058
1352
  traits: e.traits ?? null,
1059
1353
  country: e.geo?.country ?? null,
@@ -1084,12 +1378,13 @@ var MongoDBAdapter = class {
1084
1378
  site_id: siteId,
1085
1379
  timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
1086
1380
  };
1381
+ const filterMatch = buildFilterMatch(q.filters);
1087
1382
  let data = [];
1088
1383
  let total = 0;
1089
1384
  switch (q.metric) {
1090
1385
  case "pageviews": {
1091
1386
  const [result2] = await this.collection.aggregate([
1092
- { $match: { ...baseMatch, type: "pageview" } },
1387
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview" } },
1093
1388
  { $count: "count" }
1094
1389
  ]).toArray();
1095
1390
  total = result2?.count ?? 0;
@@ -1098,7 +1393,7 @@ var MongoDBAdapter = class {
1098
1393
  }
1099
1394
  case "visitors": {
1100
1395
  const [result2] = await this.collection.aggregate([
1101
- { $match: baseMatch },
1396
+ { $match: { ...baseMatch, ...filterMatch } },
1102
1397
  { $group: { _id: "$visitor_id" } },
1103
1398
  { $count: "count" }
1104
1399
  ]).toArray();
@@ -1108,7 +1403,7 @@ var MongoDBAdapter = class {
1108
1403
  }
1109
1404
  case "sessions": {
1110
1405
  const [result2] = await this.collection.aggregate([
1111
- { $match: baseMatch },
1406
+ { $match: { ...baseMatch, ...filterMatch } },
1112
1407
  { $group: { _id: "$session_id" } },
1113
1408
  { $count: "count" }
1114
1409
  ]).toArray();
@@ -1118,16 +1413,31 @@ var MongoDBAdapter = class {
1118
1413
  }
1119
1414
  case "events": {
1120
1415
  const [result2] = await this.collection.aggregate([
1121
- { $match: { ...baseMatch, type: "event" } },
1416
+ { $match: { ...baseMatch, ...filterMatch, type: "event" } },
1122
1417
  { $count: "count" }
1123
1418
  ]).toArray();
1124
1419
  total = result2?.count ?? 0;
1125
1420
  data = [{ key: "events", value: total }];
1126
1421
  break;
1127
1422
  }
1423
+ case "conversions": {
1424
+ const conversionEvents = q.conversionEvents ?? [];
1425
+ if (conversionEvents.length === 0) {
1426
+ total = 0;
1427
+ data = [{ key: "conversions", value: 0 }];
1428
+ break;
1429
+ }
1430
+ const [result2] = await this.collection.aggregate([
1431
+ { $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
1432
+ { $count: "count" }
1433
+ ]).toArray();
1434
+ total = result2?.count ?? 0;
1435
+ data = [{ key: "conversions", value: total }];
1436
+ break;
1437
+ }
1128
1438
  case "top_pages": {
1129
1439
  const rows = await this.collection.aggregate([
1130
- { $match: { ...baseMatch, type: "pageview", url: { $ne: null } } },
1440
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
1131
1441
  { $group: { _id: "$url", value: { $sum: 1 } } },
1132
1442
  { $sort: { value: -1 } },
1133
1443
  { $limit: limit }
@@ -1138,7 +1448,7 @@ var MongoDBAdapter = class {
1138
1448
  }
1139
1449
  case "top_referrers": {
1140
1450
  const rows = await this.collection.aggregate([
1141
- { $match: { ...baseMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
1451
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
1142
1452
  { $group: { _id: "$referrer", value: { $sum: 1 } } },
1143
1453
  { $sort: { value: -1 } },
1144
1454
  { $limit: limit }
@@ -1149,7 +1459,7 @@ var MongoDBAdapter = class {
1149
1459
  }
1150
1460
  case "top_countries": {
1151
1461
  const rows = await this.collection.aggregate([
1152
- { $match: { ...baseMatch, country: { $ne: null } } },
1462
+ { $match: { ...baseMatch, ...filterMatch, country: { $ne: null } } },
1153
1463
  { $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
1154
1464
  { $project: { _id: 1, value: { $size: "$value" } } },
1155
1465
  { $sort: { value: -1 } },
@@ -1161,7 +1471,7 @@ var MongoDBAdapter = class {
1161
1471
  }
1162
1472
  case "top_cities": {
1163
1473
  const rows = await this.collection.aggregate([
1164
- { $match: { ...baseMatch, city: { $ne: null } } },
1474
+ { $match: { ...baseMatch, ...filterMatch, city: { $ne: null } } },
1165
1475
  { $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
1166
1476
  { $project: { _id: 1, value: { $size: "$value" } } },
1167
1477
  { $sort: { value: -1 } },
@@ -1173,7 +1483,7 @@ var MongoDBAdapter = class {
1173
1483
  }
1174
1484
  case "top_events": {
1175
1485
  const rows = await this.collection.aggregate([
1176
- { $match: { ...baseMatch, type: "event", event_name: { $ne: null } } },
1486
+ { $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $ne: null } } },
1177
1487
  { $group: { _id: "$event_name", value: { $sum: 1 } } },
1178
1488
  { $sort: { value: -1 } },
1179
1489
  { $limit: limit }
@@ -1182,9 +1492,109 @@ var MongoDBAdapter = class {
1182
1492
  total = data.reduce((sum, d) => sum + d.value, 0);
1183
1493
  break;
1184
1494
  }
1495
+ case "top_conversions": {
1496
+ const conversionEvents = q.conversionEvents ?? [];
1497
+ if (conversionEvents.length === 0) {
1498
+ total = 0;
1499
+ data = [];
1500
+ break;
1501
+ }
1502
+ const rows = await this.collection.aggregate([
1503
+ { $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
1504
+ { $group: { _id: "$event_name", value: { $sum: 1 } } },
1505
+ { $sort: { value: -1 } },
1506
+ { $limit: limit }
1507
+ ]).toArray();
1508
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1509
+ total = data.reduce((sum, d) => sum + d.value, 0);
1510
+ break;
1511
+ }
1512
+ case "top_exit_pages": {
1513
+ const rows = await this.collection.aggregate([
1514
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
1515
+ { $sort: { timestamp: 1 } },
1516
+ { $group: { _id: "$session_id", url: { $last: "$url" } } },
1517
+ { $group: { _id: "$url", value: { $sum: 1 } } },
1518
+ { $sort: { value: -1 } },
1519
+ { $limit: limit }
1520
+ ]).toArray();
1521
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1522
+ total = data.reduce((sum, d) => sum + d.value, 0);
1523
+ break;
1524
+ }
1525
+ case "top_transitions": {
1526
+ const rows = await this.collection.aggregate([
1527
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
1528
+ {
1529
+ $setWindowFields: {
1530
+ partitionBy: "$session_id",
1531
+ sortBy: { timestamp: 1 },
1532
+ output: {
1533
+ prev_url: { $shift: { output: "$url", by: -1 } }
1534
+ }
1535
+ }
1536
+ },
1537
+ { $match: { prev_url: { $ne: null } } },
1538
+ { $group: { _id: { $concat: ["$prev_url", " \u2192 ", "$url"] }, value: { $sum: 1 } } },
1539
+ { $sort: { value: -1 } },
1540
+ { $limit: limit }
1541
+ ]).toArray();
1542
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1543
+ total = data.reduce((sum, d) => sum + d.value, 0);
1544
+ break;
1545
+ }
1546
+ case "top_scroll_pages": {
1547
+ const rows = await this.collection.aggregate([
1548
+ { $match: { ...baseMatch, ...filterMatch, type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } } },
1549
+ { $group: { _id: "$page_path", value: { $sum: 1 } } },
1550
+ { $sort: { value: -1 } },
1551
+ { $limit: limit }
1552
+ ]).toArray();
1553
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1554
+ total = data.reduce((sum, d) => sum + d.value, 0);
1555
+ break;
1556
+ }
1557
+ case "top_button_clicks": {
1558
+ const rows = await this.collection.aggregate([
1559
+ {
1560
+ $match: {
1561
+ ...baseMatch,
1562
+ ...filterMatch,
1563
+ type: "event",
1564
+ event_subtype: "button_click",
1565
+ $or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
1566
+ }
1567
+ },
1568
+ { $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
1569
+ { $sort: { value: -1 } },
1570
+ { $limit: limit }
1571
+ ]).toArray();
1572
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1573
+ total = data.reduce((sum, d) => sum + d.value, 0);
1574
+ break;
1575
+ }
1576
+ case "top_link_targets": {
1577
+ const rows = await this.collection.aggregate([
1578
+ {
1579
+ $match: {
1580
+ ...baseMatch,
1581
+ ...filterMatch,
1582
+ type: "event",
1583
+ event_subtype: { $in: ["link_click", "outbound_click"] },
1584
+ target_url_path: { $ne: null }
1585
+ }
1586
+ },
1587
+ { $group: { _id: "$target_url_path", value: { $sum: 1 } } },
1588
+ { $sort: { value: -1 } },
1589
+ { $limit: limit }
1590
+ ]).toArray();
1591
+ data = rows.map((r) => ({ key: r._id, value: r.value }));
1592
+ total = data.reduce((sum, d) => sum + d.value, 0);
1593
+ break;
1594
+ }
1185
1595
  case "top_devices": {
1186
1596
  const rows = await this.collection.aggregate([
1187
- { $match: { ...baseMatch, device_type: { $ne: null } } },
1597
+ { $match: { ...baseMatch, ...filterMatch, device_type: { $ne: null } } },
1188
1598
  { $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
1189
1599
  { $project: { _id: 1, value: { $size: "$value" } } },
1190
1600
  { $sort: { value: -1 } },
@@ -1196,7 +1606,7 @@ var MongoDBAdapter = class {
1196
1606
  }
1197
1607
  case "top_browsers": {
1198
1608
  const rows = await this.collection.aggregate([
1199
- { $match: { ...baseMatch, browser: { $ne: null } } },
1609
+ { $match: { ...baseMatch, ...filterMatch, browser: { $ne: null } } },
1200
1610
  { $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
1201
1611
  { $project: { _id: 1, value: { $size: "$value" } } },
1202
1612
  { $sort: { value: -1 } },
@@ -1208,7 +1618,7 @@ var MongoDBAdapter = class {
1208
1618
  }
1209
1619
  case "top_os": {
1210
1620
  const rows = await this.collection.aggregate([
1211
- { $match: { ...baseMatch, os: { $ne: null } } },
1621
+ { $match: { ...baseMatch, ...filterMatch, os: { $ne: null } } },
1212
1622
  { $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
1213
1623
  { $project: { _id: 1, value: { $size: "$value" } } },
1214
1624
  { $sort: { value: -1 } },
@@ -1220,7 +1630,7 @@ var MongoDBAdapter = class {
1220
1630
  }
1221
1631
  }
1222
1632
  const result = { metric: q.metric, period, data, total };
1223
- if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
1633
+ if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
1224
1634
  const prevRange = previousPeriodRange(dateRange);
1225
1635
  const prevResult = await this.query({
1226
1636
  ...q,
@@ -1252,15 +1662,34 @@ var MongoDBAdapter = class {
1252
1662
  site_id: params.siteId,
1253
1663
  timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
1254
1664
  };
1665
+ const filterMatch = buildFilterMatch(params.filters);
1255
1666
  if (params.metric === "pageviews") {
1256
1667
  baseMatch.type = "pageview";
1257
1668
  }
1669
+ if (params.metric === "events") {
1670
+ baseMatch.type = "event";
1671
+ }
1672
+ if (params.metric === "conversions") {
1673
+ baseMatch.type = "event";
1674
+ const conversionEvents = params.conversionEvents ?? [];
1675
+ if (conversionEvents.length === 0) {
1676
+ const data2 = fillBuckets(
1677
+ new Date(dateRange.from),
1678
+ new Date(dateRange.to),
1679
+ granularity,
1680
+ granularityToDateFormat(granularity),
1681
+ []
1682
+ );
1683
+ return { metric: params.metric, granularity, data: data2 };
1684
+ }
1685
+ baseMatch.event_name = { $in: conversionEvents };
1686
+ }
1258
1687
  const dateFormat = granularityToDateFormat(granularity);
1259
1688
  let pipeline;
1260
1689
  if (params.metric === "visitors" || params.metric === "sessions") {
1261
1690
  const groupField = params.metric === "visitors" ? "$visitor_id" : "$session_id";
1262
1691
  pipeline = [
1263
- { $match: baseMatch },
1692
+ { $match: { ...baseMatch, ...filterMatch } },
1264
1693
  {
1265
1694
  $group: {
1266
1695
  _id: {
@@ -1279,7 +1708,7 @@ var MongoDBAdapter = class {
1279
1708
  ];
1280
1709
  } else {
1281
1710
  pipeline = [
1282
- { $match: baseMatch },
1711
+ { $match: { ...baseMatch, ...filterMatch } },
1283
1712
  {
1284
1713
  $group: {
1285
1714
  _id: { $dateToString: { format: dateFormat, date: "$timestamp" } },
@@ -1360,7 +1789,12 @@ var MongoDBAdapter = class {
1360
1789
  const offset = params.offset ?? 0;
1361
1790
  const match = { site_id: params.siteId };
1362
1791
  if (params.type) match.type = params.type;
1363
- if (params.eventName) match.event_name = params.eventName;
1792
+ if (params.eventName) {
1793
+ match.event_name = params.eventName;
1794
+ } else if (params.eventNames && params.eventNames.length > 0) {
1795
+ match.event_name = { $in: params.eventNames };
1796
+ }
1797
+ if (params.eventSource) match.event_source = params.eventSource;
1364
1798
  if (params.visitorId) match.visitor_id = params.visitorId;
1365
1799
  if (params.userId) match.user_id = params.userId;
1366
1800
  if (params.period || params.dateFrom) {
@@ -1395,6 +1829,7 @@ var MongoDBAdapter = class {
1395
1829
  }
1396
1830
  const pipeline = [
1397
1831
  { $match: match },
1832
+ { $sort: { timestamp: 1 } },
1398
1833
  {
1399
1834
  $group: {
1400
1835
  _id: "$visitor_id",
@@ -1406,13 +1841,22 @@ var MongoDBAdapter = class {
1406
1841
  totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
1407
1842
  sessions: { $addToSet: "$session_id" },
1408
1843
  lastUrl: { $last: "$url" },
1844
+ referrer: { $last: "$referrer" },
1409
1845
  device_type: { $last: "$device_type" },
1410
1846
  browser: { $last: "$browser" },
1411
1847
  os: { $last: "$os" },
1412
1848
  country: { $last: "$country" },
1413
1849
  city: { $last: "$city" },
1414
1850
  region: { $last: "$region" },
1415
- language: { $last: "$language" }
1851
+ language: { $last: "$language" },
1852
+ timezone: { $last: "$timezone" },
1853
+ screen_width: { $last: "$screen_width" },
1854
+ screen_height: { $last: "$screen_height" },
1855
+ utm_source: { $last: "$utm_source" },
1856
+ utm_medium: { $last: "$utm_medium" },
1857
+ utm_campaign: { $last: "$utm_campaign" },
1858
+ utm_term: { $last: "$utm_term" },
1859
+ utm_content: { $last: "$utm_content" }
1416
1860
  }
1417
1861
  },
1418
1862
  { $sort: { lastSeen: -1 } },
@@ -1434,9 +1878,19 @@ var MongoDBAdapter = class {
1434
1878
  totalPageviews: u.totalPageviews,
1435
1879
  totalSessions: u.sessions.length,
1436
1880
  lastUrl: u.lastUrl ?? void 0,
1881
+ referrer: u.referrer ?? void 0,
1437
1882
  device: u.device_type ? { type: u.device_type, browser: u.browser ?? "", os: u.os ?? "" } : void 0,
1438
1883
  geo: u.country ? { country: u.country, city: u.city ?? void 0, region: u.region ?? void 0 } : void 0,
1439
- language: u.language ?? void 0
1884
+ language: u.language ?? void 0,
1885
+ timezone: u.timezone ?? void 0,
1886
+ screen: u.screen_width || u.screen_height ? { width: u.screen_width ?? 0, height: u.screen_height ?? 0 } : void 0,
1887
+ utm: u.utm_source ? {
1888
+ source: u.utm_source ?? void 0,
1889
+ medium: u.utm_medium ?? void 0,
1890
+ campaign: u.utm_campaign ?? void 0,
1891
+ term: u.utm_term ?? void 0,
1892
+ content: u.utm_content ?? void 0
1893
+ } : void 0
1440
1894
  }));
1441
1895
  return {
1442
1896
  users,
@@ -1465,6 +1919,13 @@ var MongoDBAdapter = class {
1465
1919
  title: doc.title ?? void 0,
1466
1920
  name: doc.event_name ?? void 0,
1467
1921
  properties: doc.properties ?? void 0,
1922
+ eventSource: doc.event_source ? doc.event_source : void 0,
1923
+ eventSubtype: doc.event_subtype ? doc.event_subtype : void 0,
1924
+ pagePath: doc.page_path ?? void 0,
1925
+ targetUrlPath: doc.target_url_path ?? void 0,
1926
+ elementSelector: doc.element_selector ?? void 0,
1927
+ elementText: doc.element_text ?? void 0,
1928
+ scrollDepthPct: doc.scroll_depth_pct ?? void 0,
1468
1929
  userId: doc.user_id ?? void 0,
1469
1930
  traits: doc.traits ?? void 0,
1470
1931
  geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
@@ -1488,6 +1949,7 @@ var MongoDBAdapter = class {
1488
1949
  name: data.name,
1489
1950
  domain: data.domain ?? null,
1490
1951
  allowed_origins: data.allowedOrigins ?? null,
1952
+ conversion_events: data.conversionEvents ?? null,
1491
1953
  created_at: now,
1492
1954
  updated_at: now
1493
1955
  };
@@ -1511,6 +1973,7 @@ var MongoDBAdapter = class {
1511
1973
  if (data.name !== void 0) updates.name = data.name;
1512
1974
  if (data.domain !== void 0) updates.domain = data.domain || null;
1513
1975
  if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
1976
+ if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
1514
1977
  const result = await this.sites.findOneAndUpdate(
1515
1978
  { site_id: siteId },
1516
1979
  { $set: updates },
@@ -1541,6 +2004,7 @@ var MongoDBAdapter = class {
1541
2004
  name: doc.name,
1542
2005
  domain: doc.domain ?? void 0,
1543
2006
  allowedOrigins: doc.allowed_origins ?? void 0,
2007
+ conversionEvents: doc.conversion_events ?? void 0,
1544
2008
  createdAt: doc.created_at.toISOString(),
1545
2009
  updatedAt: doc.updated_at.toISOString()
1546
2010
  };
@@ -1794,6 +2258,7 @@ async function createCollector(config) {
1794
2258
  if (allowed) {
1795
2259
  res.setHeader?.("Access-Control-Allow-Origin", origin || "*");
1796
2260
  res.setHeader?.("Access-Control-Allow-Methods", methods);
2261
+ res.setHeader?.("Access-Control-Allow-Credentials", "true");
1797
2262
  const headers = ["Content-Type", extraHeaders].filter(Boolean).join(", ");
1798
2263
  res.setHeader?.("Access-Control-Allow-Headers", headers);
1799
2264
  }
@@ -1824,7 +2289,14 @@ async function createCollector(config) {
1824
2289
  }
1825
2290
  function handler() {
1826
2291
  return async (req, res) => {
1827
- if (setCors(req, res, "POST, OPTIONS")) return;
2292
+ res.setHeader?.("Access-Control-Allow-Origin", "*");
2293
+ res.setHeader?.("Access-Control-Allow-Methods", "POST, OPTIONS");
2294
+ res.setHeader?.("Access-Control-Allow-Headers", "Content-Type");
2295
+ if (req.method === "OPTIONS") {
2296
+ res.writeHead?.(204);
2297
+ res.end?.();
2298
+ return;
2299
+ }
1828
2300
  if (req.method !== "POST") {
1829
2301
  sendJson(res, 405, { ok: false, error: "Method not allowed" });
1830
2302
  return;
@@ -1903,8 +2375,13 @@ async function createCollector(config) {
1903
2375
  period: params.period,
1904
2376
  dateFrom: params.dateFrom,
1905
2377
  dateTo: params.dateTo,
1906
- granularity: q.granularity
2378
+ granularity: q.granularity,
2379
+ filters: q.filters ? JSON.parse(q.filters) : void 0
1907
2380
  };
2381
+ if (tsParams.metric === "conversions") {
2382
+ const site = await db.getSite(params.siteId);
2383
+ tsParams.conversionEvents = site?.conversionEvents ?? [];
2384
+ }
1908
2385
  const result2 = await db.queryTimeSeries(tsParams);
1909
2386
  sendJson(res, 200, result2);
1910
2387
  return;
@@ -1920,7 +2397,15 @@ async function createCollector(config) {
1920
2397
  sendJson(res, 200, result2);
1921
2398
  return;
1922
2399
  }
1923
- const result = await db.query(params);
2400
+ const isConversionMetric = params.metric === "conversions" || params.metric === "top_conversions";
2401
+ let result;
2402
+ if (isConversionMetric) {
2403
+ const site = await db.getSite(params.siteId);
2404
+ const conversionEvents = site?.conversionEvents ?? [];
2405
+ result = await db.query({ ...params, conversionEvents });
2406
+ } else {
2407
+ result = await db.query(params);
2408
+ }
1924
2409
  sendJson(res, 200, result);
1925
2410
  } catch (err) {
1926
2411
  sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : "Internal error" });
@@ -2017,10 +2502,13 @@ async function createCollector(config) {
2017
2502
  sendJson(res, 401, { ok: false, error: "Invalid or missing secret key" });
2018
2503
  return;
2019
2504
  }
2505
+ const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2020
2506
  const params = {
2021
2507
  siteId: q.siteId,
2022
2508
  type: q.type,
2023
2509
  eventName: q.eventName,
2510
+ eventNames,
2511
+ eventSource: q.eventSource,
2024
2512
  visitorId: q.visitorId,
2025
2513
  userId: q.userId,
2026
2514
  period: q.period,
@@ -2060,9 +2548,13 @@ async function createCollector(config) {
2060
2548
  const visitorId = usersIdx >= 0 ? pathSegments[usersIdx + 1] : void 0;
2061
2549
  const action = usersIdx >= 0 ? pathSegments[usersIdx + 2] : void 0;
2062
2550
  if (visitorId && action === "events") {
2551
+ const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2063
2552
  const params2 = {
2064
2553
  siteId: q.siteId,
2065
2554
  type: q.type,
2555
+ eventName: q.eventName,
2556
+ eventNames,
2557
+ eventSource: q.eventSource,
2066
2558
  period: q.period,
2067
2559
  dateFrom: q.dateFrom,
2068
2560
  dateTo: q.dateTo,
@@ -2169,7 +2661,8 @@ function createAdapter(config) {
2169
2661
  }
2170
2662
  }
2171
2663
  async function parseBody(req) {
2172
- if (req.body) return req.body;
2664
+ if (req.body && typeof req.body === "object") return req.body;
2665
+ if (typeof req.body === "string") return JSON.parse(req.body);
2173
2666
  return new Promise((resolve, reject) => {
2174
2667
  let data = "";
2175
2668
  req.on("data", (chunk) => {