@litemetrics/node 0.1.1 → 0.1.3
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/README.md +25 -1
- package/dist/index.cjs +1088 -103
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -6
- package/dist/index.d.ts +17 -6
- package/dist/index.js +1088 -103
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -179,6 +179,18 @@ function generateSecretKey() {
|
|
|
179
179
|
// src/adapters/clickhouse.ts
|
|
180
180
|
var EVENTS_TABLE = "litemetrics_events";
|
|
181
181
|
var SITES_TABLE = "litemetrics_sites";
|
|
182
|
+
var IDENTITY_MAP_TABLE = "litemetrics_identity_map";
|
|
183
|
+
var CREATE_IDENTITY_MAP_TABLE = `
|
|
184
|
+
CREATE TABLE IF NOT EXISTS ${IDENTITY_MAP_TABLE} (
|
|
185
|
+
site_id LowCardinality(String),
|
|
186
|
+
visitor_id String,
|
|
187
|
+
user_id String,
|
|
188
|
+
identified_at DateTime64(3),
|
|
189
|
+
created_at DateTime64(3) DEFAULT now64(3)
|
|
190
|
+
) ENGINE = ReplacingMergeTree(created_at)
|
|
191
|
+
ORDER BY (site_id, visitor_id)
|
|
192
|
+
SETTINGS index_granularity = 8192
|
|
193
|
+
`;
|
|
182
194
|
var CREATE_EVENTS_TABLE = `
|
|
183
195
|
CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
184
196
|
event_id UUID DEFAULT generateUUIDv4(),
|
|
@@ -192,6 +204,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
|
192
204
|
title Nullable(String),
|
|
193
205
|
event_name Nullable(String),
|
|
194
206
|
properties Nullable(String),
|
|
207
|
+
event_source LowCardinality(Nullable(String)),
|
|
208
|
+
event_subtype LowCardinality(Nullable(String)),
|
|
209
|
+
page_path Nullable(String),
|
|
210
|
+
target_url_path Nullable(String),
|
|
211
|
+
element_selector Nullable(String),
|
|
212
|
+
element_text Nullable(String),
|
|
213
|
+
scroll_depth_pct Nullable(UInt8),
|
|
195
214
|
user_id Nullable(String),
|
|
196
215
|
traits Nullable(String),
|
|
197
216
|
country LowCardinality(Nullable(String)),
|
|
@@ -223,6 +242,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
223
242
|
name String,
|
|
224
243
|
domain Nullable(String),
|
|
225
244
|
allowed_origins Nullable(String),
|
|
245
|
+
conversion_events Nullable(String),
|
|
226
246
|
created_at DateTime64(3),
|
|
227
247
|
updated_at DateTime64(3),
|
|
228
248
|
version UInt64,
|
|
@@ -235,6 +255,39 @@ function toCHDateTime(d) {
|
|
|
235
255
|
const iso = typeof d === "string" ? d : d.toISOString();
|
|
236
256
|
return iso.replace("T", " ").replace("Z", "");
|
|
237
257
|
}
|
|
258
|
+
function buildFilterConditions(filters) {
|
|
259
|
+
if (!filters) return { conditions: [], params: {} };
|
|
260
|
+
const map = {
|
|
261
|
+
"geo.country": "country",
|
|
262
|
+
"geo.city": "city",
|
|
263
|
+
"geo.region": "region",
|
|
264
|
+
"language": "language",
|
|
265
|
+
"device.type": "device_type",
|
|
266
|
+
"device.browser": "browser",
|
|
267
|
+
"device.os": "os",
|
|
268
|
+
"utm.source": "utm_source",
|
|
269
|
+
"utm.medium": "utm_medium",
|
|
270
|
+
"utm.campaign": "utm_campaign",
|
|
271
|
+
"utm.term": "utm_term",
|
|
272
|
+
"utm.content": "utm_content",
|
|
273
|
+
"referrer": "referrer",
|
|
274
|
+
"event_source": "event_source",
|
|
275
|
+
"event_subtype": "event_subtype",
|
|
276
|
+
"page_path": "page_path",
|
|
277
|
+
"target_url_path": "target_url_path",
|
|
278
|
+
"event_name": "event_name",
|
|
279
|
+
"type": "type"
|
|
280
|
+
};
|
|
281
|
+
const conditions = [];
|
|
282
|
+
const params = {};
|
|
283
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
284
|
+
if (!value || !map[key]) continue;
|
|
285
|
+
const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
286
|
+
conditions.push(`${map[key]} = {${paramKey}:String}`);
|
|
287
|
+
params[paramKey] = value;
|
|
288
|
+
}
|
|
289
|
+
return { conditions, params };
|
|
290
|
+
}
|
|
238
291
|
var ClickHouseAdapter = class {
|
|
239
292
|
client;
|
|
240
293
|
constructor(url) {
|
|
@@ -248,6 +301,17 @@ var ClickHouseAdapter = class {
|
|
|
248
301
|
async init() {
|
|
249
302
|
await this.client.command({ query: CREATE_EVENTS_TABLE });
|
|
250
303
|
await this.client.command({ query: CREATE_SITES_TABLE });
|
|
304
|
+
await this.client.command({ query: CREATE_IDENTITY_MAP_TABLE });
|
|
305
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_source LowCardinality(Nullable(String))` });
|
|
306
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_subtype LowCardinality(Nullable(String))` });
|
|
307
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS page_path Nullable(String)` });
|
|
308
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS target_url_path Nullable(String)` });
|
|
309
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_selector Nullable(String)` });
|
|
310
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_text Nullable(String)` });
|
|
311
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS scroll_depth_pct Nullable(UInt8)` });
|
|
312
|
+
await this.client.command({
|
|
313
|
+
query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
|
|
314
|
+
});
|
|
251
315
|
}
|
|
252
316
|
async close() {
|
|
253
317
|
await this.client.close();
|
|
@@ -266,6 +330,13 @@ var ClickHouseAdapter = class {
|
|
|
266
330
|
title: e.title ?? null,
|
|
267
331
|
event_name: e.name ?? null,
|
|
268
332
|
properties: e.properties ? JSON.stringify(e.properties) : null,
|
|
333
|
+
event_source: e.eventSource ?? null,
|
|
334
|
+
event_subtype: e.eventSubtype ?? null,
|
|
335
|
+
page_path: e.pagePath ?? null,
|
|
336
|
+
target_url_path: e.targetUrlPath ?? null,
|
|
337
|
+
element_selector: e.elementSelector ?? null,
|
|
338
|
+
element_text: e.elementText ?? null,
|
|
339
|
+
scroll_depth_pct: e.scrollDepthPct ?? null,
|
|
269
340
|
user_id: e.userId ?? null,
|
|
270
341
|
traits: e.traits ? JSON.stringify(e.traits) : null,
|
|
271
342
|
country: e.geo?.country ?? null,
|
|
@@ -302,6 +373,8 @@ var ClickHouseAdapter = class {
|
|
|
302
373
|
to: toCHDateTime(dateRange.to),
|
|
303
374
|
limit
|
|
304
375
|
};
|
|
376
|
+
const filter = buildFilterConditions(q.filters);
|
|
377
|
+
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
305
378
|
let data = [];
|
|
306
379
|
let total = 0;
|
|
307
380
|
switch (q.metric) {
|
|
@@ -311,8 +384,8 @@ var ClickHouseAdapter = class {
|
|
|
311
384
|
WHERE site_id = {siteId:String}
|
|
312
385
|
AND timestamp >= {from:String}
|
|
313
386
|
AND timestamp <= {to:String}
|
|
314
|
-
AND type = 'pageview'`,
|
|
315
|
-
params
|
|
387
|
+
AND type = 'pageview'${filterSql}`,
|
|
388
|
+
{ ...params, ...filter.params }
|
|
316
389
|
);
|
|
317
390
|
total = Number(rows[0]?.value ?? 0);
|
|
318
391
|
data = [{ key: "pageviews", value: total }];
|
|
@@ -323,8 +396,8 @@ var ClickHouseAdapter = class {
|
|
|
323
396
|
`SELECT uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
324
397
|
WHERE site_id = {siteId:String}
|
|
325
398
|
AND timestamp >= {from:String}
|
|
326
|
-
AND timestamp <= {to:String}`,
|
|
327
|
-
params
|
|
399
|
+
AND timestamp <= {to:String}${filterSql}`,
|
|
400
|
+
{ ...params, ...filter.params }
|
|
328
401
|
);
|
|
329
402
|
total = Number(rows[0]?.value ?? 0);
|
|
330
403
|
data = [{ key: "visitors", value: total }];
|
|
@@ -335,8 +408,8 @@ var ClickHouseAdapter = class {
|
|
|
335
408
|
`SELECT uniq(session_id) AS value FROM ${EVENTS_TABLE}
|
|
336
409
|
WHERE site_id = {siteId:String}
|
|
337
410
|
AND timestamp >= {from:String}
|
|
338
|
-
AND timestamp <= {to:String}`,
|
|
339
|
-
params
|
|
411
|
+
AND timestamp <= {to:String}${filterSql}`,
|
|
412
|
+
{ ...params, ...filter.params }
|
|
340
413
|
);
|
|
341
414
|
total = Number(rows[0]?.value ?? 0);
|
|
342
415
|
data = [{ key: "sessions", value: total }];
|
|
@@ -348,13 +421,33 @@ var ClickHouseAdapter = class {
|
|
|
348
421
|
WHERE site_id = {siteId:String}
|
|
349
422
|
AND timestamp >= {from:String}
|
|
350
423
|
AND timestamp <= {to:String}
|
|
351
|
-
AND type = 'event'`,
|
|
352
|
-
params
|
|
424
|
+
AND type = 'event'${filterSql}`,
|
|
425
|
+
{ ...params, ...filter.params }
|
|
353
426
|
);
|
|
354
427
|
total = Number(rows[0]?.value ?? 0);
|
|
355
428
|
data = [{ key: "events", value: total }];
|
|
356
429
|
break;
|
|
357
430
|
}
|
|
431
|
+
case "conversions": {
|
|
432
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
433
|
+
if (conversionEvents.length === 0) {
|
|
434
|
+
total = 0;
|
|
435
|
+
data = [{ key: "conversions", value: 0 }];
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
const rows = await this.queryRows(
|
|
439
|
+
`SELECT count() AS value FROM ${EVENTS_TABLE}
|
|
440
|
+
WHERE site_id = {siteId:String}
|
|
441
|
+
AND timestamp >= {from:String}
|
|
442
|
+
AND timestamp <= {to:String}
|
|
443
|
+
AND type = 'event'
|
|
444
|
+
AND event_name IN {eventNames:Array(String)}${filterSql}`,
|
|
445
|
+
{ ...params, eventNames: conversionEvents, ...filter.params }
|
|
446
|
+
);
|
|
447
|
+
total = Number(rows[0]?.value ?? 0);
|
|
448
|
+
data = [{ key: "conversions", value: total }];
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
358
451
|
case "top_pages": {
|
|
359
452
|
const rows = await this.queryRows(
|
|
360
453
|
`SELECT url AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
@@ -362,11 +455,11 @@ var ClickHouseAdapter = class {
|
|
|
362
455
|
AND timestamp >= {from:String}
|
|
363
456
|
AND timestamp <= {to:String}
|
|
364
457
|
AND type = 'pageview'
|
|
365
|
-
AND url IS NOT NULL
|
|
458
|
+
AND url IS NOT NULL${filterSql}
|
|
366
459
|
GROUP BY url
|
|
367
460
|
ORDER BY value DESC
|
|
368
461
|
LIMIT {limit:UInt32}`,
|
|
369
|
-
params
|
|
462
|
+
{ ...params, ...filter.params }
|
|
370
463
|
);
|
|
371
464
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
372
465
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -380,11 +473,11 @@ var ClickHouseAdapter = class {
|
|
|
380
473
|
AND timestamp <= {to:String}
|
|
381
474
|
AND type = 'pageview'
|
|
382
475
|
AND referrer IS NOT NULL
|
|
383
|
-
AND referrer != ''
|
|
476
|
+
AND referrer != ''${filterSql}
|
|
384
477
|
GROUP BY referrer
|
|
385
478
|
ORDER BY value DESC
|
|
386
479
|
LIMIT {limit:UInt32}`,
|
|
387
|
-
params
|
|
480
|
+
{ ...params, ...filter.params }
|
|
388
481
|
);
|
|
389
482
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
390
483
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -396,11 +489,11 @@ var ClickHouseAdapter = class {
|
|
|
396
489
|
WHERE site_id = {siteId:String}
|
|
397
490
|
AND timestamp >= {from:String}
|
|
398
491
|
AND timestamp <= {to:String}
|
|
399
|
-
AND country IS NOT NULL
|
|
492
|
+
AND country IS NOT NULL${filterSql}
|
|
400
493
|
GROUP BY country
|
|
401
494
|
ORDER BY value DESC
|
|
402
495
|
LIMIT {limit:UInt32}`,
|
|
403
|
-
params
|
|
496
|
+
{ ...params, ...filter.params }
|
|
404
497
|
);
|
|
405
498
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
406
499
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -412,11 +505,11 @@ var ClickHouseAdapter = class {
|
|
|
412
505
|
WHERE site_id = {siteId:String}
|
|
413
506
|
AND timestamp >= {from:String}
|
|
414
507
|
AND timestamp <= {to:String}
|
|
415
|
-
AND city IS NOT NULL
|
|
508
|
+
AND city IS NOT NULL${filterSql}
|
|
416
509
|
GROUP BY city
|
|
417
510
|
ORDER BY value DESC
|
|
418
511
|
LIMIT {limit:UInt32}`,
|
|
419
|
-
params
|
|
512
|
+
{ ...params, ...filter.params }
|
|
420
513
|
);
|
|
421
514
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
422
515
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -430,10 +523,132 @@ var ClickHouseAdapter = class {
|
|
|
430
523
|
AND timestamp <= {to:String}
|
|
431
524
|
AND type = 'event'
|
|
432
525
|
AND event_name IS NOT NULL
|
|
526
|
+
${filterSql}
|
|
527
|
+
GROUP BY event_name
|
|
528
|
+
ORDER BY value DESC
|
|
529
|
+
LIMIT {limit:UInt32}`,
|
|
530
|
+
{ ...params, ...filter.params }
|
|
531
|
+
);
|
|
532
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
533
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case "top_conversions": {
|
|
537
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
538
|
+
if (conversionEvents.length === 0) {
|
|
539
|
+
total = 0;
|
|
540
|
+
data = [];
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
const rows = await this.queryRows(
|
|
544
|
+
`SELECT event_name AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
545
|
+
WHERE site_id = {siteId:String}
|
|
546
|
+
AND timestamp >= {from:String}
|
|
547
|
+
AND timestamp <= {to:String}
|
|
548
|
+
AND type = 'event'
|
|
549
|
+
AND event_name IN {eventNames:Array(String)}
|
|
550
|
+
${filterSql}
|
|
433
551
|
GROUP BY event_name
|
|
434
552
|
ORDER BY value DESC
|
|
435
553
|
LIMIT {limit:UInt32}`,
|
|
436
|
-
params
|
|
554
|
+
{ ...params, eventNames: conversionEvents, ...filter.params }
|
|
555
|
+
);
|
|
556
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
557
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
case "top_exit_pages": {
|
|
561
|
+
const rows = await this.queryRows(
|
|
562
|
+
`SELECT exit_url AS key, count() AS value FROM (
|
|
563
|
+
SELECT session_id, argMax(url, timestamp) AS exit_url
|
|
564
|
+
FROM ${EVENTS_TABLE}
|
|
565
|
+
WHERE site_id = {siteId:String}
|
|
566
|
+
AND timestamp >= {from:String}
|
|
567
|
+
AND timestamp <= {to:String}
|
|
568
|
+
AND type = 'pageview'
|
|
569
|
+
AND url IS NOT NULL${filterSql}
|
|
570
|
+
GROUP BY session_id
|
|
571
|
+
)
|
|
572
|
+
GROUP BY exit_url
|
|
573
|
+
ORDER BY value DESC
|
|
574
|
+
LIMIT {limit:UInt32}`,
|
|
575
|
+
{ ...params, ...filter.params }
|
|
576
|
+
);
|
|
577
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
578
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case "top_transitions": {
|
|
582
|
+
const rows = await this.queryRows(
|
|
583
|
+
`SELECT concat(prev_url, ' \u2192 ', curr_url) AS key, count() AS value FROM (
|
|
584
|
+
SELECT session_id, url AS curr_url,
|
|
585
|
+
lagInFrame(url, 1) OVER (PARTITION BY session_id ORDER BY timestamp ASC) AS prev_url
|
|
586
|
+
FROM ${EVENTS_TABLE}
|
|
587
|
+
WHERE site_id = {siteId:String}
|
|
588
|
+
AND timestamp >= {from:String}
|
|
589
|
+
AND timestamp <= {to:String}
|
|
590
|
+
AND type = 'pageview'
|
|
591
|
+
AND url IS NOT NULL${filterSql}
|
|
592
|
+
)
|
|
593
|
+
WHERE prev_url IS NOT NULL AND prev_url != ''
|
|
594
|
+
GROUP BY key
|
|
595
|
+
ORDER BY value DESC
|
|
596
|
+
LIMIT {limit:UInt32}`,
|
|
597
|
+
{ ...params, ...filter.params }
|
|
598
|
+
);
|
|
599
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
600
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case "top_scroll_pages": {
|
|
604
|
+
const rows = await this.queryRows(
|
|
605
|
+
`SELECT page_path AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
606
|
+
WHERE site_id = {siteId:String}
|
|
607
|
+
AND timestamp >= {from:String}
|
|
608
|
+
AND timestamp <= {to:String}
|
|
609
|
+
AND type = 'event'
|
|
610
|
+
AND event_subtype = 'scroll_depth'
|
|
611
|
+
AND page_path IS NOT NULL${filterSql}
|
|
612
|
+
GROUP BY page_path
|
|
613
|
+
ORDER BY value DESC
|
|
614
|
+
LIMIT {limit:UInt32}`,
|
|
615
|
+
{ ...params, ...filter.params }
|
|
616
|
+
);
|
|
617
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
618
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
case "top_button_clicks": {
|
|
622
|
+
const rows = await this.queryRows(
|
|
623
|
+
`SELECT ifNull(element_text, element_selector) AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
624
|
+
WHERE site_id = {siteId:String}
|
|
625
|
+
AND timestamp >= {from:String}
|
|
626
|
+
AND timestamp <= {to:String}
|
|
627
|
+
AND type = 'event'
|
|
628
|
+
AND event_subtype = 'button_click'
|
|
629
|
+
AND (element_text IS NOT NULL OR element_selector IS NOT NULL)${filterSql}
|
|
630
|
+
GROUP BY key
|
|
631
|
+
ORDER BY value DESC
|
|
632
|
+
LIMIT {limit:UInt32}`,
|
|
633
|
+
{ ...params, ...filter.params }
|
|
634
|
+
);
|
|
635
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
636
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case "top_link_targets": {
|
|
640
|
+
const rows = await this.queryRows(
|
|
641
|
+
`SELECT target_url_path AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
642
|
+
WHERE site_id = {siteId:String}
|
|
643
|
+
AND timestamp >= {from:String}
|
|
644
|
+
AND timestamp <= {to:String}
|
|
645
|
+
AND type = 'event'
|
|
646
|
+
AND event_subtype IN ('link_click','outbound_click')
|
|
647
|
+
AND target_url_path IS NOT NULL${filterSql}
|
|
648
|
+
GROUP BY target_url_path
|
|
649
|
+
ORDER BY value DESC
|
|
650
|
+
LIMIT {limit:UInt32}`,
|
|
651
|
+
{ ...params, ...filter.params }
|
|
437
652
|
);
|
|
438
653
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
439
654
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -446,10 +661,11 @@ var ClickHouseAdapter = class {
|
|
|
446
661
|
AND timestamp >= {from:String}
|
|
447
662
|
AND timestamp <= {to:String}
|
|
448
663
|
AND device_type IS NOT NULL
|
|
664
|
+
${filterSql}
|
|
449
665
|
GROUP BY device_type
|
|
450
666
|
ORDER BY value DESC
|
|
451
667
|
LIMIT {limit:UInt32}`,
|
|
452
|
-
params
|
|
668
|
+
{ ...params, ...filter.params }
|
|
453
669
|
);
|
|
454
670
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
455
671
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -462,10 +678,11 @@ var ClickHouseAdapter = class {
|
|
|
462
678
|
AND timestamp >= {from:String}
|
|
463
679
|
AND timestamp <= {to:String}
|
|
464
680
|
AND browser IS NOT NULL
|
|
681
|
+
${filterSql}
|
|
465
682
|
GROUP BY browser
|
|
466
683
|
ORDER BY value DESC
|
|
467
684
|
LIMIT {limit:UInt32}`,
|
|
468
|
-
params
|
|
685
|
+
{ ...params, ...filter.params }
|
|
469
686
|
);
|
|
470
687
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
471
688
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -478,10 +695,62 @@ var ClickHouseAdapter = class {
|
|
|
478
695
|
AND timestamp >= {from:String}
|
|
479
696
|
AND timestamp <= {to:String}
|
|
480
697
|
AND os IS NOT NULL
|
|
698
|
+
${filterSql}
|
|
481
699
|
GROUP BY os
|
|
482
700
|
ORDER BY value DESC
|
|
483
701
|
LIMIT {limit:UInt32}`,
|
|
484
|
-
params
|
|
702
|
+
{ ...params, ...filter.params }
|
|
703
|
+
);
|
|
704
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
705
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case "top_utm_sources": {
|
|
709
|
+
const rows = await this.queryRows(
|
|
710
|
+
`SELECT utm_source AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
711
|
+
WHERE site_id = {siteId:String}
|
|
712
|
+
AND timestamp >= {from:String}
|
|
713
|
+
AND timestamp <= {to:String}
|
|
714
|
+
AND utm_source IS NOT NULL AND utm_source != ''
|
|
715
|
+
${filterSql}
|
|
716
|
+
GROUP BY utm_source
|
|
717
|
+
ORDER BY value DESC
|
|
718
|
+
LIMIT {limit:UInt32}`,
|
|
719
|
+
{ ...params, ...filter.params }
|
|
720
|
+
);
|
|
721
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
722
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case "top_utm_mediums": {
|
|
726
|
+
const rows = await this.queryRows(
|
|
727
|
+
`SELECT utm_medium AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
728
|
+
WHERE site_id = {siteId:String}
|
|
729
|
+
AND timestamp >= {from:String}
|
|
730
|
+
AND timestamp <= {to:String}
|
|
731
|
+
AND utm_medium IS NOT NULL AND utm_medium != ''
|
|
732
|
+
${filterSql}
|
|
733
|
+
GROUP BY utm_medium
|
|
734
|
+
ORDER BY value DESC
|
|
735
|
+
LIMIT {limit:UInt32}`,
|
|
736
|
+
{ ...params, ...filter.params }
|
|
737
|
+
);
|
|
738
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
739
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case "top_utm_campaigns": {
|
|
743
|
+
const rows = await this.queryRows(
|
|
744
|
+
`SELECT utm_campaign AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
745
|
+
WHERE site_id = {siteId:String}
|
|
746
|
+
AND timestamp >= {from:String}
|
|
747
|
+
AND timestamp <= {to:String}
|
|
748
|
+
AND utm_campaign IS NOT NULL AND utm_campaign != ''
|
|
749
|
+
${filterSql}
|
|
750
|
+
GROUP BY utm_campaign
|
|
751
|
+
ORDER BY value DESC
|
|
752
|
+
LIMIT {limit:UInt32}`,
|
|
753
|
+
{ ...params, ...filter.params }
|
|
485
754
|
);
|
|
486
755
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
487
756
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -489,7 +758,7 @@ var ClickHouseAdapter = class {
|
|
|
489
758
|
}
|
|
490
759
|
}
|
|
491
760
|
const result = { metric: q.metric, period, data, total };
|
|
492
|
-
if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
|
|
761
|
+
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
493
762
|
const prevRange = previousPeriodRange(dateRange);
|
|
494
763
|
const prevResult = await this.query({
|
|
495
764
|
...q,
|
|
@@ -519,7 +788,12 @@ var ClickHouseAdapter = class {
|
|
|
519
788
|
const granularity = params.granularity ?? autoGranularity(period);
|
|
520
789
|
const bucketFn = this.granularityToClickHouseFunc(granularity);
|
|
521
790
|
const dateFormat = granularityToDateFormat(granularity);
|
|
791
|
+
const filter = buildFilterConditions(params.filters);
|
|
792
|
+
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
522
793
|
const typeFilter = params.metric === "pageviews" ? `AND type = 'pageview'` : "";
|
|
794
|
+
const eventsFilter = params.metric === "events" ? `AND type = 'event'` : "";
|
|
795
|
+
const conversionsFilter = params.metric === "conversions" ? `AND type = 'event' AND event_name IN {eventNames:Array(String)}` : "";
|
|
796
|
+
const extraFilters = [typeFilter, eventsFilter, conversionsFilter, filterSql].filter(Boolean).join(" ");
|
|
523
797
|
let sql;
|
|
524
798
|
if (params.metric === "visitors" || params.metric === "sessions") {
|
|
525
799
|
const field = params.metric === "visitors" ? "visitor_id" : "session_id";
|
|
@@ -529,7 +803,7 @@ var ClickHouseAdapter = class {
|
|
|
529
803
|
WHERE site_id = {siteId:String}
|
|
530
804
|
AND timestamp >= {from:String}
|
|
531
805
|
AND timestamp <= {to:String}
|
|
532
|
-
${
|
|
806
|
+
${extraFilters}
|
|
533
807
|
GROUP BY bucket
|
|
534
808
|
ORDER BY bucket ASC
|
|
535
809
|
`;
|
|
@@ -540,7 +814,7 @@ var ClickHouseAdapter = class {
|
|
|
540
814
|
WHERE site_id = {siteId:String}
|
|
541
815
|
AND timestamp >= {from:String}
|
|
542
816
|
AND timestamp <= {to:String}
|
|
543
|
-
${
|
|
817
|
+
${extraFilters}
|
|
544
818
|
GROUP BY bucket
|
|
545
819
|
ORDER BY bucket ASC
|
|
546
820
|
`;
|
|
@@ -548,7 +822,9 @@ var ClickHouseAdapter = class {
|
|
|
548
822
|
const rows = await this.queryRows(sql, {
|
|
549
823
|
siteId: params.siteId,
|
|
550
824
|
from: toCHDateTime(dateRange.from),
|
|
551
|
-
to: toCHDateTime(dateRange.to)
|
|
825
|
+
to: toCHDateTime(dateRange.to),
|
|
826
|
+
eventNames: params.conversionEvents ?? [],
|
|
827
|
+
...filter.params
|
|
552
828
|
});
|
|
553
829
|
const mappedRows = rows.map((r) => ({
|
|
554
830
|
_id: this.convertClickHouseBucket(r.bucket, granularity),
|
|
@@ -666,6 +942,14 @@ var ClickHouseAdapter = class {
|
|
|
666
942
|
conditions.push(`event_name = {eventName:String}`);
|
|
667
943
|
queryParams.eventName = params.eventName;
|
|
668
944
|
}
|
|
945
|
+
if (params.eventSource) {
|
|
946
|
+
conditions.push(`event_source = {eventSource:String}`);
|
|
947
|
+
queryParams.eventSource = params.eventSource;
|
|
948
|
+
}
|
|
949
|
+
if (params.eventNames && params.eventNames.length > 0) {
|
|
950
|
+
conditions.push(`event_name IN {eventNames:Array(String)}`);
|
|
951
|
+
queryParams.eventNames = params.eventNames;
|
|
952
|
+
}
|
|
669
953
|
if (params.visitorId) {
|
|
670
954
|
conditions.push(`visitor_id = {visitorId:String}`);
|
|
671
955
|
queryParams.visitorId = params.visitorId;
|
|
@@ -688,7 +972,9 @@ var ClickHouseAdapter = class {
|
|
|
688
972
|
const [events, countRows] = await Promise.all([
|
|
689
973
|
this.queryRows(
|
|
690
974
|
`SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
|
|
691
|
-
event_name, properties,
|
|
975
|
+
event_name, properties, event_source, event_subtype, page_path, target_url_path,
|
|
976
|
+
element_selector, element_text, scroll_depth_pct,
|
|
977
|
+
user_id, traits, country, city, region,
|
|
692
978
|
device_type, browser, os, language,
|
|
693
979
|
utm_source, utm_medium, utm_campaign, utm_term, utm_content
|
|
694
980
|
FROM ${EVENTS_TABLE}
|
|
@@ -723,36 +1009,59 @@ var ClickHouseAdapter = class {
|
|
|
723
1009
|
const where = conditions.join(" AND ");
|
|
724
1010
|
const [userRows, countRows] = await Promise.all([
|
|
725
1011
|
this.queryRows(
|
|
726
|
-
`
|
|
727
|
-
visitor_id,
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1012
|
+
`WITH identity AS (
|
|
1013
|
+
SELECT visitor_id, user_id
|
|
1014
|
+
FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1015
|
+
WHERE site_id = {siteId:String}
|
|
1016
|
+
)
|
|
1017
|
+
SELECT
|
|
1018
|
+
if(i.user_id IS NOT NULL AND i.user_id != '', i.user_id, e.visitor_id) AS group_key,
|
|
1019
|
+
anyLast(e.visitor_id) AS visitor_id,
|
|
1020
|
+
anyLast(i.user_id) AS userId,
|
|
1021
|
+
anyLast(e.traits) AS traits,
|
|
1022
|
+
min(e.timestamp) AS firstSeen,
|
|
1023
|
+
max(e.timestamp) AS lastSeen,
|
|
732
1024
|
count() AS totalEvents,
|
|
733
|
-
countIf(type = 'pageview') AS totalPageviews,
|
|
734
|
-
uniq(session_id) AS totalSessions,
|
|
735
|
-
anyLast(url) AS lastUrl,
|
|
736
|
-
anyLast(
|
|
737
|
-
anyLast(
|
|
738
|
-
anyLast(
|
|
739
|
-
anyLast(
|
|
740
|
-
anyLast(
|
|
741
|
-
anyLast(
|
|
742
|
-
anyLast(
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1025
|
+
countIf(e.type = 'pageview') AS totalPageviews,
|
|
1026
|
+
uniq(e.session_id) AS totalSessions,
|
|
1027
|
+
anyLast(e.url) AS lastUrl,
|
|
1028
|
+
anyLast(e.referrer) AS referrer,
|
|
1029
|
+
anyLast(e.device_type) AS device_type,
|
|
1030
|
+
anyLast(e.browser) AS browser,
|
|
1031
|
+
anyLast(e.os) AS os,
|
|
1032
|
+
anyLast(e.country) AS country,
|
|
1033
|
+
anyLast(e.city) AS city,
|
|
1034
|
+
anyLast(e.region) AS region,
|
|
1035
|
+
anyLast(e.language) AS language,
|
|
1036
|
+
anyLast(e.timezone) AS timezone,
|
|
1037
|
+
anyLast(e.screen_width) AS screen_width,
|
|
1038
|
+
anyLast(e.screen_height) AS screen_height,
|
|
1039
|
+
anyLast(e.utm_source) AS utm_source,
|
|
1040
|
+
anyLast(e.utm_medium) AS utm_medium,
|
|
1041
|
+
anyLast(e.utm_campaign) AS utm_campaign,
|
|
1042
|
+
anyLast(e.utm_term) AS utm_term,
|
|
1043
|
+
anyLast(e.utm_content) AS utm_content
|
|
1044
|
+
FROM ${EVENTS_TABLE} e
|
|
1045
|
+
LEFT JOIN identity i ON e.visitor_id = i.visitor_id
|
|
1046
|
+
WHERE e.site_id = {siteId:String}${where.includes("ILIKE") ? ` AND (e.visitor_id ILIKE {search:String} OR i.user_id ILIKE {search:String})` : ""}
|
|
1047
|
+
GROUP BY group_key
|
|
746
1048
|
ORDER BY lastSeen DESC
|
|
747
1049
|
LIMIT {limit:UInt32}
|
|
748
1050
|
OFFSET {offset:UInt32}`,
|
|
749
1051
|
queryParams
|
|
750
1052
|
),
|
|
751
1053
|
this.queryRows(
|
|
752
|
-
`
|
|
753
|
-
SELECT visitor_id
|
|
754
|
-
|
|
755
|
-
|
|
1054
|
+
`WITH identity AS (
|
|
1055
|
+
SELECT visitor_id, user_id
|
|
1056
|
+
FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1057
|
+
WHERE site_id = {siteId:String}
|
|
1058
|
+
)
|
|
1059
|
+
SELECT count() AS total FROM (
|
|
1060
|
+
SELECT if(i.user_id IS NOT NULL AND i.user_id != '', i.user_id, e.visitor_id) AS group_key
|
|
1061
|
+
FROM ${EVENTS_TABLE} e
|
|
1062
|
+
LEFT JOIN identity i ON e.visitor_id = i.visitor_id
|
|
1063
|
+
WHERE e.site_id = {siteId:String}${where.includes("ILIKE") ? ` AND (e.visitor_id ILIKE {search:String} OR i.user_id ILIKE {search:String})` : ""}
|
|
1064
|
+
GROUP BY group_key
|
|
756
1065
|
)`,
|
|
757
1066
|
queryParams
|
|
758
1067
|
)
|
|
@@ -767,9 +1076,19 @@ var ClickHouseAdapter = class {
|
|
|
767
1076
|
totalPageviews: Number(u.totalPageviews),
|
|
768
1077
|
totalSessions: Number(u.totalSessions),
|
|
769
1078
|
lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
|
|
1079
|
+
referrer: u.referrer ? String(u.referrer) : void 0,
|
|
770
1080
|
device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
|
|
771
1081
|
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
|
|
1082
|
+
language: u.language ? String(u.language) : void 0,
|
|
1083
|
+
timezone: u.timezone ? String(u.timezone) : void 0,
|
|
1084
|
+
screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
|
|
1085
|
+
utm: u.utm_source ? {
|
|
1086
|
+
source: String(u.utm_source),
|
|
1087
|
+
medium: u.utm_medium ? String(u.utm_medium) : void 0,
|
|
1088
|
+
campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
|
|
1089
|
+
term: u.utm_term ? String(u.utm_term) : void 0,
|
|
1090
|
+
content: u.utm_content ? String(u.utm_content) : void 0
|
|
1091
|
+
} : void 0
|
|
773
1092
|
}));
|
|
774
1093
|
return {
|
|
775
1094
|
users,
|
|
@@ -778,13 +1097,178 @@ var ClickHouseAdapter = class {
|
|
|
778
1097
|
offset
|
|
779
1098
|
};
|
|
780
1099
|
}
|
|
781
|
-
async getUserDetail(siteId,
|
|
782
|
-
const
|
|
783
|
-
|
|
1100
|
+
async getUserDetail(siteId, identifier) {
|
|
1101
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
1102
|
+
if (visitorIds.length > 0) {
|
|
1103
|
+
return this.getMergedUserDetail(siteId, identifier, visitorIds);
|
|
1104
|
+
}
|
|
1105
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
1106
|
+
if (userId) {
|
|
1107
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
1108
|
+
return this.getMergedUserDetail(siteId, userId, allVisitorIds.length > 0 ? allVisitorIds : [identifier]);
|
|
1109
|
+
}
|
|
1110
|
+
const result = await this.listUsers({ siteId, search: identifier, limit: 1 });
|
|
1111
|
+
const user = result.users.find((u) => u.visitorId === identifier);
|
|
784
1112
|
return user ?? null;
|
|
785
1113
|
}
|
|
786
|
-
async getUserEvents(siteId,
|
|
787
|
-
|
|
1114
|
+
async getUserEvents(siteId, identifier, params) {
|
|
1115
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
1116
|
+
if (visitorIds.length > 0) {
|
|
1117
|
+
return this.listEventsForVisitorIds(siteId, visitorIds, params);
|
|
1118
|
+
}
|
|
1119
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
1120
|
+
if (userId) {
|
|
1121
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
1122
|
+
if (allVisitorIds.length > 0) {
|
|
1123
|
+
return this.listEventsForVisitorIds(siteId, allVisitorIds, params);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return this.listEvents({ ...params, siteId, visitorId: identifier });
|
|
1127
|
+
}
|
|
1128
|
+
// ─── Identity Mapping ──────────────────────────────────────
|
|
1129
|
+
async upsertIdentity(siteId, visitorId, userId) {
|
|
1130
|
+
await this.client.insert({
|
|
1131
|
+
table: IDENTITY_MAP_TABLE,
|
|
1132
|
+
values: [{
|
|
1133
|
+
site_id: siteId,
|
|
1134
|
+
visitor_id: visitorId,
|
|
1135
|
+
user_id: userId,
|
|
1136
|
+
identified_at: toCHDateTime(/* @__PURE__ */ new Date())
|
|
1137
|
+
}],
|
|
1138
|
+
format: "JSONEachRow"
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
async getVisitorIdsForUser(siteId, userId) {
|
|
1142
|
+
const rows = await this.queryRows(
|
|
1143
|
+
`SELECT visitor_id FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1144
|
+
WHERE site_id = {siteId:String} AND user_id = {userId:String}`,
|
|
1145
|
+
{ siteId, userId }
|
|
1146
|
+
);
|
|
1147
|
+
return rows.map((r) => r.visitor_id);
|
|
1148
|
+
}
|
|
1149
|
+
async getUserIdForVisitor(siteId, visitorId) {
|
|
1150
|
+
const rows = await this.queryRows(
|
|
1151
|
+
`SELECT user_id FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1152
|
+
WHERE site_id = {siteId:String} AND visitor_id = {visitorId:String}
|
|
1153
|
+
LIMIT 1`,
|
|
1154
|
+
{ siteId, visitorId }
|
|
1155
|
+
);
|
|
1156
|
+
return rows.length > 0 ? rows[0].user_id : null;
|
|
1157
|
+
}
|
|
1158
|
+
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
1159
|
+
const rows = await this.queryRows(
|
|
1160
|
+
`SELECT
|
|
1161
|
+
anyLast(visitor_id) AS visitor_id,
|
|
1162
|
+
anyLast(traits) AS traits,
|
|
1163
|
+
min(timestamp) AS firstSeen,
|
|
1164
|
+
max(timestamp) AS lastSeen,
|
|
1165
|
+
count() AS totalEvents,
|
|
1166
|
+
countIf(type = 'pageview') AS totalPageviews,
|
|
1167
|
+
uniq(session_id) AS totalSessions,
|
|
1168
|
+
anyLast(url) AS lastUrl,
|
|
1169
|
+
anyLast(referrer) AS referrer,
|
|
1170
|
+
anyLast(device_type) AS device_type,
|
|
1171
|
+
anyLast(browser) AS browser,
|
|
1172
|
+
anyLast(os) AS os,
|
|
1173
|
+
anyLast(country) AS country,
|
|
1174
|
+
anyLast(city) AS city,
|
|
1175
|
+
anyLast(region) AS region,
|
|
1176
|
+
anyLast(language) AS language,
|
|
1177
|
+
anyLast(timezone) AS timezone,
|
|
1178
|
+
anyLast(screen_width) AS screen_width,
|
|
1179
|
+
anyLast(screen_height) AS screen_height,
|
|
1180
|
+
anyLast(utm_source) AS utm_source,
|
|
1181
|
+
anyLast(utm_medium) AS utm_medium,
|
|
1182
|
+
anyLast(utm_campaign) AS utm_campaign,
|
|
1183
|
+
anyLast(utm_term) AS utm_term,
|
|
1184
|
+
anyLast(utm_content) AS utm_content
|
|
1185
|
+
FROM ${EVENTS_TABLE}
|
|
1186
|
+
WHERE site_id = {siteId:String}
|
|
1187
|
+
AND visitor_id IN {visitorIds:Array(String)}`,
|
|
1188
|
+
{ siteId, visitorIds }
|
|
1189
|
+
);
|
|
1190
|
+
if (rows.length === 0) return null;
|
|
1191
|
+
const u = rows[0];
|
|
1192
|
+
return {
|
|
1193
|
+
visitorId: String(u.visitor_id),
|
|
1194
|
+
visitorIds,
|
|
1195
|
+
userId,
|
|
1196
|
+
traits: this.parseJSON(u.traits),
|
|
1197
|
+
firstSeen: new Date(String(u.firstSeen)).toISOString(),
|
|
1198
|
+
lastSeen: new Date(String(u.lastSeen)).toISOString(),
|
|
1199
|
+
totalEvents: Number(u.totalEvents),
|
|
1200
|
+
totalPageviews: Number(u.totalPageviews),
|
|
1201
|
+
totalSessions: Number(u.totalSessions),
|
|
1202
|
+
lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
|
|
1203
|
+
referrer: u.referrer ? String(u.referrer) : void 0,
|
|
1204
|
+
device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
|
|
1205
|
+
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,
|
|
1206
|
+
language: u.language ? String(u.language) : void 0,
|
|
1207
|
+
timezone: u.timezone ? String(u.timezone) : void 0,
|
|
1208
|
+
screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
|
|
1209
|
+
utm: u.utm_source ? {
|
|
1210
|
+
source: String(u.utm_source),
|
|
1211
|
+
medium: u.utm_medium ? String(u.utm_medium) : void 0,
|
|
1212
|
+
campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
|
|
1213
|
+
term: u.utm_term ? String(u.utm_term) : void 0,
|
|
1214
|
+
content: u.utm_content ? String(u.utm_content) : void 0
|
|
1215
|
+
} : void 0
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
async listEventsForVisitorIds(siteId, visitorIds, params) {
|
|
1219
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
1220
|
+
const offset = params.offset ?? 0;
|
|
1221
|
+
const conditions = [`site_id = {siteId:String}`, `visitor_id IN {visitorIds:Array(String)}`];
|
|
1222
|
+
const queryParams = { siteId, visitorIds, limit, offset };
|
|
1223
|
+
if (params.type) {
|
|
1224
|
+
conditions.push(`type = {type:String}`);
|
|
1225
|
+
queryParams.type = params.type;
|
|
1226
|
+
}
|
|
1227
|
+
if (params.eventName) {
|
|
1228
|
+
conditions.push(`event_name = {eventName:String}`);
|
|
1229
|
+
queryParams.eventName = params.eventName;
|
|
1230
|
+
}
|
|
1231
|
+
if (params.eventNames && params.eventNames.length > 0) {
|
|
1232
|
+
conditions.push(`event_name IN {eventNames:Array(String)}`);
|
|
1233
|
+
queryParams.eventNames = params.eventNames;
|
|
1234
|
+
}
|
|
1235
|
+
if (params.period || params.dateFrom) {
|
|
1236
|
+
const { dateRange } = resolvePeriod({
|
|
1237
|
+
period: params.period,
|
|
1238
|
+
dateFrom: params.dateFrom,
|
|
1239
|
+
dateTo: params.dateTo
|
|
1240
|
+
});
|
|
1241
|
+
conditions.push(`timestamp >= {from:String} AND timestamp <= {to:String}`);
|
|
1242
|
+
queryParams.from = toCHDateTime(dateRange.from);
|
|
1243
|
+
queryParams.to = toCHDateTime(dateRange.to);
|
|
1244
|
+
}
|
|
1245
|
+
const where = conditions.join(" AND ");
|
|
1246
|
+
const [events, countRows] = await Promise.all([
|
|
1247
|
+
this.queryRows(
|
|
1248
|
+
`SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
|
|
1249
|
+
event_name, properties, event_source, event_subtype, page_path, target_url_path,
|
|
1250
|
+
element_selector, element_text, scroll_depth_pct,
|
|
1251
|
+
user_id, traits, country, city, region,
|
|
1252
|
+
device_type, browser, os, language,
|
|
1253
|
+
utm_source, utm_medium, utm_campaign, utm_term, utm_content
|
|
1254
|
+
FROM ${EVENTS_TABLE}
|
|
1255
|
+
WHERE ${where}
|
|
1256
|
+
ORDER BY timestamp DESC
|
|
1257
|
+
LIMIT {limit:UInt32}
|
|
1258
|
+
OFFSET {offset:UInt32}`,
|
|
1259
|
+
queryParams
|
|
1260
|
+
),
|
|
1261
|
+
this.queryRows(
|
|
1262
|
+
`SELECT count() AS total FROM ${EVENTS_TABLE} WHERE ${where}`,
|
|
1263
|
+
queryParams
|
|
1264
|
+
)
|
|
1265
|
+
]);
|
|
1266
|
+
return {
|
|
1267
|
+
events: events.map((e) => this.toEventListItem(e)),
|
|
1268
|
+
total: Number(countRows[0]?.total ?? 0),
|
|
1269
|
+
limit,
|
|
1270
|
+
offset
|
|
1271
|
+
};
|
|
788
1272
|
}
|
|
789
1273
|
// ─── Site Management ──────────────────────────────────────
|
|
790
1274
|
async createSite(data) {
|
|
@@ -797,6 +1281,7 @@ var ClickHouseAdapter = class {
|
|
|
797
1281
|
name: data.name,
|
|
798
1282
|
domain: data.domain,
|
|
799
1283
|
allowedOrigins: data.allowedOrigins,
|
|
1284
|
+
conversionEvents: data.conversionEvents,
|
|
800
1285
|
createdAt: nowISO,
|
|
801
1286
|
updatedAt: nowISO
|
|
802
1287
|
};
|
|
@@ -808,6 +1293,7 @@ var ClickHouseAdapter = class {
|
|
|
808
1293
|
name: site.name,
|
|
809
1294
|
domain: site.domain ?? null,
|
|
810
1295
|
allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
|
|
1296
|
+
conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
|
|
811
1297
|
created_at: nowCH,
|
|
812
1298
|
updated_at: nowCH,
|
|
813
1299
|
version: 1,
|
|
@@ -819,7 +1305,7 @@ var ClickHouseAdapter = class {
|
|
|
819
1305
|
}
|
|
820
1306
|
async getSite(siteId) {
|
|
821
1307
|
const rows = await this.queryRows(
|
|
822
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1308
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
823
1309
|
FROM ${SITES_TABLE} FINAL
|
|
824
1310
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
825
1311
|
{ siteId }
|
|
@@ -828,7 +1314,7 @@ var ClickHouseAdapter = class {
|
|
|
828
1314
|
}
|
|
829
1315
|
async getSiteBySecret(secretKey) {
|
|
830
1316
|
const rows = await this.queryRows(
|
|
831
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1317
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
832
1318
|
FROM ${SITES_TABLE} FINAL
|
|
833
1319
|
WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
|
|
834
1320
|
{ secretKey }
|
|
@@ -837,7 +1323,7 @@ var ClickHouseAdapter = class {
|
|
|
837
1323
|
}
|
|
838
1324
|
async listSites() {
|
|
839
1325
|
const rows = await this.queryRows(
|
|
840
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1326
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
841
1327
|
FROM ${SITES_TABLE} FINAL
|
|
842
1328
|
WHERE is_deleted = 0
|
|
843
1329
|
ORDER BY created_at DESC`,
|
|
@@ -847,7 +1333,7 @@ var ClickHouseAdapter = class {
|
|
|
847
1333
|
}
|
|
848
1334
|
async updateSite(siteId, data) {
|
|
849
1335
|
const currentRows = await this.queryRows(
|
|
850
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at, version
|
|
1336
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
851
1337
|
FROM ${SITES_TABLE} FINAL
|
|
852
1338
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
853
1339
|
{ siteId }
|
|
@@ -861,6 +1347,7 @@ var ClickHouseAdapter = class {
|
|
|
861
1347
|
const newName = data.name !== void 0 ? data.name : String(current.name);
|
|
862
1348
|
const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
|
|
863
1349
|
const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
|
|
1350
|
+
const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
|
|
864
1351
|
await this.client.insert({
|
|
865
1352
|
table: SITES_TABLE,
|
|
866
1353
|
values: [{
|
|
@@ -869,6 +1356,7 @@ var ClickHouseAdapter = class {
|
|
|
869
1356
|
name: newName,
|
|
870
1357
|
domain: newDomain,
|
|
871
1358
|
allowed_origins: newOrigins,
|
|
1359
|
+
conversion_events: newConversions,
|
|
872
1360
|
created_at: toCHDateTime(String(current.created_at)),
|
|
873
1361
|
updated_at: nowCH,
|
|
874
1362
|
version: newVersion,
|
|
@@ -882,13 +1370,14 @@ var ClickHouseAdapter = class {
|
|
|
882
1370
|
name: newName,
|
|
883
1371
|
domain: newDomain ?? void 0,
|
|
884
1372
|
allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
|
|
1373
|
+
conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
|
|
885
1374
|
createdAt: String(current.created_at),
|
|
886
1375
|
updatedAt: nowISO
|
|
887
1376
|
};
|
|
888
1377
|
}
|
|
889
1378
|
async deleteSite(siteId) {
|
|
890
1379
|
const currentRows = await this.queryRows(
|
|
891
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
|
|
1380
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
892
1381
|
FROM ${SITES_TABLE} FINAL
|
|
893
1382
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
894
1383
|
{ siteId }
|
|
@@ -904,6 +1393,7 @@ var ClickHouseAdapter = class {
|
|
|
904
1393
|
name: String(current.name),
|
|
905
1394
|
domain: current.domain ? String(current.domain) : null,
|
|
906
1395
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1396
|
+
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
907
1397
|
created_at: toCHDateTime(String(current.created_at)),
|
|
908
1398
|
updated_at: nowCH,
|
|
909
1399
|
version: Number(current.version) + 1,
|
|
@@ -915,7 +1405,7 @@ var ClickHouseAdapter = class {
|
|
|
915
1405
|
}
|
|
916
1406
|
async regenerateSecret(siteId) {
|
|
917
1407
|
const currentRows = await this.queryRows(
|
|
918
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
|
|
1408
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
919
1409
|
FROM ${SITES_TABLE} FINAL
|
|
920
1410
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
921
1411
|
{ siteId }
|
|
@@ -934,6 +1424,7 @@ var ClickHouseAdapter = class {
|
|
|
934
1424
|
name: String(current.name),
|
|
935
1425
|
domain: current.domain ? String(current.domain) : null,
|
|
936
1426
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1427
|
+
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
937
1428
|
created_at: toCHDateTime(String(current.created_at)),
|
|
938
1429
|
updated_at: nowCH,
|
|
939
1430
|
version: Number(current.version) + 1,
|
|
@@ -947,6 +1438,7 @@ var ClickHouseAdapter = class {
|
|
|
947
1438
|
name: String(current.name),
|
|
948
1439
|
domain: current.domain ? String(current.domain) : void 0,
|
|
949
1440
|
allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
|
|
1441
|
+
conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
|
|
950
1442
|
createdAt: String(current.created_at),
|
|
951
1443
|
updatedAt: nowISO
|
|
952
1444
|
};
|
|
@@ -967,6 +1459,7 @@ var ClickHouseAdapter = class {
|
|
|
967
1459
|
name: String(row.name),
|
|
968
1460
|
domain: row.domain ? String(row.domain) : void 0,
|
|
969
1461
|
allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
|
|
1462
|
+
conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
|
|
970
1463
|
createdAt: new Date(String(row.created_at)).toISOString(),
|
|
971
1464
|
updatedAt: new Date(String(row.updated_at)).toISOString()
|
|
972
1465
|
};
|
|
@@ -983,6 +1476,13 @@ var ClickHouseAdapter = class {
|
|
|
983
1476
|
title: row.title ? String(row.title) : void 0,
|
|
984
1477
|
name: row.event_name ? String(row.event_name) : void 0,
|
|
985
1478
|
properties: this.parseJSON(row.properties),
|
|
1479
|
+
eventSource: row.event_source ? String(row.event_source) : void 0,
|
|
1480
|
+
eventSubtype: row.event_subtype ? String(row.event_subtype) : void 0,
|
|
1481
|
+
pagePath: row.page_path ? String(row.page_path) : void 0,
|
|
1482
|
+
targetUrlPath: row.target_url_path ? String(row.target_url_path) : void 0,
|
|
1483
|
+
elementSelector: row.element_selector ? String(row.element_selector) : void 0,
|
|
1484
|
+
elementText: row.element_text ? String(row.element_text) : void 0,
|
|
1485
|
+
scrollDepthPct: row.scroll_depth_pct !== null && row.scroll_depth_pct !== void 0 ? Number(row.scroll_depth_pct) : void 0,
|
|
986
1486
|
userId: row.user_id ? String(row.user_id) : void 0,
|
|
987
1487
|
traits: this.parseJSON(row.traits),
|
|
988
1488
|
geo: row.country ? {
|
|
@@ -1019,11 +1519,43 @@ var ClickHouseAdapter = class {
|
|
|
1019
1519
|
var import_mongodb = require("mongodb");
|
|
1020
1520
|
var EVENTS_COLLECTION = "litemetrics_events";
|
|
1021
1521
|
var SITES_COLLECTION = "litemetrics_sites";
|
|
1522
|
+
var IDENTITY_MAP_COLLECTION = "litemetrics_identity_map";
|
|
1523
|
+
function buildFilterMatch(filters) {
|
|
1524
|
+
if (!filters) return {};
|
|
1525
|
+
const map = {
|
|
1526
|
+
"geo.country": "country",
|
|
1527
|
+
"geo.city": "city",
|
|
1528
|
+
"geo.region": "region",
|
|
1529
|
+
"language": "language",
|
|
1530
|
+
"device.type": "device_type",
|
|
1531
|
+
"device.browser": "browser",
|
|
1532
|
+
"device.os": "os",
|
|
1533
|
+
"utm.source": "utm_source",
|
|
1534
|
+
"utm.medium": "utm_medium",
|
|
1535
|
+
"utm.campaign": "utm_campaign",
|
|
1536
|
+
"utm.term": "utm_term",
|
|
1537
|
+
"utm.content": "utm_content",
|
|
1538
|
+
"referrer": "referrer",
|
|
1539
|
+
"event_source": "event_source",
|
|
1540
|
+
"event_subtype": "event_subtype",
|
|
1541
|
+
"page_path": "page_path",
|
|
1542
|
+
"target_url_path": "target_url_path",
|
|
1543
|
+
"event_name": "event_name",
|
|
1544
|
+
"type": "type"
|
|
1545
|
+
};
|
|
1546
|
+
const match = {};
|
|
1547
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
1548
|
+
if (!value || !map[key]) continue;
|
|
1549
|
+
match[map[key]] = value;
|
|
1550
|
+
}
|
|
1551
|
+
return match;
|
|
1552
|
+
}
|
|
1022
1553
|
var MongoDBAdapter = class {
|
|
1023
1554
|
client;
|
|
1024
1555
|
db;
|
|
1025
1556
|
collection;
|
|
1026
1557
|
sites;
|
|
1558
|
+
identityMap;
|
|
1027
1559
|
constructor(url) {
|
|
1028
1560
|
this.client = new import_mongodb.MongoClient(url);
|
|
1029
1561
|
}
|
|
@@ -1032,13 +1564,16 @@ var MongoDBAdapter = class {
|
|
|
1032
1564
|
this.db = this.client.db();
|
|
1033
1565
|
this.collection = this.db.collection(EVENTS_COLLECTION);
|
|
1034
1566
|
this.sites = this.db.collection(SITES_COLLECTION);
|
|
1567
|
+
this.identityMap = this.db.collection(IDENTITY_MAP_COLLECTION);
|
|
1035
1568
|
await Promise.all([
|
|
1036
1569
|
this.collection.createIndex({ site_id: 1, timestamp: -1 }),
|
|
1037
1570
|
this.collection.createIndex({ site_id: 1, type: 1 }),
|
|
1038
1571
|
this.collection.createIndex({ site_id: 1, visitor_id: 1 }),
|
|
1039
1572
|
this.collection.createIndex({ site_id: 1, session_id: 1 }),
|
|
1040
1573
|
this.sites.createIndex({ site_id: 1 }, { unique: true }),
|
|
1041
|
-
this.sites.createIndex({ secret_key: 1 })
|
|
1574
|
+
this.sites.createIndex({ secret_key: 1 }),
|
|
1575
|
+
this.identityMap.createIndex({ site_id: 1, visitor_id: 1 }, { unique: true }),
|
|
1576
|
+
this.identityMap.createIndex({ site_id: 1, user_id: 1 })
|
|
1042
1577
|
]);
|
|
1043
1578
|
}
|
|
1044
1579
|
async insertEvents(events) {
|
|
@@ -1054,6 +1589,13 @@ var MongoDBAdapter = class {
|
|
|
1054
1589
|
title: e.title ?? null,
|
|
1055
1590
|
event_name: e.name ?? null,
|
|
1056
1591
|
properties: e.properties ?? null,
|
|
1592
|
+
event_source: e.eventSource ?? null,
|
|
1593
|
+
event_subtype: e.eventSubtype ?? null,
|
|
1594
|
+
page_path: e.pagePath ?? null,
|
|
1595
|
+
target_url_path: e.targetUrlPath ?? null,
|
|
1596
|
+
element_selector: e.elementSelector ?? null,
|
|
1597
|
+
element_text: e.elementText ?? null,
|
|
1598
|
+
scroll_depth_pct: e.scrollDepthPct ?? null,
|
|
1057
1599
|
user_id: e.userId ?? null,
|
|
1058
1600
|
traits: e.traits ?? null,
|
|
1059
1601
|
country: e.geo?.country ?? null,
|
|
@@ -1084,12 +1626,13 @@ var MongoDBAdapter = class {
|
|
|
1084
1626
|
site_id: siteId,
|
|
1085
1627
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1086
1628
|
};
|
|
1629
|
+
const filterMatch = buildFilterMatch(q.filters);
|
|
1087
1630
|
let data = [];
|
|
1088
1631
|
let total = 0;
|
|
1089
1632
|
switch (q.metric) {
|
|
1090
1633
|
case "pageviews": {
|
|
1091
1634
|
const [result2] = await this.collection.aggregate([
|
|
1092
|
-
{ $match: { ...baseMatch, type: "pageview" } },
|
|
1635
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview" } },
|
|
1093
1636
|
{ $count: "count" }
|
|
1094
1637
|
]).toArray();
|
|
1095
1638
|
total = result2?.count ?? 0;
|
|
@@ -1098,7 +1641,7 @@ var MongoDBAdapter = class {
|
|
|
1098
1641
|
}
|
|
1099
1642
|
case "visitors": {
|
|
1100
1643
|
const [result2] = await this.collection.aggregate([
|
|
1101
|
-
{ $match: baseMatch },
|
|
1644
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1102
1645
|
{ $group: { _id: "$visitor_id" } },
|
|
1103
1646
|
{ $count: "count" }
|
|
1104
1647
|
]).toArray();
|
|
@@ -1108,7 +1651,7 @@ var MongoDBAdapter = class {
|
|
|
1108
1651
|
}
|
|
1109
1652
|
case "sessions": {
|
|
1110
1653
|
const [result2] = await this.collection.aggregate([
|
|
1111
|
-
{ $match: baseMatch },
|
|
1654
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1112
1655
|
{ $group: { _id: "$session_id" } },
|
|
1113
1656
|
{ $count: "count" }
|
|
1114
1657
|
]).toArray();
|
|
@@ -1118,16 +1661,31 @@ var MongoDBAdapter = class {
|
|
|
1118
1661
|
}
|
|
1119
1662
|
case "events": {
|
|
1120
1663
|
const [result2] = await this.collection.aggregate([
|
|
1121
|
-
{ $match: { ...baseMatch, type: "event" } },
|
|
1664
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event" } },
|
|
1122
1665
|
{ $count: "count" }
|
|
1123
1666
|
]).toArray();
|
|
1124
1667
|
total = result2?.count ?? 0;
|
|
1125
1668
|
data = [{ key: "events", value: total }];
|
|
1126
1669
|
break;
|
|
1127
1670
|
}
|
|
1671
|
+
case "conversions": {
|
|
1672
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
1673
|
+
if (conversionEvents.length === 0) {
|
|
1674
|
+
total = 0;
|
|
1675
|
+
data = [{ key: "conversions", value: 0 }];
|
|
1676
|
+
break;
|
|
1677
|
+
}
|
|
1678
|
+
const [result2] = await this.collection.aggregate([
|
|
1679
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
|
|
1680
|
+
{ $count: "count" }
|
|
1681
|
+
]).toArray();
|
|
1682
|
+
total = result2?.count ?? 0;
|
|
1683
|
+
data = [{ key: "conversions", value: total }];
|
|
1684
|
+
break;
|
|
1685
|
+
}
|
|
1128
1686
|
case "top_pages": {
|
|
1129
1687
|
const rows = await this.collection.aggregate([
|
|
1130
|
-
{ $match: { ...baseMatch, type: "pageview", url: { $ne: null } } },
|
|
1688
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1131
1689
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1132
1690
|
{ $sort: { value: -1 } },
|
|
1133
1691
|
{ $limit: limit }
|
|
@@ -1138,7 +1696,7 @@ var MongoDBAdapter = class {
|
|
|
1138
1696
|
}
|
|
1139
1697
|
case "top_referrers": {
|
|
1140
1698
|
const rows = await this.collection.aggregate([
|
|
1141
|
-
{ $match: { ...baseMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
|
|
1699
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
|
|
1142
1700
|
{ $group: { _id: "$referrer", value: { $sum: 1 } } },
|
|
1143
1701
|
{ $sort: { value: -1 } },
|
|
1144
1702
|
{ $limit: limit }
|
|
@@ -1149,7 +1707,7 @@ var MongoDBAdapter = class {
|
|
|
1149
1707
|
}
|
|
1150
1708
|
case "top_countries": {
|
|
1151
1709
|
const rows = await this.collection.aggregate([
|
|
1152
|
-
{ $match: { ...baseMatch, country: { $ne: null } } },
|
|
1710
|
+
{ $match: { ...baseMatch, ...filterMatch, country: { $ne: null } } },
|
|
1153
1711
|
{ $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
|
|
1154
1712
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1155
1713
|
{ $sort: { value: -1 } },
|
|
@@ -1161,7 +1719,7 @@ var MongoDBAdapter = class {
|
|
|
1161
1719
|
}
|
|
1162
1720
|
case "top_cities": {
|
|
1163
1721
|
const rows = await this.collection.aggregate([
|
|
1164
|
-
{ $match: { ...baseMatch, city: { $ne: null } } },
|
|
1722
|
+
{ $match: { ...baseMatch, ...filterMatch, city: { $ne: null } } },
|
|
1165
1723
|
{ $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
|
|
1166
1724
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1167
1725
|
{ $sort: { value: -1 } },
|
|
@@ -1173,7 +1731,24 @@ var MongoDBAdapter = class {
|
|
|
1173
1731
|
}
|
|
1174
1732
|
case "top_events": {
|
|
1175
1733
|
const rows = await this.collection.aggregate([
|
|
1176
|
-
{ $match: { ...baseMatch, type: "event", event_name: { $ne: null } } },
|
|
1734
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $ne: null } } },
|
|
1735
|
+
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1736
|
+
{ $sort: { value: -1 } },
|
|
1737
|
+
{ $limit: limit }
|
|
1738
|
+
]).toArray();
|
|
1739
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1740
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1741
|
+
break;
|
|
1742
|
+
}
|
|
1743
|
+
case "top_conversions": {
|
|
1744
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
1745
|
+
if (conversionEvents.length === 0) {
|
|
1746
|
+
total = 0;
|
|
1747
|
+
data = [];
|
|
1748
|
+
break;
|
|
1749
|
+
}
|
|
1750
|
+
const rows = await this.collection.aggregate([
|
|
1751
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
|
|
1177
1752
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1178
1753
|
{ $sort: { value: -1 } },
|
|
1179
1754
|
{ $limit: limit }
|
|
@@ -1182,9 +1757,92 @@ var MongoDBAdapter = class {
|
|
|
1182
1757
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1183
1758
|
break;
|
|
1184
1759
|
}
|
|
1760
|
+
case "top_exit_pages": {
|
|
1761
|
+
const rows = await this.collection.aggregate([
|
|
1762
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1763
|
+
{ $sort: { timestamp: 1 } },
|
|
1764
|
+
{ $group: { _id: "$session_id", url: { $last: "$url" } } },
|
|
1765
|
+
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1766
|
+
{ $sort: { value: -1 } },
|
|
1767
|
+
{ $limit: limit }
|
|
1768
|
+
]).toArray();
|
|
1769
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1770
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1771
|
+
break;
|
|
1772
|
+
}
|
|
1773
|
+
case "top_transitions": {
|
|
1774
|
+
const rows = await this.collection.aggregate([
|
|
1775
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1776
|
+
{
|
|
1777
|
+
$setWindowFields: {
|
|
1778
|
+
partitionBy: "$session_id",
|
|
1779
|
+
sortBy: { timestamp: 1 },
|
|
1780
|
+
output: {
|
|
1781
|
+
prev_url: { $shift: { output: "$url", by: -1 } }
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
},
|
|
1785
|
+
{ $match: { prev_url: { $ne: null } } },
|
|
1786
|
+
{ $group: { _id: { $concat: ["$prev_url", " \u2192 ", "$url"] }, value: { $sum: 1 } } },
|
|
1787
|
+
{ $sort: { value: -1 } },
|
|
1788
|
+
{ $limit: limit }
|
|
1789
|
+
]).toArray();
|
|
1790
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1791
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1794
|
+
case "top_scroll_pages": {
|
|
1795
|
+
const rows = await this.collection.aggregate([
|
|
1796
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } } },
|
|
1797
|
+
{ $group: { _id: "$page_path", value: { $sum: 1 } } },
|
|
1798
|
+
{ $sort: { value: -1 } },
|
|
1799
|
+
{ $limit: limit }
|
|
1800
|
+
]).toArray();
|
|
1801
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1802
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
case "top_button_clicks": {
|
|
1806
|
+
const rows = await this.collection.aggregate([
|
|
1807
|
+
{
|
|
1808
|
+
$match: {
|
|
1809
|
+
...baseMatch,
|
|
1810
|
+
...filterMatch,
|
|
1811
|
+
type: "event",
|
|
1812
|
+
event_subtype: "button_click",
|
|
1813
|
+
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
1814
|
+
}
|
|
1815
|
+
},
|
|
1816
|
+
{ $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
|
|
1817
|
+
{ $sort: { value: -1 } },
|
|
1818
|
+
{ $limit: limit }
|
|
1819
|
+
]).toArray();
|
|
1820
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1821
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1822
|
+
break;
|
|
1823
|
+
}
|
|
1824
|
+
case "top_link_targets": {
|
|
1825
|
+
const rows = await this.collection.aggregate([
|
|
1826
|
+
{
|
|
1827
|
+
$match: {
|
|
1828
|
+
...baseMatch,
|
|
1829
|
+
...filterMatch,
|
|
1830
|
+
type: "event",
|
|
1831
|
+
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
1832
|
+
target_url_path: { $ne: null }
|
|
1833
|
+
}
|
|
1834
|
+
},
|
|
1835
|
+
{ $group: { _id: "$target_url_path", value: { $sum: 1 } } },
|
|
1836
|
+
{ $sort: { value: -1 } },
|
|
1837
|
+
{ $limit: limit }
|
|
1838
|
+
]).toArray();
|
|
1839
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1840
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1185
1843
|
case "top_devices": {
|
|
1186
1844
|
const rows = await this.collection.aggregate([
|
|
1187
|
-
{ $match: { ...baseMatch, device_type: { $ne: null } } },
|
|
1845
|
+
{ $match: { ...baseMatch, ...filterMatch, device_type: { $ne: null } } },
|
|
1188
1846
|
{ $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
|
|
1189
1847
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1190
1848
|
{ $sort: { value: -1 } },
|
|
@@ -1196,7 +1854,7 @@ var MongoDBAdapter = class {
|
|
|
1196
1854
|
}
|
|
1197
1855
|
case "top_browsers": {
|
|
1198
1856
|
const rows = await this.collection.aggregate([
|
|
1199
|
-
{ $match: { ...baseMatch, browser: { $ne: null } } },
|
|
1857
|
+
{ $match: { ...baseMatch, ...filterMatch, browser: { $ne: null } } },
|
|
1200
1858
|
{ $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
|
|
1201
1859
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1202
1860
|
{ $sort: { value: -1 } },
|
|
@@ -1208,7 +1866,7 @@ var MongoDBAdapter = class {
|
|
|
1208
1866
|
}
|
|
1209
1867
|
case "top_os": {
|
|
1210
1868
|
const rows = await this.collection.aggregate([
|
|
1211
|
-
{ $match: { ...baseMatch, os: { $ne: null } } },
|
|
1869
|
+
{ $match: { ...baseMatch, ...filterMatch, os: { $ne: null } } },
|
|
1212
1870
|
{ $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
|
|
1213
1871
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1214
1872
|
{ $sort: { value: -1 } },
|
|
@@ -1218,9 +1876,45 @@ var MongoDBAdapter = class {
|
|
|
1218
1876
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1219
1877
|
break;
|
|
1220
1878
|
}
|
|
1879
|
+
case "top_utm_sources": {
|
|
1880
|
+
const rows = await this.collection.aggregate([
|
|
1881
|
+
{ $match: { ...baseMatch, ...filterMatch, utm_source: { $nin: [null, ""] } } },
|
|
1882
|
+
{ $group: { _id: "$utm_source", value: { $addToSet: "$visitor_id" } } },
|
|
1883
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1884
|
+
{ $sort: { value: -1 } },
|
|
1885
|
+
{ $limit: limit }
|
|
1886
|
+
]).toArray();
|
|
1887
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1888
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1889
|
+
break;
|
|
1890
|
+
}
|
|
1891
|
+
case "top_utm_mediums": {
|
|
1892
|
+
const rows = await this.collection.aggregate([
|
|
1893
|
+
{ $match: { ...baseMatch, ...filterMatch, utm_medium: { $nin: [null, ""] } } },
|
|
1894
|
+
{ $group: { _id: "$utm_medium", value: { $addToSet: "$visitor_id" } } },
|
|
1895
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1896
|
+
{ $sort: { value: -1 } },
|
|
1897
|
+
{ $limit: limit }
|
|
1898
|
+
]).toArray();
|
|
1899
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1900
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1901
|
+
break;
|
|
1902
|
+
}
|
|
1903
|
+
case "top_utm_campaigns": {
|
|
1904
|
+
const rows = await this.collection.aggregate([
|
|
1905
|
+
{ $match: { ...baseMatch, ...filterMatch, utm_campaign: { $nin: [null, ""] } } },
|
|
1906
|
+
{ $group: { _id: "$utm_campaign", value: { $addToSet: "$visitor_id" } } },
|
|
1907
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1908
|
+
{ $sort: { value: -1 } },
|
|
1909
|
+
{ $limit: limit }
|
|
1910
|
+
]).toArray();
|
|
1911
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1912
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1913
|
+
break;
|
|
1914
|
+
}
|
|
1221
1915
|
}
|
|
1222
1916
|
const result = { metric: q.metric, period, data, total };
|
|
1223
|
-
if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
|
|
1917
|
+
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
1224
1918
|
const prevRange = previousPeriodRange(dateRange);
|
|
1225
1919
|
const prevResult = await this.query({
|
|
1226
1920
|
...q,
|
|
@@ -1252,15 +1946,34 @@ var MongoDBAdapter = class {
|
|
|
1252
1946
|
site_id: params.siteId,
|
|
1253
1947
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1254
1948
|
};
|
|
1949
|
+
const filterMatch = buildFilterMatch(params.filters);
|
|
1255
1950
|
if (params.metric === "pageviews") {
|
|
1256
1951
|
baseMatch.type = "pageview";
|
|
1257
1952
|
}
|
|
1953
|
+
if (params.metric === "events") {
|
|
1954
|
+
baseMatch.type = "event";
|
|
1955
|
+
}
|
|
1956
|
+
if (params.metric === "conversions") {
|
|
1957
|
+
baseMatch.type = "event";
|
|
1958
|
+
const conversionEvents = params.conversionEvents ?? [];
|
|
1959
|
+
if (conversionEvents.length === 0) {
|
|
1960
|
+
const data2 = fillBuckets(
|
|
1961
|
+
new Date(dateRange.from),
|
|
1962
|
+
new Date(dateRange.to),
|
|
1963
|
+
granularity,
|
|
1964
|
+
granularityToDateFormat(granularity),
|
|
1965
|
+
[]
|
|
1966
|
+
);
|
|
1967
|
+
return { metric: params.metric, granularity, data: data2 };
|
|
1968
|
+
}
|
|
1969
|
+
baseMatch.event_name = { $in: conversionEvents };
|
|
1970
|
+
}
|
|
1258
1971
|
const dateFormat = granularityToDateFormat(granularity);
|
|
1259
1972
|
let pipeline;
|
|
1260
1973
|
if (params.metric === "visitors" || params.metric === "sessions") {
|
|
1261
1974
|
const groupField = params.metric === "visitors" ? "$visitor_id" : "$session_id";
|
|
1262
1975
|
pipeline = [
|
|
1263
|
-
{ $match: baseMatch },
|
|
1976
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1264
1977
|
{
|
|
1265
1978
|
$group: {
|
|
1266
1979
|
_id: {
|
|
@@ -1279,7 +1992,7 @@ var MongoDBAdapter = class {
|
|
|
1279
1992
|
];
|
|
1280
1993
|
} else {
|
|
1281
1994
|
pipeline = [
|
|
1282
|
-
{ $match: baseMatch },
|
|
1995
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1283
1996
|
{
|
|
1284
1997
|
$group: {
|
|
1285
1998
|
_id: { $dateToString: { format: dateFormat, date: "$timestamp" } },
|
|
@@ -1360,7 +2073,12 @@ var MongoDBAdapter = class {
|
|
|
1360
2073
|
const offset = params.offset ?? 0;
|
|
1361
2074
|
const match = { site_id: params.siteId };
|
|
1362
2075
|
if (params.type) match.type = params.type;
|
|
1363
|
-
if (params.eventName)
|
|
2076
|
+
if (params.eventName) {
|
|
2077
|
+
match.event_name = params.eventName;
|
|
2078
|
+
} else if (params.eventNames && params.eventNames.length > 0) {
|
|
2079
|
+
match.event_name = { $in: params.eventNames };
|
|
2080
|
+
}
|
|
2081
|
+
if (params.eventSource) match.event_source = params.eventSource;
|
|
1364
2082
|
if (params.visitorId) match.visitor_id = params.visitorId;
|
|
1365
2083
|
if (params.userId) match.user_id = params.userId;
|
|
1366
2084
|
if (params.period || params.dateFrom) {
|
|
@@ -1382,23 +2100,72 @@ var MongoDBAdapter = class {
|
|
|
1382
2100
|
offset
|
|
1383
2101
|
};
|
|
1384
2102
|
}
|
|
2103
|
+
// ─── Identity Mapping ──────────────────────────────────────
|
|
2104
|
+
async upsertIdentity(siteId, visitorId, userId) {
|
|
2105
|
+
await this.identityMap.updateOne(
|
|
2106
|
+
{ site_id: siteId, visitor_id: visitorId },
|
|
2107
|
+
{
|
|
2108
|
+
$set: { user_id: userId, identified_at: /* @__PURE__ */ new Date() },
|
|
2109
|
+
$setOnInsert: { site_id: siteId, visitor_id: visitorId, created_at: /* @__PURE__ */ new Date() }
|
|
2110
|
+
},
|
|
2111
|
+
{ upsert: true }
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
async getVisitorIdsForUser(siteId, userId) {
|
|
2115
|
+
const docs = await this.identityMap.find({ site_id: siteId, user_id: userId }).toArray();
|
|
2116
|
+
return docs.map((d) => d.visitor_id);
|
|
2117
|
+
}
|
|
2118
|
+
async getUserIdForVisitor(siteId, visitorId) {
|
|
2119
|
+
const doc = await this.identityMap.findOne({ site_id: siteId, visitor_id: visitorId });
|
|
2120
|
+
return doc?.user_id ?? null;
|
|
2121
|
+
}
|
|
1385
2122
|
// ─── User Listing ──────────────────────────────────────
|
|
1386
2123
|
async listUsers(params) {
|
|
1387
2124
|
const limit = Math.min(params.limit ?? 50, 200);
|
|
1388
2125
|
const offset = params.offset ?? 0;
|
|
1389
2126
|
const match = { site_id: params.siteId };
|
|
1390
|
-
if (params.search) {
|
|
1391
|
-
match.$or = [
|
|
1392
|
-
{ visitor_id: { $regex: params.search, $options: "i" } },
|
|
1393
|
-
{ user_id: { $regex: params.search, $options: "i" } }
|
|
1394
|
-
];
|
|
1395
|
-
}
|
|
1396
2127
|
const pipeline = [
|
|
1397
2128
|
{ $match: match },
|
|
2129
|
+
// Join with identity map to resolve visitor → user
|
|
2130
|
+
{
|
|
2131
|
+
$lookup: {
|
|
2132
|
+
from: IDENTITY_MAP_COLLECTION,
|
|
2133
|
+
let: { vid: "$visitor_id", sid: "$site_id" },
|
|
2134
|
+
pipeline: [
|
|
2135
|
+
{ $match: { $expr: { $and: [{ $eq: ["$visitor_id", "$$vid"] }, { $eq: ["$site_id", "$$sid"] }] } } }
|
|
2136
|
+
],
|
|
2137
|
+
as: "_identity"
|
|
2138
|
+
}
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
$addFields: {
|
|
2142
|
+
_resolved_id: {
|
|
2143
|
+
$ifNull: [{ $arrayElemAt: ["$_identity.user_id", 0] }, "$visitor_id"]
|
|
2144
|
+
},
|
|
2145
|
+
_resolved_user_id: {
|
|
2146
|
+
$arrayElemAt: ["$_identity.user_id", 0]
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
];
|
|
2151
|
+
if (params.search) {
|
|
2152
|
+
pipeline.push({
|
|
2153
|
+
$match: {
|
|
2154
|
+
$or: [
|
|
2155
|
+
{ visitor_id: { $regex: params.search, $options: "i" } },
|
|
2156
|
+
{ user_id: { $regex: params.search, $options: "i" } },
|
|
2157
|
+
{ _resolved_user_id: { $regex: params.search, $options: "i" } }
|
|
2158
|
+
]
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
pipeline.push(
|
|
2163
|
+
{ $sort: { timestamp: 1 } },
|
|
1398
2164
|
{
|
|
1399
2165
|
$group: {
|
|
1400
|
-
_id: "$
|
|
1401
|
-
|
|
2166
|
+
_id: "$_resolved_id",
|
|
2167
|
+
visitorIds: { $addToSet: "$visitor_id" },
|
|
2168
|
+
userId: { $last: { $ifNull: ["$_resolved_user_id", "$user_id"] } },
|
|
1402
2169
|
traits: { $last: "$traits" },
|
|
1403
2170
|
firstSeen: { $min: "$timestamp" },
|
|
1404
2171
|
lastSeen: { $max: "$timestamp" },
|
|
@@ -1406,13 +2173,22 @@ var MongoDBAdapter = class {
|
|
|
1406
2173
|
totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
|
|
1407
2174
|
sessions: { $addToSet: "$session_id" },
|
|
1408
2175
|
lastUrl: { $last: "$url" },
|
|
2176
|
+
referrer: { $last: "$referrer" },
|
|
1409
2177
|
device_type: { $last: "$device_type" },
|
|
1410
2178
|
browser: { $last: "$browser" },
|
|
1411
2179
|
os: { $last: "$os" },
|
|
1412
2180
|
country: { $last: "$country" },
|
|
1413
2181
|
city: { $last: "$city" },
|
|
1414
2182
|
region: { $last: "$region" },
|
|
1415
|
-
language: { $last: "$language" }
|
|
2183
|
+
language: { $last: "$language" },
|
|
2184
|
+
timezone: { $last: "$timezone" },
|
|
2185
|
+
screen_width: { $last: "$screen_width" },
|
|
2186
|
+
screen_height: { $last: "$screen_height" },
|
|
2187
|
+
utm_source: { $last: "$utm_source" },
|
|
2188
|
+
utm_medium: { $last: "$utm_medium" },
|
|
2189
|
+
utm_campaign: { $last: "$utm_campaign" },
|
|
2190
|
+
utm_term: { $last: "$utm_term" },
|
|
2191
|
+
utm_content: { $last: "$utm_content" }
|
|
1416
2192
|
}
|
|
1417
2193
|
},
|
|
1418
2194
|
{ $sort: { lastSeen: -1 } },
|
|
@@ -1422,10 +2198,11 @@ var MongoDBAdapter = class {
|
|
|
1422
2198
|
count: [{ $count: "total" }]
|
|
1423
2199
|
}
|
|
1424
2200
|
}
|
|
1425
|
-
|
|
2201
|
+
);
|
|
1426
2202
|
const [result] = await this.collection.aggregate(pipeline).toArray();
|
|
1427
2203
|
const users = (result?.data ?? []).map((u) => ({
|
|
1428
|
-
visitorId: u._id,
|
|
2204
|
+
visitorId: u.visitorIds[0] ?? u._id,
|
|
2205
|
+
visitorIds: u.visitorIds.length > 1 ? u.visitorIds : void 0,
|
|
1429
2206
|
userId: u.userId ?? void 0,
|
|
1430
2207
|
traits: u.traits ?? void 0,
|
|
1431
2208
|
firstSeen: u.firstSeen.toISOString(),
|
|
@@ -1434,9 +2211,19 @@ var MongoDBAdapter = class {
|
|
|
1434
2211
|
totalPageviews: u.totalPageviews,
|
|
1435
2212
|
totalSessions: u.sessions.length,
|
|
1436
2213
|
lastUrl: u.lastUrl ?? void 0,
|
|
2214
|
+
referrer: u.referrer ?? void 0,
|
|
1437
2215
|
device: u.device_type ? { type: u.device_type, browser: u.browser ?? "", os: u.os ?? "" } : void 0,
|
|
1438
2216
|
geo: u.country ? { country: u.country, city: u.city ?? void 0, region: u.region ?? void 0 } : void 0,
|
|
1439
|
-
language: u.language ?? void 0
|
|
2217
|
+
language: u.language ?? void 0,
|
|
2218
|
+
timezone: u.timezone ?? void 0,
|
|
2219
|
+
screen: u.screen_width || u.screen_height ? { width: u.screen_width ?? 0, height: u.screen_height ?? 0 } : void 0,
|
|
2220
|
+
utm: u.utm_source ? {
|
|
2221
|
+
source: u.utm_source ?? void 0,
|
|
2222
|
+
medium: u.utm_medium ?? void 0,
|
|
2223
|
+
campaign: u.utm_campaign ?? void 0,
|
|
2224
|
+
term: u.utm_term ?? void 0,
|
|
2225
|
+
content: u.utm_content ?? void 0
|
|
2226
|
+
} : void 0
|
|
1440
2227
|
}));
|
|
1441
2228
|
return {
|
|
1442
2229
|
users,
|
|
@@ -1445,13 +2232,125 @@ var MongoDBAdapter = class {
|
|
|
1445
2232
|
offset
|
|
1446
2233
|
};
|
|
1447
2234
|
}
|
|
1448
|
-
async getUserDetail(siteId,
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
2235
|
+
async getUserDetail(siteId, identifier) {
|
|
2236
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
2237
|
+
if (visitorIds.length > 0) {
|
|
2238
|
+
return this.getMergedUserDetail(siteId, identifier, visitorIds);
|
|
2239
|
+
}
|
|
2240
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
2241
|
+
if (userId) {
|
|
2242
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
2243
|
+
return this.getMergedUserDetail(siteId, userId, allVisitorIds);
|
|
2244
|
+
}
|
|
2245
|
+
return this.getMergedUserDetail(siteId, void 0, [identifier]);
|
|
2246
|
+
}
|
|
2247
|
+
async getUserEvents(siteId, identifier, params) {
|
|
2248
|
+
let visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
2249
|
+
if (visitorIds.length === 0) {
|
|
2250
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
2251
|
+
if (userId) {
|
|
2252
|
+
visitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
if (visitorIds.length === 0) {
|
|
2256
|
+
visitorIds = [identifier];
|
|
2257
|
+
}
|
|
2258
|
+
return this.listEventsForVisitorIds(siteId, visitorIds, params);
|
|
1452
2259
|
}
|
|
1453
|
-
async
|
|
1454
|
-
|
|
2260
|
+
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
2261
|
+
const pipeline = [
|
|
2262
|
+
{ $match: { site_id: siteId, visitor_id: { $in: visitorIds } } },
|
|
2263
|
+
{ $sort: { timestamp: 1 } },
|
|
2264
|
+
{
|
|
2265
|
+
$group: {
|
|
2266
|
+
_id: null,
|
|
2267
|
+
visitorIds: { $addToSet: "$visitor_id" },
|
|
2268
|
+
traits: { $last: "$traits" },
|
|
2269
|
+
firstSeen: { $min: "$timestamp" },
|
|
2270
|
+
lastSeen: { $max: "$timestamp" },
|
|
2271
|
+
totalEvents: { $sum: 1 },
|
|
2272
|
+
totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
|
|
2273
|
+
sessions: { $addToSet: "$session_id" },
|
|
2274
|
+
lastUrl: { $last: "$url" },
|
|
2275
|
+
referrer: { $last: "$referrer" },
|
|
2276
|
+
device_type: { $last: "$device_type" },
|
|
2277
|
+
browser: { $last: "$browser" },
|
|
2278
|
+
os: { $last: "$os" },
|
|
2279
|
+
country: { $last: "$country" },
|
|
2280
|
+
city: { $last: "$city" },
|
|
2281
|
+
region: { $last: "$region" },
|
|
2282
|
+
language: { $last: "$language" },
|
|
2283
|
+
timezone: { $last: "$timezone" },
|
|
2284
|
+
screen_width: { $last: "$screen_width" },
|
|
2285
|
+
screen_height: { $last: "$screen_height" },
|
|
2286
|
+
utm_source: { $last: "$utm_source" },
|
|
2287
|
+
utm_medium: { $last: "$utm_medium" },
|
|
2288
|
+
utm_campaign: { $last: "$utm_campaign" },
|
|
2289
|
+
utm_term: { $last: "$utm_term" },
|
|
2290
|
+
utm_content: { $last: "$utm_content" }
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
];
|
|
2294
|
+
const [row] = await this.collection.aggregate(pipeline).toArray();
|
|
2295
|
+
if (!row) return null;
|
|
2296
|
+
return {
|
|
2297
|
+
visitorId: visitorIds[0],
|
|
2298
|
+
visitorIds: row.visitorIds.length > 1 ? row.visitorIds : void 0,
|
|
2299
|
+
userId: userId ?? void 0,
|
|
2300
|
+
traits: row.traits ?? void 0,
|
|
2301
|
+
firstSeen: row.firstSeen.toISOString(),
|
|
2302
|
+
lastSeen: row.lastSeen.toISOString(),
|
|
2303
|
+
totalEvents: row.totalEvents,
|
|
2304
|
+
totalPageviews: row.totalPageviews,
|
|
2305
|
+
totalSessions: row.sessions.length,
|
|
2306
|
+
lastUrl: row.lastUrl ?? void 0,
|
|
2307
|
+
referrer: row.referrer ?? void 0,
|
|
2308
|
+
device: row.device_type ? { type: row.device_type, browser: row.browser ?? "", os: row.os ?? "" } : void 0,
|
|
2309
|
+
geo: row.country ? { country: row.country, city: row.city ?? void 0, region: row.region ?? void 0 } : void 0,
|
|
2310
|
+
language: row.language ?? void 0,
|
|
2311
|
+
timezone: row.timezone ?? void 0,
|
|
2312
|
+
screen: row.screen_width || row.screen_height ? { width: row.screen_width ?? 0, height: row.screen_height ?? 0 } : void 0,
|
|
2313
|
+
utm: row.utm_source ? {
|
|
2314
|
+
source: row.utm_source ?? void 0,
|
|
2315
|
+
medium: row.utm_medium ?? void 0,
|
|
2316
|
+
campaign: row.utm_campaign ?? void 0,
|
|
2317
|
+
term: row.utm_term ?? void 0,
|
|
2318
|
+
content: row.utm_content ?? void 0
|
|
2319
|
+
} : void 0
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
async listEventsForVisitorIds(siteId, visitorIds, params) {
|
|
2323
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
2324
|
+
const offset = params.offset ?? 0;
|
|
2325
|
+
const match = {
|
|
2326
|
+
site_id: siteId,
|
|
2327
|
+
visitor_id: { $in: visitorIds }
|
|
2328
|
+
};
|
|
2329
|
+
if (params.type) match.type = params.type;
|
|
2330
|
+
if (params.eventName) {
|
|
2331
|
+
match.event_name = params.eventName;
|
|
2332
|
+
} else if (params.eventNames && params.eventNames.length > 0) {
|
|
2333
|
+
match.event_name = { $in: params.eventNames };
|
|
2334
|
+
}
|
|
2335
|
+
if (params.eventSource) match.event_source = params.eventSource;
|
|
2336
|
+
if (params.period || params.dateFrom) {
|
|
2337
|
+
const { dateRange } = resolvePeriod({
|
|
2338
|
+
period: params.period,
|
|
2339
|
+
dateFrom: params.dateFrom,
|
|
2340
|
+
dateTo: params.dateTo
|
|
2341
|
+
});
|
|
2342
|
+
match.timestamp = { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) };
|
|
2343
|
+
}
|
|
2344
|
+
const [events, countResult] = await Promise.all([
|
|
2345
|
+
this.collection.find(match).sort({ timestamp: -1 }).skip(offset).limit(limit).toArray(),
|
|
2346
|
+
this.collection.countDocuments(match)
|
|
2347
|
+
]);
|
|
2348
|
+
return {
|
|
2349
|
+
events: events.map((e) => this.toEventListItem(e)),
|
|
2350
|
+
total: countResult,
|
|
2351
|
+
limit,
|
|
2352
|
+
offset
|
|
2353
|
+
};
|
|
1455
2354
|
}
|
|
1456
2355
|
toEventListItem(doc) {
|
|
1457
2356
|
return {
|
|
@@ -1465,6 +2364,13 @@ var MongoDBAdapter = class {
|
|
|
1465
2364
|
title: doc.title ?? void 0,
|
|
1466
2365
|
name: doc.event_name ?? void 0,
|
|
1467
2366
|
properties: doc.properties ?? void 0,
|
|
2367
|
+
eventSource: doc.event_source ? doc.event_source : void 0,
|
|
2368
|
+
eventSubtype: doc.event_subtype ? doc.event_subtype : void 0,
|
|
2369
|
+
pagePath: doc.page_path ?? void 0,
|
|
2370
|
+
targetUrlPath: doc.target_url_path ?? void 0,
|
|
2371
|
+
elementSelector: doc.element_selector ?? void 0,
|
|
2372
|
+
elementText: doc.element_text ?? void 0,
|
|
2373
|
+
scrollDepthPct: doc.scroll_depth_pct ?? void 0,
|
|
1468
2374
|
userId: doc.user_id ?? void 0,
|
|
1469
2375
|
traits: doc.traits ?? void 0,
|
|
1470
2376
|
geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
|
|
@@ -1488,6 +2394,7 @@ var MongoDBAdapter = class {
|
|
|
1488
2394
|
name: data.name,
|
|
1489
2395
|
domain: data.domain ?? null,
|
|
1490
2396
|
allowed_origins: data.allowedOrigins ?? null,
|
|
2397
|
+
conversion_events: data.conversionEvents ?? null,
|
|
1491
2398
|
created_at: now,
|
|
1492
2399
|
updated_at: now
|
|
1493
2400
|
};
|
|
@@ -1511,6 +2418,7 @@ var MongoDBAdapter = class {
|
|
|
1511
2418
|
if (data.name !== void 0) updates.name = data.name;
|
|
1512
2419
|
if (data.domain !== void 0) updates.domain = data.domain || null;
|
|
1513
2420
|
if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
|
|
2421
|
+
if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
|
|
1514
2422
|
const result = await this.sites.findOneAndUpdate(
|
|
1515
2423
|
{ site_id: siteId },
|
|
1516
2424
|
{ $set: updates },
|
|
@@ -1541,6 +2449,7 @@ var MongoDBAdapter = class {
|
|
|
1541
2449
|
name: doc.name,
|
|
1542
2450
|
domain: doc.domain ?? void 0,
|
|
1543
2451
|
allowedOrigins: doc.allowed_origins ?? void 0,
|
|
2452
|
+
conversionEvents: doc.conversion_events ?? void 0,
|
|
1544
2453
|
createdAt: doc.created_at.toISOString(),
|
|
1545
2454
|
updatedAt: doc.updated_at.toISOString()
|
|
1546
2455
|
};
|
|
@@ -1794,6 +2703,7 @@ async function createCollector(config) {
|
|
|
1794
2703
|
if (allowed) {
|
|
1795
2704
|
res.setHeader?.("Access-Control-Allow-Origin", origin || "*");
|
|
1796
2705
|
res.setHeader?.("Access-Control-Allow-Methods", methods);
|
|
2706
|
+
res.setHeader?.("Access-Control-Allow-Credentials", "true");
|
|
1797
2707
|
const headers = ["Content-Type", extraHeaders].filter(Boolean).join(", ");
|
|
1798
2708
|
res.setHeader?.("Access-Control-Allow-Headers", headers);
|
|
1799
2709
|
}
|
|
@@ -1811,6 +2721,51 @@ async function createCollector(config) {
|
|
|
1811
2721
|
return { ...event, ip, geo, device };
|
|
1812
2722
|
});
|
|
1813
2723
|
}
|
|
2724
|
+
const identityCache = /* @__PURE__ */ new Map();
|
|
2725
|
+
const IDENTITY_CACHE_TTL = 5 * 60 * 1e3;
|
|
2726
|
+
function getCachedUserId(siteId, visitorId) {
|
|
2727
|
+
const key = `${siteId}:${visitorId}`;
|
|
2728
|
+
const entry = identityCache.get(key);
|
|
2729
|
+
if (!entry) return void 0;
|
|
2730
|
+
if (Date.now() > entry.expires) {
|
|
2731
|
+
identityCache.delete(key);
|
|
2732
|
+
return void 0;
|
|
2733
|
+
}
|
|
2734
|
+
return entry.userId;
|
|
2735
|
+
}
|
|
2736
|
+
function setCachedUserId(siteId, visitorId, userId) {
|
|
2737
|
+
const key = `${siteId}:${visitorId}`;
|
|
2738
|
+
identityCache.set(key, { userId, expires: Date.now() + IDENTITY_CACHE_TTL });
|
|
2739
|
+
if (identityCache.size > 1e4) {
|
|
2740
|
+
const now = Date.now();
|
|
2741
|
+
for (const [k, v] of identityCache) {
|
|
2742
|
+
if (now > v.expires) identityCache.delete(k);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
async function processIdentity(events) {
|
|
2747
|
+
for (const event of events) {
|
|
2748
|
+
if (!event.visitorId || event.visitorId === "server") continue;
|
|
2749
|
+
if (event.type === "identify" && event.userId) {
|
|
2750
|
+
await db.upsertIdentity(event.siteId, event.visitorId, event.userId);
|
|
2751
|
+
setCachedUserId(event.siteId, event.visitorId, event.userId);
|
|
2752
|
+
} else if (!event.userId) {
|
|
2753
|
+
const cached = getCachedUserId(event.siteId, event.visitorId);
|
|
2754
|
+
if (cached) {
|
|
2755
|
+
event.userId = cached;
|
|
2756
|
+
} else {
|
|
2757
|
+
const resolved = await db.getUserIdForVisitor(event.siteId, event.visitorId);
|
|
2758
|
+
if (resolved) {
|
|
2759
|
+
event.userId = resolved;
|
|
2760
|
+
setCachedUserId(event.siteId, event.visitorId, resolved);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
} else if (event.userId) {
|
|
2764
|
+
setCachedUserId(event.siteId, event.visitorId, event.userId);
|
|
2765
|
+
await db.upsertIdentity(event.siteId, event.visitorId, event.userId);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
1814
2769
|
function extractIp(req) {
|
|
1815
2770
|
if (config.trustProxy ?? true) {
|
|
1816
2771
|
const forwarded = req.headers?.["x-forwarded-for"];
|
|
@@ -1824,7 +2779,14 @@ async function createCollector(config) {
|
|
|
1824
2779
|
}
|
|
1825
2780
|
function handler() {
|
|
1826
2781
|
return async (req, res) => {
|
|
1827
|
-
|
|
2782
|
+
res.setHeader?.("Access-Control-Allow-Origin", "*");
|
|
2783
|
+
res.setHeader?.("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
2784
|
+
res.setHeader?.("Access-Control-Allow-Headers", "Content-Type");
|
|
2785
|
+
if (req.method === "OPTIONS") {
|
|
2786
|
+
res.writeHead?.(204);
|
|
2787
|
+
res.end?.();
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
1828
2790
|
if (req.method !== "POST") {
|
|
1829
2791
|
sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
1830
2792
|
return;
|
|
@@ -1865,11 +2827,13 @@ async function createCollector(config) {
|
|
|
1865
2827
|
sendJson(res, 200, { ok: true });
|
|
1866
2828
|
return;
|
|
1867
2829
|
}
|
|
2830
|
+
await processIdentity(filtered);
|
|
1868
2831
|
await db.insertEvents(filtered);
|
|
1869
2832
|
sendJson(res, 200, { ok: true });
|
|
1870
2833
|
return;
|
|
1871
2834
|
}
|
|
1872
2835
|
}
|
|
2836
|
+
await processIdentity(enriched);
|
|
1873
2837
|
await db.insertEvents(enriched);
|
|
1874
2838
|
sendJson(res, 200, { ok: true });
|
|
1875
2839
|
} catch (err) {
|
|
@@ -1903,8 +2867,13 @@ async function createCollector(config) {
|
|
|
1903
2867
|
period: params.period,
|
|
1904
2868
|
dateFrom: params.dateFrom,
|
|
1905
2869
|
dateTo: params.dateTo,
|
|
1906
|
-
granularity: q.granularity
|
|
2870
|
+
granularity: q.granularity,
|
|
2871
|
+
filters: q.filters ? JSON.parse(q.filters) : void 0
|
|
1907
2872
|
};
|
|
2873
|
+
if (tsParams.metric === "conversions") {
|
|
2874
|
+
const site = await db.getSite(params.siteId);
|
|
2875
|
+
tsParams.conversionEvents = site?.conversionEvents ?? [];
|
|
2876
|
+
}
|
|
1908
2877
|
const result2 = await db.queryTimeSeries(tsParams);
|
|
1909
2878
|
sendJson(res, 200, result2);
|
|
1910
2879
|
return;
|
|
@@ -1920,7 +2889,15 @@ async function createCollector(config) {
|
|
|
1920
2889
|
sendJson(res, 200, result2);
|
|
1921
2890
|
return;
|
|
1922
2891
|
}
|
|
1923
|
-
const
|
|
2892
|
+
const isConversionMetric = params.metric === "conversions" || params.metric === "top_conversions";
|
|
2893
|
+
let result;
|
|
2894
|
+
if (isConversionMetric) {
|
|
2895
|
+
const site = await db.getSite(params.siteId);
|
|
2896
|
+
const conversionEvents = site?.conversionEvents ?? [];
|
|
2897
|
+
result = await db.query({ ...params, conversionEvents });
|
|
2898
|
+
} else {
|
|
2899
|
+
result = await db.query(params);
|
|
2900
|
+
}
|
|
1924
2901
|
sendJson(res, 200, result);
|
|
1925
2902
|
} catch (err) {
|
|
1926
2903
|
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : "Internal error" });
|
|
@@ -2017,10 +2994,13 @@ async function createCollector(config) {
|
|
|
2017
2994
|
sendJson(res, 401, { ok: false, error: "Invalid or missing secret key" });
|
|
2018
2995
|
return;
|
|
2019
2996
|
}
|
|
2997
|
+
const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
2020
2998
|
const params = {
|
|
2021
2999
|
siteId: q.siteId,
|
|
2022
3000
|
type: q.type,
|
|
2023
3001
|
eventName: q.eventName,
|
|
3002
|
+
eventNames,
|
|
3003
|
+
eventSource: q.eventSource,
|
|
2024
3004
|
visitorId: q.visitorId,
|
|
2025
3005
|
userId: q.userId,
|
|
2026
3006
|
period: q.period,
|
|
@@ -2060,9 +3040,13 @@ async function createCollector(config) {
|
|
|
2060
3040
|
const visitorId = usersIdx >= 0 ? pathSegments[usersIdx + 1] : void 0;
|
|
2061
3041
|
const action = usersIdx >= 0 ? pathSegments[usersIdx + 2] : void 0;
|
|
2062
3042
|
if (visitorId && action === "events") {
|
|
3043
|
+
const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
2063
3044
|
const params2 = {
|
|
2064
3045
|
siteId: q.siteId,
|
|
2065
3046
|
type: q.type,
|
|
3047
|
+
eventName: q.eventName,
|
|
3048
|
+
eventNames,
|
|
3049
|
+
eventSource: q.eventSource,
|
|
2066
3050
|
period: q.period,
|
|
2067
3051
|
dateFrom: q.dateFrom,
|
|
2068
3052
|
dateTo: q.dateTo,
|
|
@@ -2110,11 +3094,11 @@ async function createCollector(config) {
|
|
|
2110
3094
|
async listUsers(params) {
|
|
2111
3095
|
return db.listUsers(params);
|
|
2112
3096
|
},
|
|
2113
|
-
async getUserDetail(siteId,
|
|
2114
|
-
return db.getUserDetail(siteId,
|
|
3097
|
+
async getUserDetail(siteId, identifier) {
|
|
3098
|
+
return db.getUserDetail(siteId, identifier);
|
|
2115
3099
|
},
|
|
2116
|
-
async getUserEvents(siteId,
|
|
2117
|
-
return db.getUserEvents(siteId,
|
|
3100
|
+
async getUserEvents(siteId, identifier, params) {
|
|
3101
|
+
return db.getUserEvents(siteId, identifier, params);
|
|
2118
3102
|
},
|
|
2119
3103
|
async track(siteId, name, properties, options) {
|
|
2120
3104
|
const event = {
|
|
@@ -2169,7 +3153,8 @@ function createAdapter(config) {
|
|
|
2169
3153
|
}
|
|
2170
3154
|
}
|
|
2171
3155
|
async function parseBody(req) {
|
|
2172
|
-
if (req.body) return req.body;
|
|
3156
|
+
if (req.body && typeof req.body === "object") return req.body;
|
|
3157
|
+
if (typeof req.body === "string") return JSON.parse(req.body);
|
|
2173
3158
|
return new Promise((resolve, reject) => {
|
|
2174
3159
|
let data = "";
|
|
2175
3160
|
req.on("data", (chunk) => {
|