@litemetrics/node 0.1.0 → 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
@@ -32,7 +32,8 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ClickHouseAdapter: () => ClickHouseAdapter,
34
34
  MongoDBAdapter: () => MongoDBAdapter,
35
- createCollector: () => createCollector
35
+ createCollector: () => createCollector,
36
+ isBot: () => isBot
36
37
  });
37
38
  module.exports = __toCommonJS(index_exports);
38
39
 
@@ -191,6 +192,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
191
192
  title Nullable(String),
192
193
  event_name Nullable(String),
193
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),
194
202
  user_id Nullable(String),
195
203
  traits Nullable(String),
196
204
  country LowCardinality(Nullable(String)),
@@ -222,6 +230,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
222
230
  name String,
223
231
  domain Nullable(String),
224
232
  allowed_origins Nullable(String),
233
+ conversion_events Nullable(String),
225
234
  created_at DateTime64(3),
226
235
  updated_at DateTime64(3),
227
236
  version UInt64,
@@ -230,6 +239,43 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
230
239
  ORDER BY (site_id)
231
240
  SETTINGS index_granularity = 8192
232
241
  `;
242
+ function toCHDateTime(d) {
243
+ const iso = typeof d === "string" ? d : d.toISOString();
244
+ return iso.replace("T", " ").replace("Z", "");
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
+ }
233
279
  var ClickHouseAdapter = class {
234
280
  client;
235
281
  constructor(url) {
@@ -243,6 +289,16 @@ var ClickHouseAdapter = class {
243
289
  async init() {
244
290
  await this.client.command({ query: CREATE_EVENTS_TABLE });
245
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
+ });
246
302
  }
247
303
  async close() {
248
304
  await this.client.close();
@@ -253,7 +309,7 @@ var ClickHouseAdapter = class {
253
309
  const rows = events.map((e) => ({
254
310
  site_id: e.siteId,
255
311
  type: e.type,
256
- timestamp: new Date(e.timestamp).toISOString(),
312
+ timestamp: toCHDateTime(new Date(e.timestamp)),
257
313
  session_id: e.sessionId,
258
314
  visitor_id: e.visitorId,
259
315
  url: e.url ?? null,
@@ -261,6 +317,13 @@ var ClickHouseAdapter = class {
261
317
  title: e.title ?? null,
262
318
  event_name: e.name ?? null,
263
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,
264
327
  user_id: e.userId ?? null,
265
328
  traits: e.traits ? JSON.stringify(e.traits) : null,
266
329
  country: e.geo?.country ?? null,
@@ -293,10 +356,12 @@ var ClickHouseAdapter = class {
293
356
  const limit = q.limit ?? 10;
294
357
  const params = {
295
358
  siteId,
296
- from: dateRange.from,
297
- to: dateRange.to,
359
+ from: toCHDateTime(dateRange.from),
360
+ to: toCHDateTime(dateRange.to),
298
361
  limit
299
362
  };
363
+ const filter = buildFilterConditions(q.filters);
364
+ const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
300
365
  let data = [];
301
366
  let total = 0;
302
367
  switch (q.metric) {
@@ -306,8 +371,8 @@ var ClickHouseAdapter = class {
306
371
  WHERE site_id = {siteId:String}
307
372
  AND timestamp >= {from:String}
308
373
  AND timestamp <= {to:String}
309
- AND type = 'pageview'`,
310
- params
374
+ AND type = 'pageview'${filterSql}`,
375
+ { ...params, ...filter.params }
311
376
  );
312
377
  total = Number(rows[0]?.value ?? 0);
313
378
  data = [{ key: "pageviews", value: total }];
@@ -318,8 +383,8 @@ var ClickHouseAdapter = class {
318
383
  `SELECT uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
319
384
  WHERE site_id = {siteId:String}
320
385
  AND timestamp >= {from:String}
321
- AND timestamp <= {to:String}`,
322
- params
386
+ AND timestamp <= {to:String}${filterSql}`,
387
+ { ...params, ...filter.params }
323
388
  );
324
389
  total = Number(rows[0]?.value ?? 0);
325
390
  data = [{ key: "visitors", value: total }];
@@ -330,8 +395,8 @@ var ClickHouseAdapter = class {
330
395
  `SELECT uniq(session_id) AS value FROM ${EVENTS_TABLE}
331
396
  WHERE site_id = {siteId:String}
332
397
  AND timestamp >= {from:String}
333
- AND timestamp <= {to:String}`,
334
- params
398
+ AND timestamp <= {to:String}${filterSql}`,
399
+ { ...params, ...filter.params }
335
400
  );
336
401
  total = Number(rows[0]?.value ?? 0);
337
402
  data = [{ key: "sessions", value: total }];
@@ -343,13 +408,33 @@ var ClickHouseAdapter = class {
343
408
  WHERE site_id = {siteId:String}
344
409
  AND timestamp >= {from:String}
345
410
  AND timestamp <= {to:String}
346
- AND type = 'event'`,
347
- params
411
+ AND type = 'event'${filterSql}`,
412
+ { ...params, ...filter.params }
348
413
  );
349
414
  total = Number(rows[0]?.value ?? 0);
350
415
  data = [{ key: "events", value: total }];
351
416
  break;
352
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
+ }
353
438
  case "top_pages": {
354
439
  const rows = await this.queryRows(
355
440
  `SELECT url AS key, count() AS value FROM ${EVENTS_TABLE}
@@ -357,11 +442,11 @@ var ClickHouseAdapter = class {
357
442
  AND timestamp >= {from:String}
358
443
  AND timestamp <= {to:String}
359
444
  AND type = 'pageview'
360
- AND url IS NOT NULL
445
+ AND url IS NOT NULL${filterSql}
361
446
  GROUP BY url
362
447
  ORDER BY value DESC
363
448
  LIMIT {limit:UInt32}`,
364
- params
449
+ { ...params, ...filter.params }
365
450
  );
366
451
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
367
452
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -375,11 +460,11 @@ var ClickHouseAdapter = class {
375
460
  AND timestamp <= {to:String}
376
461
  AND type = 'pageview'
377
462
  AND referrer IS NOT NULL
378
- AND referrer != ''
463
+ AND referrer != ''${filterSql}
379
464
  GROUP BY referrer
380
465
  ORDER BY value DESC
381
466
  LIMIT {limit:UInt32}`,
382
- params
467
+ { ...params, ...filter.params }
383
468
  );
384
469
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
385
470
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -391,11 +476,11 @@ var ClickHouseAdapter = class {
391
476
  WHERE site_id = {siteId:String}
392
477
  AND timestamp >= {from:String}
393
478
  AND timestamp <= {to:String}
394
- AND country IS NOT NULL
479
+ AND country IS NOT NULL${filterSql}
395
480
  GROUP BY country
396
481
  ORDER BY value DESC
397
482
  LIMIT {limit:UInt32}`,
398
- params
483
+ { ...params, ...filter.params }
399
484
  );
400
485
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
401
486
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -407,11 +492,11 @@ var ClickHouseAdapter = class {
407
492
  WHERE site_id = {siteId:String}
408
493
  AND timestamp >= {from:String}
409
494
  AND timestamp <= {to:String}
410
- AND city IS NOT NULL
495
+ AND city IS NOT NULL${filterSql}
411
496
  GROUP BY city
412
497
  ORDER BY value DESC
413
498
  LIMIT {limit:UInt32}`,
414
- params
499
+ { ...params, ...filter.params }
415
500
  );
416
501
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
417
502
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -425,10 +510,132 @@ var ClickHouseAdapter = class {
425
510
  AND timestamp <= {to:String}
426
511
  AND type = 'event'
427
512
  AND event_name IS NOT NULL
513
+ ${filterSql}
514
+ GROUP BY event_name
515
+ ORDER BY value DESC
516
+ LIMIT {limit:UInt32}`,
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}
428
538
  GROUP BY event_name
429
539
  ORDER BY value DESC
430
540
  LIMIT {limit:UInt32}`,
431
- params
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 }
432
639
  );
433
640
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
434
641
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -441,10 +648,11 @@ var ClickHouseAdapter = class {
441
648
  AND timestamp >= {from:String}
442
649
  AND timestamp <= {to:String}
443
650
  AND device_type IS NOT NULL
651
+ ${filterSql}
444
652
  GROUP BY device_type
445
653
  ORDER BY value DESC
446
654
  LIMIT {limit:UInt32}`,
447
- params
655
+ { ...params, ...filter.params }
448
656
  );
449
657
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
450
658
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -457,10 +665,11 @@ var ClickHouseAdapter = class {
457
665
  AND timestamp >= {from:String}
458
666
  AND timestamp <= {to:String}
459
667
  AND browser IS NOT NULL
668
+ ${filterSql}
460
669
  GROUP BY browser
461
670
  ORDER BY value DESC
462
671
  LIMIT {limit:UInt32}`,
463
- params
672
+ { ...params, ...filter.params }
464
673
  );
465
674
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
466
675
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -473,10 +682,11 @@ var ClickHouseAdapter = class {
473
682
  AND timestamp >= {from:String}
474
683
  AND timestamp <= {to:String}
475
684
  AND os IS NOT NULL
685
+ ${filterSql}
476
686
  GROUP BY os
477
687
  ORDER BY value DESC
478
688
  LIMIT {limit:UInt32}`,
479
- params
689
+ { ...params, ...filter.params }
480
690
  );
481
691
  data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
482
692
  total = data.reduce((sum, d) => sum + d.value, 0);
@@ -484,7 +694,7 @@ var ClickHouseAdapter = class {
484
694
  }
485
695
  }
486
696
  const result = { metric: q.metric, period, data, total };
487
- if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
697
+ if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
488
698
  const prevRange = previousPeriodRange(dateRange);
489
699
  const prevResult = await this.query({
490
700
  ...q,
@@ -514,7 +724,12 @@ var ClickHouseAdapter = class {
514
724
  const granularity = params.granularity ?? autoGranularity(period);
515
725
  const bucketFn = this.granularityToClickHouseFunc(granularity);
516
726
  const dateFormat = granularityToDateFormat(granularity);
727
+ const filter = buildFilterConditions(params.filters);
728
+ const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
517
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(" ");
518
733
  let sql;
519
734
  if (params.metric === "visitors" || params.metric === "sessions") {
520
735
  const field = params.metric === "visitors" ? "visitor_id" : "session_id";
@@ -524,7 +739,7 @@ var ClickHouseAdapter = class {
524
739
  WHERE site_id = {siteId:String}
525
740
  AND timestamp >= {from:String}
526
741
  AND timestamp <= {to:String}
527
- ${typeFilter}
742
+ ${extraFilters}
528
743
  GROUP BY bucket
529
744
  ORDER BY bucket ASC
530
745
  `;
@@ -535,15 +750,17 @@ var ClickHouseAdapter = class {
535
750
  WHERE site_id = {siteId:String}
536
751
  AND timestamp >= {from:String}
537
752
  AND timestamp <= {to:String}
538
- ${typeFilter}
753
+ ${extraFilters}
539
754
  GROUP BY bucket
540
755
  ORDER BY bucket ASC
541
756
  `;
542
757
  }
543
758
  const rows = await this.queryRows(sql, {
544
759
  siteId: params.siteId,
545
- from: dateRange.from,
546
- to: dateRange.to
760
+ from: toCHDateTime(dateRange.from),
761
+ to: toCHDateTime(dateRange.to),
762
+ eventNames: params.conversionEvents ?? [],
763
+ ...filter.params
547
764
  });
548
765
  const mappedRows = rows.map((r) => ({
549
766
  _id: this.convertClickHouseBucket(r.bucket, granularity),
@@ -609,7 +826,7 @@ var ClickHouseAdapter = class {
609
826
  GROUP BY visitor_id`,
610
827
  {
611
828
  siteId: params.siteId,
612
- since: startDate.toISOString()
829
+ since: toCHDateTime(startDate)
613
830
  }
614
831
  );
615
832
  const cohortMap = /* @__PURE__ */ new Map();
@@ -661,6 +878,14 @@ var ClickHouseAdapter = class {
661
878
  conditions.push(`event_name = {eventName:String}`);
662
879
  queryParams.eventName = params.eventName;
663
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
+ }
664
889
  if (params.visitorId) {
665
890
  conditions.push(`visitor_id = {visitorId:String}`);
666
891
  queryParams.visitorId = params.visitorId;
@@ -676,14 +901,16 @@ var ClickHouseAdapter = class {
676
901
  dateTo: params.dateTo
677
902
  });
678
903
  conditions.push(`timestamp >= {from:String} AND timestamp <= {to:String}`);
679
- queryParams.from = dateRange.from;
680
- queryParams.to = dateRange.to;
904
+ queryParams.from = toCHDateTime(dateRange.from);
905
+ queryParams.to = toCHDateTime(dateRange.to);
681
906
  }
682
907
  const where = conditions.join(" AND ");
683
908
  const [events, countRows] = await Promise.all([
684
909
  this.queryRows(
685
910
  `SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
686
- 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,
687
914
  device_type, browser, os, language,
688
915
  utm_source, utm_medium, utm_campaign, utm_term, utm_content
689
916
  FROM ${EVENTS_TABLE}
@@ -728,13 +955,22 @@ var ClickHouseAdapter = class {
728
955
  countIf(type = 'pageview') AS totalPageviews,
729
956
  uniq(session_id) AS totalSessions,
730
957
  anyLast(url) AS lastUrl,
958
+ anyLast(referrer) AS referrer,
731
959
  anyLast(device_type) AS device_type,
732
960
  anyLast(browser) AS browser,
733
961
  anyLast(os) AS os,
734
962
  anyLast(country) AS country,
735
963
  anyLast(city) AS city,
736
964
  anyLast(region) AS region,
737
- 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
738
974
  FROM ${EVENTS_TABLE}
739
975
  WHERE ${where}
740
976
  GROUP BY visitor_id
@@ -762,9 +998,19 @@ var ClickHouseAdapter = class {
762
998
  totalPageviews: Number(u.totalPageviews),
763
999
  totalSessions: Number(u.totalSessions),
764
1000
  lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
1001
+ referrer: u.referrer ? String(u.referrer) : void 0,
765
1002
  device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
766
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,
767
- 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
768
1014
  }));
769
1015
  return {
770
1016
  users,
@@ -783,15 +1029,18 @@ var ClickHouseAdapter = class {
783
1029
  }
784
1030
  // ─── Site Management ──────────────────────────────────────
785
1031
  async createSite(data) {
786
- const now = (/* @__PURE__ */ new Date()).toISOString();
1032
+ const now = /* @__PURE__ */ new Date();
1033
+ const nowISO = now.toISOString();
1034
+ const nowCH = toCHDateTime(now);
787
1035
  const site = {
788
1036
  siteId: generateSiteId(),
789
1037
  secretKey: generateSecretKey(),
790
1038
  name: data.name,
791
1039
  domain: data.domain,
792
1040
  allowedOrigins: data.allowedOrigins,
793
- createdAt: now,
794
- updatedAt: now
1041
+ conversionEvents: data.conversionEvents,
1042
+ createdAt: nowISO,
1043
+ updatedAt: nowISO
795
1044
  };
796
1045
  await this.client.insert({
797
1046
  table: SITES_TABLE,
@@ -801,8 +1050,9 @@ var ClickHouseAdapter = class {
801
1050
  name: site.name,
802
1051
  domain: site.domain ?? null,
803
1052
  allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
804
- created_at: now,
805
- updated_at: now,
1053
+ conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
1054
+ created_at: nowCH,
1055
+ updated_at: nowCH,
806
1056
  version: 1,
807
1057
  is_deleted: 0
808
1058
  }],
@@ -812,7 +1062,7 @@ var ClickHouseAdapter = class {
812
1062
  }
813
1063
  async getSite(siteId) {
814
1064
  const rows = await this.queryRows(
815
- `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
816
1066
  FROM ${SITES_TABLE} FINAL
817
1067
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
818
1068
  { siteId }
@@ -821,7 +1071,7 @@ var ClickHouseAdapter = class {
821
1071
  }
822
1072
  async getSiteBySecret(secretKey) {
823
1073
  const rows = await this.queryRows(
824
- `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
825
1075
  FROM ${SITES_TABLE} FINAL
826
1076
  WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
827
1077
  { secretKey }
@@ -830,7 +1080,7 @@ var ClickHouseAdapter = class {
830
1080
  }
831
1081
  async listSites() {
832
1082
  const rows = await this.queryRows(
833
- `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
834
1084
  FROM ${SITES_TABLE} FINAL
835
1085
  WHERE is_deleted = 0
836
1086
  ORDER BY created_at DESC`,
@@ -840,18 +1090,21 @@ var ClickHouseAdapter = class {
840
1090
  }
841
1091
  async updateSite(siteId, data) {
842
1092
  const currentRows = await this.queryRows(
843
- `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
844
1094
  FROM ${SITES_TABLE} FINAL
845
1095
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
846
1096
  { siteId }
847
1097
  );
848
1098
  if (currentRows.length === 0) return null;
849
1099
  const current = currentRows[0];
850
- const now = (/* @__PURE__ */ new Date()).toISOString();
1100
+ const now = /* @__PURE__ */ new Date();
1101
+ const nowISO = now.toISOString();
1102
+ const nowCH = toCHDateTime(now);
851
1103
  const newVersion = Number(current.version) + 1;
852
1104
  const newName = data.name !== void 0 ? data.name : String(current.name);
853
1105
  const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
854
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;
855
1108
  await this.client.insert({
856
1109
  table: SITES_TABLE,
857
1110
  values: [{
@@ -860,8 +1113,9 @@ var ClickHouseAdapter = class {
860
1113
  name: newName,
861
1114
  domain: newDomain,
862
1115
  allowed_origins: newOrigins,
863
- created_at: String(current.created_at),
864
- updated_at: now,
1116
+ conversion_events: newConversions,
1117
+ created_at: toCHDateTime(String(current.created_at)),
1118
+ updated_at: nowCH,
865
1119
  version: newVersion,
866
1120
  is_deleted: 0
867
1121
  }],
@@ -873,20 +1127,21 @@ var ClickHouseAdapter = class {
873
1127
  name: newName,
874
1128
  domain: newDomain ?? void 0,
875
1129
  allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
1130
+ conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
876
1131
  createdAt: String(current.created_at),
877
- updatedAt: now
1132
+ updatedAt: nowISO
878
1133
  };
879
1134
  }
880
1135
  async deleteSite(siteId) {
881
1136
  const currentRows = await this.queryRows(
882
- `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
883
1138
  FROM ${SITES_TABLE} FINAL
884
1139
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
885
1140
  { siteId }
886
1141
  );
887
1142
  if (currentRows.length === 0) return false;
888
1143
  const current = currentRows[0];
889
- const now = (/* @__PURE__ */ new Date()).toISOString();
1144
+ const nowCH = toCHDateTime(/* @__PURE__ */ new Date());
890
1145
  await this.client.insert({
891
1146
  table: SITES_TABLE,
892
1147
  values: [{
@@ -895,8 +1150,9 @@ var ClickHouseAdapter = class {
895
1150
  name: String(current.name),
896
1151
  domain: current.domain ? String(current.domain) : null,
897
1152
  allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
898
- created_at: String(current.created_at),
899
- updated_at: now,
1153
+ conversion_events: current.conversion_events ? String(current.conversion_events) : null,
1154
+ created_at: toCHDateTime(String(current.created_at)),
1155
+ updated_at: nowCH,
900
1156
  version: Number(current.version) + 1,
901
1157
  is_deleted: 1
902
1158
  }],
@@ -906,14 +1162,16 @@ var ClickHouseAdapter = class {
906
1162
  }
907
1163
  async regenerateSecret(siteId) {
908
1164
  const currentRows = await this.queryRows(
909
- `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
910
1166
  FROM ${SITES_TABLE} FINAL
911
1167
  WHERE site_id = {siteId:String} AND is_deleted = 0`,
912
1168
  { siteId }
913
1169
  );
914
1170
  if (currentRows.length === 0) return null;
915
1171
  const current = currentRows[0];
916
- const now = (/* @__PURE__ */ new Date()).toISOString();
1172
+ const now = /* @__PURE__ */ new Date();
1173
+ const nowISO = now.toISOString();
1174
+ const nowCH = toCHDateTime(now);
917
1175
  const newSecret = generateSecretKey();
918
1176
  await this.client.insert({
919
1177
  table: SITES_TABLE,
@@ -923,8 +1181,9 @@ var ClickHouseAdapter = class {
923
1181
  name: String(current.name),
924
1182
  domain: current.domain ? String(current.domain) : null,
925
1183
  allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
926
- created_at: String(current.created_at),
927
- updated_at: now,
1184
+ conversion_events: current.conversion_events ? String(current.conversion_events) : null,
1185
+ created_at: toCHDateTime(String(current.created_at)),
1186
+ updated_at: nowCH,
928
1187
  version: Number(current.version) + 1,
929
1188
  is_deleted: 0
930
1189
  }],
@@ -936,8 +1195,9 @@ var ClickHouseAdapter = class {
936
1195
  name: String(current.name),
937
1196
  domain: current.domain ? String(current.domain) : void 0,
938
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,
939
1199
  createdAt: String(current.created_at),
940
- updatedAt: now
1200
+ updatedAt: nowISO
941
1201
  };
942
1202
  }
943
1203
  // ─── Helpers ─────────────────────────────────────────────
@@ -956,6 +1216,7 @@ var ClickHouseAdapter = class {
956
1216
  name: String(row.name),
957
1217
  domain: row.domain ? String(row.domain) : void 0,
958
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,
959
1220
  createdAt: new Date(String(row.created_at)).toISOString(),
960
1221
  updatedAt: new Date(String(row.updated_at)).toISOString()
961
1222
  };
@@ -972,6 +1233,13 @@ var ClickHouseAdapter = class {
972
1233
  title: row.title ? String(row.title) : void 0,
973
1234
  name: row.event_name ? String(row.event_name) : void 0,
974
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,
975
1243
  userId: row.user_id ? String(row.user_id) : void 0,
976
1244
  traits: this.parseJSON(row.traits),
977
1245
  geo: row.country ? {
@@ -1008,6 +1276,36 @@ var ClickHouseAdapter = class {
1008
1276
  var import_mongodb = require("mongodb");
1009
1277
  var EVENTS_COLLECTION = "litemetrics_events";
1010
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
+ }
1011
1309
  var MongoDBAdapter = class {
1012
1310
  client;
1013
1311
  db;
@@ -1043,6 +1341,13 @@ var MongoDBAdapter = class {
1043
1341
  title: e.title ?? null,
1044
1342
  event_name: e.name ?? null,
1045
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,
1046
1351
  user_id: e.userId ?? null,
1047
1352
  traits: e.traits ?? null,
1048
1353
  country: e.geo?.country ?? null,
@@ -1073,12 +1378,13 @@ var MongoDBAdapter = class {
1073
1378
  site_id: siteId,
1074
1379
  timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
1075
1380
  };
1381
+ const filterMatch = buildFilterMatch(q.filters);
1076
1382
  let data = [];
1077
1383
  let total = 0;
1078
1384
  switch (q.metric) {
1079
1385
  case "pageviews": {
1080
1386
  const [result2] = await this.collection.aggregate([
1081
- { $match: { ...baseMatch, type: "pageview" } },
1387
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview" } },
1082
1388
  { $count: "count" }
1083
1389
  ]).toArray();
1084
1390
  total = result2?.count ?? 0;
@@ -1087,7 +1393,7 @@ var MongoDBAdapter = class {
1087
1393
  }
1088
1394
  case "visitors": {
1089
1395
  const [result2] = await this.collection.aggregate([
1090
- { $match: baseMatch },
1396
+ { $match: { ...baseMatch, ...filterMatch } },
1091
1397
  { $group: { _id: "$visitor_id" } },
1092
1398
  { $count: "count" }
1093
1399
  ]).toArray();
@@ -1097,7 +1403,7 @@ var MongoDBAdapter = class {
1097
1403
  }
1098
1404
  case "sessions": {
1099
1405
  const [result2] = await this.collection.aggregate([
1100
- { $match: baseMatch },
1406
+ { $match: { ...baseMatch, ...filterMatch } },
1101
1407
  { $group: { _id: "$session_id" } },
1102
1408
  { $count: "count" }
1103
1409
  ]).toArray();
@@ -1107,16 +1413,31 @@ var MongoDBAdapter = class {
1107
1413
  }
1108
1414
  case "events": {
1109
1415
  const [result2] = await this.collection.aggregate([
1110
- { $match: { ...baseMatch, type: "event" } },
1416
+ { $match: { ...baseMatch, ...filterMatch, type: "event" } },
1111
1417
  { $count: "count" }
1112
1418
  ]).toArray();
1113
1419
  total = result2?.count ?? 0;
1114
1420
  data = [{ key: "events", value: total }];
1115
1421
  break;
1116
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
+ }
1117
1438
  case "top_pages": {
1118
1439
  const rows = await this.collection.aggregate([
1119
- { $match: { ...baseMatch, type: "pageview", url: { $ne: null } } },
1440
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
1120
1441
  { $group: { _id: "$url", value: { $sum: 1 } } },
1121
1442
  { $sort: { value: -1 } },
1122
1443
  { $limit: limit }
@@ -1127,7 +1448,7 @@ var MongoDBAdapter = class {
1127
1448
  }
1128
1449
  case "top_referrers": {
1129
1450
  const rows = await this.collection.aggregate([
1130
- { $match: { ...baseMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
1451
+ { $match: { ...baseMatch, ...filterMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
1131
1452
  { $group: { _id: "$referrer", value: { $sum: 1 } } },
1132
1453
  { $sort: { value: -1 } },
1133
1454
  { $limit: limit }
@@ -1138,7 +1459,7 @@ var MongoDBAdapter = class {
1138
1459
  }
1139
1460
  case "top_countries": {
1140
1461
  const rows = await this.collection.aggregate([
1141
- { $match: { ...baseMatch, country: { $ne: null } } },
1462
+ { $match: { ...baseMatch, ...filterMatch, country: { $ne: null } } },
1142
1463
  { $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
1143
1464
  { $project: { _id: 1, value: { $size: "$value" } } },
1144
1465
  { $sort: { value: -1 } },
@@ -1150,7 +1471,7 @@ var MongoDBAdapter = class {
1150
1471
  }
1151
1472
  case "top_cities": {
1152
1473
  const rows = await this.collection.aggregate([
1153
- { $match: { ...baseMatch, city: { $ne: null } } },
1474
+ { $match: { ...baseMatch, ...filterMatch, city: { $ne: null } } },
1154
1475
  { $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
1155
1476
  { $project: { _id: 1, value: { $size: "$value" } } },
1156
1477
  { $sort: { value: -1 } },
@@ -1162,7 +1483,7 @@ var MongoDBAdapter = class {
1162
1483
  }
1163
1484
  case "top_events": {
1164
1485
  const rows = await this.collection.aggregate([
1165
- { $match: { ...baseMatch, type: "event", event_name: { $ne: null } } },
1486
+ { $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $ne: null } } },
1166
1487
  { $group: { _id: "$event_name", value: { $sum: 1 } } },
1167
1488
  { $sort: { value: -1 } },
1168
1489
  { $limit: limit }
@@ -1171,9 +1492,109 @@ var MongoDBAdapter = class {
1171
1492
  total = data.reduce((sum, d) => sum + d.value, 0);
1172
1493
  break;
1173
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
+ }
1174
1595
  case "top_devices": {
1175
1596
  const rows = await this.collection.aggregate([
1176
- { $match: { ...baseMatch, device_type: { $ne: null } } },
1597
+ { $match: { ...baseMatch, ...filterMatch, device_type: { $ne: null } } },
1177
1598
  { $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
1178
1599
  { $project: { _id: 1, value: { $size: "$value" } } },
1179
1600
  { $sort: { value: -1 } },
@@ -1185,7 +1606,7 @@ var MongoDBAdapter = class {
1185
1606
  }
1186
1607
  case "top_browsers": {
1187
1608
  const rows = await this.collection.aggregate([
1188
- { $match: { ...baseMatch, browser: { $ne: null } } },
1609
+ { $match: { ...baseMatch, ...filterMatch, browser: { $ne: null } } },
1189
1610
  { $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
1190
1611
  { $project: { _id: 1, value: { $size: "$value" } } },
1191
1612
  { $sort: { value: -1 } },
@@ -1197,7 +1618,7 @@ var MongoDBAdapter = class {
1197
1618
  }
1198
1619
  case "top_os": {
1199
1620
  const rows = await this.collection.aggregate([
1200
- { $match: { ...baseMatch, os: { $ne: null } } },
1621
+ { $match: { ...baseMatch, ...filterMatch, os: { $ne: null } } },
1201
1622
  { $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
1202
1623
  { $project: { _id: 1, value: { $size: "$value" } } },
1203
1624
  { $sort: { value: -1 } },
@@ -1209,7 +1630,7 @@ var MongoDBAdapter = class {
1209
1630
  }
1210
1631
  }
1211
1632
  const result = { metric: q.metric, period, data, total };
1212
- if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
1633
+ if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
1213
1634
  const prevRange = previousPeriodRange(dateRange);
1214
1635
  const prevResult = await this.query({
1215
1636
  ...q,
@@ -1241,15 +1662,34 @@ var MongoDBAdapter = class {
1241
1662
  site_id: params.siteId,
1242
1663
  timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
1243
1664
  };
1665
+ const filterMatch = buildFilterMatch(params.filters);
1244
1666
  if (params.metric === "pageviews") {
1245
1667
  baseMatch.type = "pageview";
1246
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
+ }
1247
1687
  const dateFormat = granularityToDateFormat(granularity);
1248
1688
  let pipeline;
1249
1689
  if (params.metric === "visitors" || params.metric === "sessions") {
1250
1690
  const groupField = params.metric === "visitors" ? "$visitor_id" : "$session_id";
1251
1691
  pipeline = [
1252
- { $match: baseMatch },
1692
+ { $match: { ...baseMatch, ...filterMatch } },
1253
1693
  {
1254
1694
  $group: {
1255
1695
  _id: {
@@ -1268,7 +1708,7 @@ var MongoDBAdapter = class {
1268
1708
  ];
1269
1709
  } else {
1270
1710
  pipeline = [
1271
- { $match: baseMatch },
1711
+ { $match: { ...baseMatch, ...filterMatch } },
1272
1712
  {
1273
1713
  $group: {
1274
1714
  _id: { $dateToString: { format: dateFormat, date: "$timestamp" } },
@@ -1349,7 +1789,12 @@ var MongoDBAdapter = class {
1349
1789
  const offset = params.offset ?? 0;
1350
1790
  const match = { site_id: params.siteId };
1351
1791
  if (params.type) match.type = params.type;
1352
- 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;
1353
1798
  if (params.visitorId) match.visitor_id = params.visitorId;
1354
1799
  if (params.userId) match.user_id = params.userId;
1355
1800
  if (params.period || params.dateFrom) {
@@ -1384,6 +1829,7 @@ var MongoDBAdapter = class {
1384
1829
  }
1385
1830
  const pipeline = [
1386
1831
  { $match: match },
1832
+ { $sort: { timestamp: 1 } },
1387
1833
  {
1388
1834
  $group: {
1389
1835
  _id: "$visitor_id",
@@ -1395,13 +1841,22 @@ var MongoDBAdapter = class {
1395
1841
  totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
1396
1842
  sessions: { $addToSet: "$session_id" },
1397
1843
  lastUrl: { $last: "$url" },
1844
+ referrer: { $last: "$referrer" },
1398
1845
  device_type: { $last: "$device_type" },
1399
1846
  browser: { $last: "$browser" },
1400
1847
  os: { $last: "$os" },
1401
1848
  country: { $last: "$country" },
1402
1849
  city: { $last: "$city" },
1403
1850
  region: { $last: "$region" },
1404
- 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" }
1405
1860
  }
1406
1861
  },
1407
1862
  { $sort: { lastSeen: -1 } },
@@ -1423,9 +1878,19 @@ var MongoDBAdapter = class {
1423
1878
  totalPageviews: u.totalPageviews,
1424
1879
  totalSessions: u.sessions.length,
1425
1880
  lastUrl: u.lastUrl ?? void 0,
1881
+ referrer: u.referrer ?? void 0,
1426
1882
  device: u.device_type ? { type: u.device_type, browser: u.browser ?? "", os: u.os ?? "" } : void 0,
1427
1883
  geo: u.country ? { country: u.country, city: u.city ?? void 0, region: u.region ?? void 0 } : void 0,
1428
- 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
1429
1894
  }));
1430
1895
  return {
1431
1896
  users,
@@ -1454,6 +1919,13 @@ var MongoDBAdapter = class {
1454
1919
  title: doc.title ?? void 0,
1455
1920
  name: doc.event_name ?? void 0,
1456
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,
1457
1929
  userId: doc.user_id ?? void 0,
1458
1930
  traits: doc.traits ?? void 0,
1459
1931
  geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
@@ -1477,6 +1949,7 @@ var MongoDBAdapter = class {
1477
1949
  name: data.name,
1478
1950
  domain: data.domain ?? null,
1479
1951
  allowed_origins: data.allowedOrigins ?? null,
1952
+ conversion_events: data.conversionEvents ?? null,
1480
1953
  created_at: now,
1481
1954
  updated_at: now
1482
1955
  };
@@ -1500,6 +1973,7 @@ var MongoDBAdapter = class {
1500
1973
  if (data.name !== void 0) updates.name = data.name;
1501
1974
  if (data.domain !== void 0) updates.domain = data.domain || null;
1502
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;
1503
1977
  const result = await this.sites.findOneAndUpdate(
1504
1978
  { site_id: siteId },
1505
1979
  { $set: updates },
@@ -1530,6 +2004,7 @@ var MongoDBAdapter = class {
1530
2004
  name: doc.name,
1531
2005
  domain: doc.domain ?? void 0,
1532
2006
  allowedOrigins: doc.allowed_origins ?? void 0,
2007
+ conversionEvents: doc.conversion_events ?? void 0,
1533
2008
  createdAt: doc.created_at.toISOString(),
1534
2009
  updatedAt: doc.updated_at.toISOString()
1535
2010
  };
@@ -1700,6 +2175,63 @@ function resolveDeviceType(type) {
1700
2175
  return "desktop";
1701
2176
  }
1702
2177
 
2178
+ // src/botfilter.ts
2179
+ var BOT_PATTERNS = [
2180
+ // Headless browsers
2181
+ /HeadlessChrome/i,
2182
+ /PhantomJS/i,
2183
+ /Selenium/i,
2184
+ /Puppeteer/i,
2185
+ /Playwright/i,
2186
+ // Common bots
2187
+ /bot\b/i,
2188
+ /spider/i,
2189
+ /crawl/i,
2190
+ /slurp/i,
2191
+ /mediapartners/i,
2192
+ /facebookexternalhit/i,
2193
+ /Twitterbot/i,
2194
+ /LinkedInBot/i,
2195
+ /WhatsApp/i,
2196
+ /Discordbot/i,
2197
+ /TelegramBot/i,
2198
+ /Applebot/i,
2199
+ /Baiduspider/i,
2200
+ /YandexBot/i,
2201
+ /DuckDuckBot/i,
2202
+ /Sogou/i,
2203
+ /Exabot/i,
2204
+ /ia_archiver/i,
2205
+ // HTTP libraries & API tools
2206
+ /PostmanRuntime/i,
2207
+ /axios/i,
2208
+ /node-fetch/i,
2209
+ /python-requests/i,
2210
+ /Go-http-client/i,
2211
+ /Java\//i,
2212
+ /libwww-perl/i,
2213
+ /wget/i,
2214
+ /curl/i,
2215
+ /httpie/i,
2216
+ // Monitoring / uptime
2217
+ /UptimeRobot/i,
2218
+ /Pingdom/i,
2219
+ /StatusCake/i,
2220
+ /Site24x7/i,
2221
+ /NewRelic/i,
2222
+ /Datadog/i,
2223
+ // Preview/embed
2224
+ /Slackbot/i,
2225
+ /Embedly/i,
2226
+ /Quora Link Preview/i,
2227
+ /redditbot/i,
2228
+ /Pinterestbot/i
2229
+ ];
2230
+ function isBot(ua) {
2231
+ if (!ua || ua.length === 0) return true;
2232
+ return BOT_PATTERNS.some((re) => re.test(ua));
2233
+ }
2234
+
1703
2235
  // src/collector.ts
1704
2236
  async function createCollector(config) {
1705
2237
  const db = createAdapter(config.db);
@@ -1726,6 +2258,7 @@ async function createCollector(config) {
1726
2258
  if (allowed) {
1727
2259
  res.setHeader?.("Access-Control-Allow-Origin", origin || "*");
1728
2260
  res.setHeader?.("Access-Control-Allow-Methods", methods);
2261
+ res.setHeader?.("Access-Control-Allow-Credentials", "true");
1729
2262
  const headers = ["Content-Type", extraHeaders].filter(Boolean).join(", ");
1730
2263
  res.setHeader?.("Access-Control-Allow-Headers", headers);
1731
2264
  }
@@ -1756,7 +2289,14 @@ async function createCollector(config) {
1756
2289
  }
1757
2290
  function handler() {
1758
2291
  return async (req, res) => {
1759
- 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
+ }
1760
2300
  if (req.method !== "POST") {
1761
2301
  sendJson(res, 405, { ok: false, error: "Method not allowed" });
1762
2302
  return;
@@ -1772,9 +2312,36 @@ async function createCollector(config) {
1772
2312
  sendJson(res, 400, { ok: false, error: "Too many events (max 100)" });
1773
2313
  return;
1774
2314
  }
1775
- const ip = extractIp(req);
1776
2315
  const userAgent = req.headers?.["user-agent"] || "";
2316
+ if (isBot(userAgent)) {
2317
+ sendJson(res, 200, { ok: true });
2318
+ return;
2319
+ }
2320
+ const ip = extractIp(req);
1777
2321
  const enriched = enrichEvents(payload.events, ip, userAgent);
2322
+ const siteId = enriched[0]?.siteId;
2323
+ if (siteId) {
2324
+ const site = await db.getSite(siteId);
2325
+ if (site?.allowedOrigins && site.allowedOrigins.length > 0) {
2326
+ const allowed = new Set(site.allowedOrigins.map((h) => h.toLowerCase()));
2327
+ const filtered = enriched.filter((event) => {
2328
+ if (!event.url) return true;
2329
+ try {
2330
+ const hostname = new URL(event.url).hostname.toLowerCase();
2331
+ return allowed.has(hostname);
2332
+ } catch {
2333
+ return true;
2334
+ }
2335
+ });
2336
+ if (filtered.length === 0) {
2337
+ sendJson(res, 200, { ok: true });
2338
+ return;
2339
+ }
2340
+ await db.insertEvents(filtered);
2341
+ sendJson(res, 200, { ok: true });
2342
+ return;
2343
+ }
2344
+ }
1778
2345
  await db.insertEvents(enriched);
1779
2346
  sendJson(res, 200, { ok: true });
1780
2347
  } catch (err) {
@@ -1808,8 +2375,13 @@ async function createCollector(config) {
1808
2375
  period: params.period,
1809
2376
  dateFrom: params.dateFrom,
1810
2377
  dateTo: params.dateTo,
1811
- granularity: q.granularity
2378
+ granularity: q.granularity,
2379
+ filters: q.filters ? JSON.parse(q.filters) : void 0
1812
2380
  };
2381
+ if (tsParams.metric === "conversions") {
2382
+ const site = await db.getSite(params.siteId);
2383
+ tsParams.conversionEvents = site?.conversionEvents ?? [];
2384
+ }
1813
2385
  const result2 = await db.queryTimeSeries(tsParams);
1814
2386
  sendJson(res, 200, result2);
1815
2387
  return;
@@ -1825,7 +2397,15 @@ async function createCollector(config) {
1825
2397
  sendJson(res, 200, result2);
1826
2398
  return;
1827
2399
  }
1828
- 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
+ }
1829
2409
  sendJson(res, 200, result);
1830
2410
  } catch (err) {
1831
2411
  sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : "Internal error" });
@@ -1922,10 +2502,13 @@ async function createCollector(config) {
1922
2502
  sendJson(res, 401, { ok: false, error: "Invalid or missing secret key" });
1923
2503
  return;
1924
2504
  }
2505
+ const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1925
2506
  const params = {
1926
2507
  siteId: q.siteId,
1927
2508
  type: q.type,
1928
2509
  eventName: q.eventName,
2510
+ eventNames,
2511
+ eventSource: q.eventSource,
1929
2512
  visitorId: q.visitorId,
1930
2513
  userId: q.userId,
1931
2514
  period: q.period,
@@ -1965,9 +2548,13 @@ async function createCollector(config) {
1965
2548
  const visitorId = usersIdx >= 0 ? pathSegments[usersIdx + 1] : void 0;
1966
2549
  const action = usersIdx >= 0 ? pathSegments[usersIdx + 2] : void 0;
1967
2550
  if (visitorId && action === "events") {
2551
+ const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1968
2552
  const params2 = {
1969
2553
  siteId: q.siteId,
1970
2554
  type: q.type,
2555
+ eventName: q.eventName,
2556
+ eventNames,
2557
+ eventSource: q.eventSource,
1971
2558
  period: q.period,
1972
2559
  dateFrom: q.dateFrom,
1973
2560
  dateTo: q.dateTo,
@@ -2074,7 +2661,8 @@ function createAdapter(config) {
2074
2661
  }
2075
2662
  }
2076
2663
  async function parseBody(req) {
2077
- 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);
2078
2666
  return new Promise((resolve, reject) => {
2079
2667
  let data = "";
2080
2668
  req.on("data", (chunk) => {
@@ -2115,6 +2703,7 @@ function sendJson(res, status, body) {
2115
2703
  0 && (module.exports = {
2116
2704
  ClickHouseAdapter,
2117
2705
  MongoDBAdapter,
2118
- createCollector
2706
+ createCollector,
2707
+ isBot
2119
2708
  });
2120
2709
  //# sourceMappingURL=index.cjs.map