@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.js
CHANGED
|
@@ -140,6 +140,18 @@ function generateSecretKey() {
|
|
|
140
140
|
// src/adapters/clickhouse.ts
|
|
141
141
|
var EVENTS_TABLE = "litemetrics_events";
|
|
142
142
|
var SITES_TABLE = "litemetrics_sites";
|
|
143
|
+
var IDENTITY_MAP_TABLE = "litemetrics_identity_map";
|
|
144
|
+
var CREATE_IDENTITY_MAP_TABLE = `
|
|
145
|
+
CREATE TABLE IF NOT EXISTS ${IDENTITY_MAP_TABLE} (
|
|
146
|
+
site_id LowCardinality(String),
|
|
147
|
+
visitor_id String,
|
|
148
|
+
user_id String,
|
|
149
|
+
identified_at DateTime64(3),
|
|
150
|
+
created_at DateTime64(3) DEFAULT now64(3)
|
|
151
|
+
) ENGINE = ReplacingMergeTree(created_at)
|
|
152
|
+
ORDER BY (site_id, visitor_id)
|
|
153
|
+
SETTINGS index_granularity = 8192
|
|
154
|
+
`;
|
|
143
155
|
var CREATE_EVENTS_TABLE = `
|
|
144
156
|
CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
145
157
|
event_id UUID DEFAULT generateUUIDv4(),
|
|
@@ -153,6 +165,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
|
153
165
|
title Nullable(String),
|
|
154
166
|
event_name Nullable(String),
|
|
155
167
|
properties Nullable(String),
|
|
168
|
+
event_source LowCardinality(Nullable(String)),
|
|
169
|
+
event_subtype LowCardinality(Nullable(String)),
|
|
170
|
+
page_path Nullable(String),
|
|
171
|
+
target_url_path Nullable(String),
|
|
172
|
+
element_selector Nullable(String),
|
|
173
|
+
element_text Nullable(String),
|
|
174
|
+
scroll_depth_pct Nullable(UInt8),
|
|
156
175
|
user_id Nullable(String),
|
|
157
176
|
traits Nullable(String),
|
|
158
177
|
country LowCardinality(Nullable(String)),
|
|
@@ -184,6 +203,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
184
203
|
name String,
|
|
185
204
|
domain Nullable(String),
|
|
186
205
|
allowed_origins Nullable(String),
|
|
206
|
+
conversion_events Nullable(String),
|
|
187
207
|
created_at DateTime64(3),
|
|
188
208
|
updated_at DateTime64(3),
|
|
189
209
|
version UInt64,
|
|
@@ -196,6 +216,39 @@ function toCHDateTime(d) {
|
|
|
196
216
|
const iso = typeof d === "string" ? d : d.toISOString();
|
|
197
217
|
return iso.replace("T", " ").replace("Z", "");
|
|
198
218
|
}
|
|
219
|
+
function buildFilterConditions(filters) {
|
|
220
|
+
if (!filters) return { conditions: [], params: {} };
|
|
221
|
+
const map = {
|
|
222
|
+
"geo.country": "country",
|
|
223
|
+
"geo.city": "city",
|
|
224
|
+
"geo.region": "region",
|
|
225
|
+
"language": "language",
|
|
226
|
+
"device.type": "device_type",
|
|
227
|
+
"device.browser": "browser",
|
|
228
|
+
"device.os": "os",
|
|
229
|
+
"utm.source": "utm_source",
|
|
230
|
+
"utm.medium": "utm_medium",
|
|
231
|
+
"utm.campaign": "utm_campaign",
|
|
232
|
+
"utm.term": "utm_term",
|
|
233
|
+
"utm.content": "utm_content",
|
|
234
|
+
"referrer": "referrer",
|
|
235
|
+
"event_source": "event_source",
|
|
236
|
+
"event_subtype": "event_subtype",
|
|
237
|
+
"page_path": "page_path",
|
|
238
|
+
"target_url_path": "target_url_path",
|
|
239
|
+
"event_name": "event_name",
|
|
240
|
+
"type": "type"
|
|
241
|
+
};
|
|
242
|
+
const conditions = [];
|
|
243
|
+
const params = {};
|
|
244
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
245
|
+
if (!value || !map[key]) continue;
|
|
246
|
+
const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
247
|
+
conditions.push(`${map[key]} = {${paramKey}:String}`);
|
|
248
|
+
params[paramKey] = value;
|
|
249
|
+
}
|
|
250
|
+
return { conditions, params };
|
|
251
|
+
}
|
|
199
252
|
var ClickHouseAdapter = class {
|
|
200
253
|
client;
|
|
201
254
|
constructor(url) {
|
|
@@ -209,6 +262,17 @@ var ClickHouseAdapter = class {
|
|
|
209
262
|
async init() {
|
|
210
263
|
await this.client.command({ query: CREATE_EVENTS_TABLE });
|
|
211
264
|
await this.client.command({ query: CREATE_SITES_TABLE });
|
|
265
|
+
await this.client.command({ query: CREATE_IDENTITY_MAP_TABLE });
|
|
266
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_source LowCardinality(Nullable(String))` });
|
|
267
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_subtype LowCardinality(Nullable(String))` });
|
|
268
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS page_path Nullable(String)` });
|
|
269
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS target_url_path Nullable(String)` });
|
|
270
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_selector Nullable(String)` });
|
|
271
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_text Nullable(String)` });
|
|
272
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS scroll_depth_pct Nullable(UInt8)` });
|
|
273
|
+
await this.client.command({
|
|
274
|
+
query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
|
|
275
|
+
});
|
|
212
276
|
}
|
|
213
277
|
async close() {
|
|
214
278
|
await this.client.close();
|
|
@@ -227,6 +291,13 @@ var ClickHouseAdapter = class {
|
|
|
227
291
|
title: e.title ?? null,
|
|
228
292
|
event_name: e.name ?? null,
|
|
229
293
|
properties: e.properties ? JSON.stringify(e.properties) : null,
|
|
294
|
+
event_source: e.eventSource ?? null,
|
|
295
|
+
event_subtype: e.eventSubtype ?? null,
|
|
296
|
+
page_path: e.pagePath ?? null,
|
|
297
|
+
target_url_path: e.targetUrlPath ?? null,
|
|
298
|
+
element_selector: e.elementSelector ?? null,
|
|
299
|
+
element_text: e.elementText ?? null,
|
|
300
|
+
scroll_depth_pct: e.scrollDepthPct ?? null,
|
|
230
301
|
user_id: e.userId ?? null,
|
|
231
302
|
traits: e.traits ? JSON.stringify(e.traits) : null,
|
|
232
303
|
country: e.geo?.country ?? null,
|
|
@@ -263,6 +334,8 @@ var ClickHouseAdapter = class {
|
|
|
263
334
|
to: toCHDateTime(dateRange.to),
|
|
264
335
|
limit
|
|
265
336
|
};
|
|
337
|
+
const filter = buildFilterConditions(q.filters);
|
|
338
|
+
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
266
339
|
let data = [];
|
|
267
340
|
let total = 0;
|
|
268
341
|
switch (q.metric) {
|
|
@@ -272,8 +345,8 @@ var ClickHouseAdapter = class {
|
|
|
272
345
|
WHERE site_id = {siteId:String}
|
|
273
346
|
AND timestamp >= {from:String}
|
|
274
347
|
AND timestamp <= {to:String}
|
|
275
|
-
AND type = 'pageview'`,
|
|
276
|
-
params
|
|
348
|
+
AND type = 'pageview'${filterSql}`,
|
|
349
|
+
{ ...params, ...filter.params }
|
|
277
350
|
);
|
|
278
351
|
total = Number(rows[0]?.value ?? 0);
|
|
279
352
|
data = [{ key: "pageviews", value: total }];
|
|
@@ -284,8 +357,8 @@ var ClickHouseAdapter = class {
|
|
|
284
357
|
`SELECT uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
285
358
|
WHERE site_id = {siteId:String}
|
|
286
359
|
AND timestamp >= {from:String}
|
|
287
|
-
AND timestamp <= {to:String}`,
|
|
288
|
-
params
|
|
360
|
+
AND timestamp <= {to:String}${filterSql}`,
|
|
361
|
+
{ ...params, ...filter.params }
|
|
289
362
|
);
|
|
290
363
|
total = Number(rows[0]?.value ?? 0);
|
|
291
364
|
data = [{ key: "visitors", value: total }];
|
|
@@ -296,8 +369,8 @@ var ClickHouseAdapter = class {
|
|
|
296
369
|
`SELECT uniq(session_id) AS value FROM ${EVENTS_TABLE}
|
|
297
370
|
WHERE site_id = {siteId:String}
|
|
298
371
|
AND timestamp >= {from:String}
|
|
299
|
-
AND timestamp <= {to:String}`,
|
|
300
|
-
params
|
|
372
|
+
AND timestamp <= {to:String}${filterSql}`,
|
|
373
|
+
{ ...params, ...filter.params }
|
|
301
374
|
);
|
|
302
375
|
total = Number(rows[0]?.value ?? 0);
|
|
303
376
|
data = [{ key: "sessions", value: total }];
|
|
@@ -309,13 +382,33 @@ var ClickHouseAdapter = class {
|
|
|
309
382
|
WHERE site_id = {siteId:String}
|
|
310
383
|
AND timestamp >= {from:String}
|
|
311
384
|
AND timestamp <= {to:String}
|
|
312
|
-
AND type = 'event'`,
|
|
313
|
-
params
|
|
385
|
+
AND type = 'event'${filterSql}`,
|
|
386
|
+
{ ...params, ...filter.params }
|
|
314
387
|
);
|
|
315
388
|
total = Number(rows[0]?.value ?? 0);
|
|
316
389
|
data = [{ key: "events", value: total }];
|
|
317
390
|
break;
|
|
318
391
|
}
|
|
392
|
+
case "conversions": {
|
|
393
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
394
|
+
if (conversionEvents.length === 0) {
|
|
395
|
+
total = 0;
|
|
396
|
+
data = [{ key: "conversions", value: 0 }];
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
const rows = await this.queryRows(
|
|
400
|
+
`SELECT count() AS value FROM ${EVENTS_TABLE}
|
|
401
|
+
WHERE site_id = {siteId:String}
|
|
402
|
+
AND timestamp >= {from:String}
|
|
403
|
+
AND timestamp <= {to:String}
|
|
404
|
+
AND type = 'event'
|
|
405
|
+
AND event_name IN {eventNames:Array(String)}${filterSql}`,
|
|
406
|
+
{ ...params, eventNames: conversionEvents, ...filter.params }
|
|
407
|
+
);
|
|
408
|
+
total = Number(rows[0]?.value ?? 0);
|
|
409
|
+
data = [{ key: "conversions", value: total }];
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
319
412
|
case "top_pages": {
|
|
320
413
|
const rows = await this.queryRows(
|
|
321
414
|
`SELECT url AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
@@ -323,11 +416,11 @@ var ClickHouseAdapter = class {
|
|
|
323
416
|
AND timestamp >= {from:String}
|
|
324
417
|
AND timestamp <= {to:String}
|
|
325
418
|
AND type = 'pageview'
|
|
326
|
-
AND url IS NOT NULL
|
|
419
|
+
AND url IS NOT NULL${filterSql}
|
|
327
420
|
GROUP BY url
|
|
328
421
|
ORDER BY value DESC
|
|
329
422
|
LIMIT {limit:UInt32}`,
|
|
330
|
-
params
|
|
423
|
+
{ ...params, ...filter.params }
|
|
331
424
|
);
|
|
332
425
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
333
426
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -341,11 +434,11 @@ var ClickHouseAdapter = class {
|
|
|
341
434
|
AND timestamp <= {to:String}
|
|
342
435
|
AND type = 'pageview'
|
|
343
436
|
AND referrer IS NOT NULL
|
|
344
|
-
AND referrer != ''
|
|
437
|
+
AND referrer != ''${filterSql}
|
|
345
438
|
GROUP BY referrer
|
|
346
439
|
ORDER BY value DESC
|
|
347
440
|
LIMIT {limit:UInt32}`,
|
|
348
|
-
params
|
|
441
|
+
{ ...params, ...filter.params }
|
|
349
442
|
);
|
|
350
443
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
351
444
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -357,11 +450,11 @@ var ClickHouseAdapter = class {
|
|
|
357
450
|
WHERE site_id = {siteId:String}
|
|
358
451
|
AND timestamp >= {from:String}
|
|
359
452
|
AND timestamp <= {to:String}
|
|
360
|
-
AND country IS NOT NULL
|
|
453
|
+
AND country IS NOT NULL${filterSql}
|
|
361
454
|
GROUP BY country
|
|
362
455
|
ORDER BY value DESC
|
|
363
456
|
LIMIT {limit:UInt32}`,
|
|
364
|
-
params
|
|
457
|
+
{ ...params, ...filter.params }
|
|
365
458
|
);
|
|
366
459
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
367
460
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -373,11 +466,11 @@ var ClickHouseAdapter = class {
|
|
|
373
466
|
WHERE site_id = {siteId:String}
|
|
374
467
|
AND timestamp >= {from:String}
|
|
375
468
|
AND timestamp <= {to:String}
|
|
376
|
-
AND city IS NOT NULL
|
|
469
|
+
AND city IS NOT NULL${filterSql}
|
|
377
470
|
GROUP BY city
|
|
378
471
|
ORDER BY value DESC
|
|
379
472
|
LIMIT {limit:UInt32}`,
|
|
380
|
-
params
|
|
473
|
+
{ ...params, ...filter.params }
|
|
381
474
|
);
|
|
382
475
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
383
476
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -391,10 +484,132 @@ var ClickHouseAdapter = class {
|
|
|
391
484
|
AND timestamp <= {to:String}
|
|
392
485
|
AND type = 'event'
|
|
393
486
|
AND event_name IS NOT NULL
|
|
487
|
+
${filterSql}
|
|
488
|
+
GROUP BY event_name
|
|
489
|
+
ORDER BY value DESC
|
|
490
|
+
LIMIT {limit:UInt32}`,
|
|
491
|
+
{ ...params, ...filter.params }
|
|
492
|
+
);
|
|
493
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
494
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case "top_conversions": {
|
|
498
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
499
|
+
if (conversionEvents.length === 0) {
|
|
500
|
+
total = 0;
|
|
501
|
+
data = [];
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
const rows = await this.queryRows(
|
|
505
|
+
`SELECT event_name AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
506
|
+
WHERE site_id = {siteId:String}
|
|
507
|
+
AND timestamp >= {from:String}
|
|
508
|
+
AND timestamp <= {to:String}
|
|
509
|
+
AND type = 'event'
|
|
510
|
+
AND event_name IN {eventNames:Array(String)}
|
|
511
|
+
${filterSql}
|
|
394
512
|
GROUP BY event_name
|
|
395
513
|
ORDER BY value DESC
|
|
396
514
|
LIMIT {limit:UInt32}`,
|
|
397
|
-
params
|
|
515
|
+
{ ...params, eventNames: conversionEvents, ...filter.params }
|
|
516
|
+
);
|
|
517
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
518
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case "top_exit_pages": {
|
|
522
|
+
const rows = await this.queryRows(
|
|
523
|
+
`SELECT exit_url AS key, count() AS value FROM (
|
|
524
|
+
SELECT session_id, argMax(url, timestamp) AS exit_url
|
|
525
|
+
FROM ${EVENTS_TABLE}
|
|
526
|
+
WHERE site_id = {siteId:String}
|
|
527
|
+
AND timestamp >= {from:String}
|
|
528
|
+
AND timestamp <= {to:String}
|
|
529
|
+
AND type = 'pageview'
|
|
530
|
+
AND url IS NOT NULL${filterSql}
|
|
531
|
+
GROUP BY session_id
|
|
532
|
+
)
|
|
533
|
+
GROUP BY exit_url
|
|
534
|
+
ORDER BY value DESC
|
|
535
|
+
LIMIT {limit:UInt32}`,
|
|
536
|
+
{ ...params, ...filter.params }
|
|
537
|
+
);
|
|
538
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
539
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case "top_transitions": {
|
|
543
|
+
const rows = await this.queryRows(
|
|
544
|
+
`SELECT concat(prev_url, ' \u2192 ', curr_url) AS key, count() AS value FROM (
|
|
545
|
+
SELECT session_id, url AS curr_url,
|
|
546
|
+
lagInFrame(url, 1) OVER (PARTITION BY session_id ORDER BY timestamp ASC) AS prev_url
|
|
547
|
+
FROM ${EVENTS_TABLE}
|
|
548
|
+
WHERE site_id = {siteId:String}
|
|
549
|
+
AND timestamp >= {from:String}
|
|
550
|
+
AND timestamp <= {to:String}
|
|
551
|
+
AND type = 'pageview'
|
|
552
|
+
AND url IS NOT NULL${filterSql}
|
|
553
|
+
)
|
|
554
|
+
WHERE prev_url IS NOT NULL AND prev_url != ''
|
|
555
|
+
GROUP BY key
|
|
556
|
+
ORDER BY value DESC
|
|
557
|
+
LIMIT {limit:UInt32}`,
|
|
558
|
+
{ ...params, ...filter.params }
|
|
559
|
+
);
|
|
560
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
561
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case "top_scroll_pages": {
|
|
565
|
+
const rows = await this.queryRows(
|
|
566
|
+
`SELECT page_path AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
567
|
+
WHERE site_id = {siteId:String}
|
|
568
|
+
AND timestamp >= {from:String}
|
|
569
|
+
AND timestamp <= {to:String}
|
|
570
|
+
AND type = 'event'
|
|
571
|
+
AND event_subtype = 'scroll_depth'
|
|
572
|
+
AND page_path IS NOT NULL${filterSql}
|
|
573
|
+
GROUP BY page_path
|
|
574
|
+
ORDER BY value DESC
|
|
575
|
+
LIMIT {limit:UInt32}`,
|
|
576
|
+
{ ...params, ...filter.params }
|
|
577
|
+
);
|
|
578
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
579
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
case "top_button_clicks": {
|
|
583
|
+
const rows = await this.queryRows(
|
|
584
|
+
`SELECT ifNull(element_text, element_selector) AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
585
|
+
WHERE site_id = {siteId:String}
|
|
586
|
+
AND timestamp >= {from:String}
|
|
587
|
+
AND timestamp <= {to:String}
|
|
588
|
+
AND type = 'event'
|
|
589
|
+
AND event_subtype = 'button_click'
|
|
590
|
+
AND (element_text IS NOT NULL OR element_selector IS NOT NULL)${filterSql}
|
|
591
|
+
GROUP BY key
|
|
592
|
+
ORDER BY value DESC
|
|
593
|
+
LIMIT {limit:UInt32}`,
|
|
594
|
+
{ ...params, ...filter.params }
|
|
595
|
+
);
|
|
596
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
597
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
case "top_link_targets": {
|
|
601
|
+
const rows = await this.queryRows(
|
|
602
|
+
`SELECT target_url_path AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
603
|
+
WHERE site_id = {siteId:String}
|
|
604
|
+
AND timestamp >= {from:String}
|
|
605
|
+
AND timestamp <= {to:String}
|
|
606
|
+
AND type = 'event'
|
|
607
|
+
AND event_subtype IN ('link_click','outbound_click')
|
|
608
|
+
AND target_url_path IS NOT NULL${filterSql}
|
|
609
|
+
GROUP BY target_url_path
|
|
610
|
+
ORDER BY value DESC
|
|
611
|
+
LIMIT {limit:UInt32}`,
|
|
612
|
+
{ ...params, ...filter.params }
|
|
398
613
|
);
|
|
399
614
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
400
615
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -407,10 +622,11 @@ var ClickHouseAdapter = class {
|
|
|
407
622
|
AND timestamp >= {from:String}
|
|
408
623
|
AND timestamp <= {to:String}
|
|
409
624
|
AND device_type IS NOT NULL
|
|
625
|
+
${filterSql}
|
|
410
626
|
GROUP BY device_type
|
|
411
627
|
ORDER BY value DESC
|
|
412
628
|
LIMIT {limit:UInt32}`,
|
|
413
|
-
params
|
|
629
|
+
{ ...params, ...filter.params }
|
|
414
630
|
);
|
|
415
631
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
416
632
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -423,10 +639,11 @@ var ClickHouseAdapter = class {
|
|
|
423
639
|
AND timestamp >= {from:String}
|
|
424
640
|
AND timestamp <= {to:String}
|
|
425
641
|
AND browser IS NOT NULL
|
|
642
|
+
${filterSql}
|
|
426
643
|
GROUP BY browser
|
|
427
644
|
ORDER BY value DESC
|
|
428
645
|
LIMIT {limit:UInt32}`,
|
|
429
|
-
params
|
|
646
|
+
{ ...params, ...filter.params }
|
|
430
647
|
);
|
|
431
648
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
432
649
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -439,10 +656,62 @@ var ClickHouseAdapter = class {
|
|
|
439
656
|
AND timestamp >= {from:String}
|
|
440
657
|
AND timestamp <= {to:String}
|
|
441
658
|
AND os IS NOT NULL
|
|
659
|
+
${filterSql}
|
|
442
660
|
GROUP BY os
|
|
443
661
|
ORDER BY value DESC
|
|
444
662
|
LIMIT {limit:UInt32}`,
|
|
445
|
-
params
|
|
663
|
+
{ ...params, ...filter.params }
|
|
664
|
+
);
|
|
665
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
666
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
case "top_utm_sources": {
|
|
670
|
+
const rows = await this.queryRows(
|
|
671
|
+
`SELECT utm_source AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
672
|
+
WHERE site_id = {siteId:String}
|
|
673
|
+
AND timestamp >= {from:String}
|
|
674
|
+
AND timestamp <= {to:String}
|
|
675
|
+
AND utm_source IS NOT NULL AND utm_source != ''
|
|
676
|
+
${filterSql}
|
|
677
|
+
GROUP BY utm_source
|
|
678
|
+
ORDER BY value DESC
|
|
679
|
+
LIMIT {limit:UInt32}`,
|
|
680
|
+
{ ...params, ...filter.params }
|
|
681
|
+
);
|
|
682
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
683
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case "top_utm_mediums": {
|
|
687
|
+
const rows = await this.queryRows(
|
|
688
|
+
`SELECT utm_medium AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
689
|
+
WHERE site_id = {siteId:String}
|
|
690
|
+
AND timestamp >= {from:String}
|
|
691
|
+
AND timestamp <= {to:String}
|
|
692
|
+
AND utm_medium IS NOT NULL AND utm_medium != ''
|
|
693
|
+
${filterSql}
|
|
694
|
+
GROUP BY utm_medium
|
|
695
|
+
ORDER BY value DESC
|
|
696
|
+
LIMIT {limit:UInt32}`,
|
|
697
|
+
{ ...params, ...filter.params }
|
|
698
|
+
);
|
|
699
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
700
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
case "top_utm_campaigns": {
|
|
704
|
+
const rows = await this.queryRows(
|
|
705
|
+
`SELECT utm_campaign AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
706
|
+
WHERE site_id = {siteId:String}
|
|
707
|
+
AND timestamp >= {from:String}
|
|
708
|
+
AND timestamp <= {to:String}
|
|
709
|
+
AND utm_campaign IS NOT NULL AND utm_campaign != ''
|
|
710
|
+
${filterSql}
|
|
711
|
+
GROUP BY utm_campaign
|
|
712
|
+
ORDER BY value DESC
|
|
713
|
+
LIMIT {limit:UInt32}`,
|
|
714
|
+
{ ...params, ...filter.params }
|
|
446
715
|
);
|
|
447
716
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
448
717
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -450,7 +719,7 @@ var ClickHouseAdapter = class {
|
|
|
450
719
|
}
|
|
451
720
|
}
|
|
452
721
|
const result = { metric: q.metric, period, data, total };
|
|
453
|
-
if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
|
|
722
|
+
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
454
723
|
const prevRange = previousPeriodRange(dateRange);
|
|
455
724
|
const prevResult = await this.query({
|
|
456
725
|
...q,
|
|
@@ -480,7 +749,12 @@ var ClickHouseAdapter = class {
|
|
|
480
749
|
const granularity = params.granularity ?? autoGranularity(period);
|
|
481
750
|
const bucketFn = this.granularityToClickHouseFunc(granularity);
|
|
482
751
|
const dateFormat = granularityToDateFormat(granularity);
|
|
752
|
+
const filter = buildFilterConditions(params.filters);
|
|
753
|
+
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
483
754
|
const typeFilter = params.metric === "pageviews" ? `AND type = 'pageview'` : "";
|
|
755
|
+
const eventsFilter = params.metric === "events" ? `AND type = 'event'` : "";
|
|
756
|
+
const conversionsFilter = params.metric === "conversions" ? `AND type = 'event' AND event_name IN {eventNames:Array(String)}` : "";
|
|
757
|
+
const extraFilters = [typeFilter, eventsFilter, conversionsFilter, filterSql].filter(Boolean).join(" ");
|
|
484
758
|
let sql;
|
|
485
759
|
if (params.metric === "visitors" || params.metric === "sessions") {
|
|
486
760
|
const field = params.metric === "visitors" ? "visitor_id" : "session_id";
|
|
@@ -490,7 +764,7 @@ var ClickHouseAdapter = class {
|
|
|
490
764
|
WHERE site_id = {siteId:String}
|
|
491
765
|
AND timestamp >= {from:String}
|
|
492
766
|
AND timestamp <= {to:String}
|
|
493
|
-
${
|
|
767
|
+
${extraFilters}
|
|
494
768
|
GROUP BY bucket
|
|
495
769
|
ORDER BY bucket ASC
|
|
496
770
|
`;
|
|
@@ -501,7 +775,7 @@ var ClickHouseAdapter = class {
|
|
|
501
775
|
WHERE site_id = {siteId:String}
|
|
502
776
|
AND timestamp >= {from:String}
|
|
503
777
|
AND timestamp <= {to:String}
|
|
504
|
-
${
|
|
778
|
+
${extraFilters}
|
|
505
779
|
GROUP BY bucket
|
|
506
780
|
ORDER BY bucket ASC
|
|
507
781
|
`;
|
|
@@ -509,7 +783,9 @@ var ClickHouseAdapter = class {
|
|
|
509
783
|
const rows = await this.queryRows(sql, {
|
|
510
784
|
siteId: params.siteId,
|
|
511
785
|
from: toCHDateTime(dateRange.from),
|
|
512
|
-
to: toCHDateTime(dateRange.to)
|
|
786
|
+
to: toCHDateTime(dateRange.to),
|
|
787
|
+
eventNames: params.conversionEvents ?? [],
|
|
788
|
+
...filter.params
|
|
513
789
|
});
|
|
514
790
|
const mappedRows = rows.map((r) => ({
|
|
515
791
|
_id: this.convertClickHouseBucket(r.bucket, granularity),
|
|
@@ -627,6 +903,14 @@ var ClickHouseAdapter = class {
|
|
|
627
903
|
conditions.push(`event_name = {eventName:String}`);
|
|
628
904
|
queryParams.eventName = params.eventName;
|
|
629
905
|
}
|
|
906
|
+
if (params.eventSource) {
|
|
907
|
+
conditions.push(`event_source = {eventSource:String}`);
|
|
908
|
+
queryParams.eventSource = params.eventSource;
|
|
909
|
+
}
|
|
910
|
+
if (params.eventNames && params.eventNames.length > 0) {
|
|
911
|
+
conditions.push(`event_name IN {eventNames:Array(String)}`);
|
|
912
|
+
queryParams.eventNames = params.eventNames;
|
|
913
|
+
}
|
|
630
914
|
if (params.visitorId) {
|
|
631
915
|
conditions.push(`visitor_id = {visitorId:String}`);
|
|
632
916
|
queryParams.visitorId = params.visitorId;
|
|
@@ -649,7 +933,9 @@ var ClickHouseAdapter = class {
|
|
|
649
933
|
const [events, countRows] = await Promise.all([
|
|
650
934
|
this.queryRows(
|
|
651
935
|
`SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
|
|
652
|
-
event_name, properties,
|
|
936
|
+
event_name, properties, event_source, event_subtype, page_path, target_url_path,
|
|
937
|
+
element_selector, element_text, scroll_depth_pct,
|
|
938
|
+
user_id, traits, country, city, region,
|
|
653
939
|
device_type, browser, os, language,
|
|
654
940
|
utm_source, utm_medium, utm_campaign, utm_term, utm_content
|
|
655
941
|
FROM ${EVENTS_TABLE}
|
|
@@ -684,36 +970,59 @@ var ClickHouseAdapter = class {
|
|
|
684
970
|
const where = conditions.join(" AND ");
|
|
685
971
|
const [userRows, countRows] = await Promise.all([
|
|
686
972
|
this.queryRows(
|
|
687
|
-
`
|
|
688
|
-
visitor_id,
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
973
|
+
`WITH identity AS (
|
|
974
|
+
SELECT visitor_id, user_id
|
|
975
|
+
FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
976
|
+
WHERE site_id = {siteId:String}
|
|
977
|
+
)
|
|
978
|
+
SELECT
|
|
979
|
+
if(i.user_id IS NOT NULL AND i.user_id != '', i.user_id, e.visitor_id) AS group_key,
|
|
980
|
+
anyLast(e.visitor_id) AS visitor_id,
|
|
981
|
+
anyLast(i.user_id) AS userId,
|
|
982
|
+
anyLast(e.traits) AS traits,
|
|
983
|
+
min(e.timestamp) AS firstSeen,
|
|
984
|
+
max(e.timestamp) AS lastSeen,
|
|
693
985
|
count() AS totalEvents,
|
|
694
|
-
countIf(type = 'pageview') AS totalPageviews,
|
|
695
|
-
uniq(session_id) AS totalSessions,
|
|
696
|
-
anyLast(url) AS lastUrl,
|
|
697
|
-
anyLast(
|
|
698
|
-
anyLast(
|
|
699
|
-
anyLast(
|
|
700
|
-
anyLast(
|
|
701
|
-
anyLast(
|
|
702
|
-
anyLast(
|
|
703
|
-
anyLast(
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
986
|
+
countIf(e.type = 'pageview') AS totalPageviews,
|
|
987
|
+
uniq(e.session_id) AS totalSessions,
|
|
988
|
+
anyLast(e.url) AS lastUrl,
|
|
989
|
+
anyLast(e.referrer) AS referrer,
|
|
990
|
+
anyLast(e.device_type) AS device_type,
|
|
991
|
+
anyLast(e.browser) AS browser,
|
|
992
|
+
anyLast(e.os) AS os,
|
|
993
|
+
anyLast(e.country) AS country,
|
|
994
|
+
anyLast(e.city) AS city,
|
|
995
|
+
anyLast(e.region) AS region,
|
|
996
|
+
anyLast(e.language) AS language,
|
|
997
|
+
anyLast(e.timezone) AS timezone,
|
|
998
|
+
anyLast(e.screen_width) AS screen_width,
|
|
999
|
+
anyLast(e.screen_height) AS screen_height,
|
|
1000
|
+
anyLast(e.utm_source) AS utm_source,
|
|
1001
|
+
anyLast(e.utm_medium) AS utm_medium,
|
|
1002
|
+
anyLast(e.utm_campaign) AS utm_campaign,
|
|
1003
|
+
anyLast(e.utm_term) AS utm_term,
|
|
1004
|
+
anyLast(e.utm_content) AS utm_content
|
|
1005
|
+
FROM ${EVENTS_TABLE} e
|
|
1006
|
+
LEFT JOIN identity i ON e.visitor_id = i.visitor_id
|
|
1007
|
+
WHERE e.site_id = {siteId:String}${where.includes("ILIKE") ? ` AND (e.visitor_id ILIKE {search:String} OR i.user_id ILIKE {search:String})` : ""}
|
|
1008
|
+
GROUP BY group_key
|
|
707
1009
|
ORDER BY lastSeen DESC
|
|
708
1010
|
LIMIT {limit:UInt32}
|
|
709
1011
|
OFFSET {offset:UInt32}`,
|
|
710
1012
|
queryParams
|
|
711
1013
|
),
|
|
712
1014
|
this.queryRows(
|
|
713
|
-
`
|
|
714
|
-
SELECT visitor_id
|
|
715
|
-
|
|
716
|
-
|
|
1015
|
+
`WITH identity AS (
|
|
1016
|
+
SELECT visitor_id, user_id
|
|
1017
|
+
FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1018
|
+
WHERE site_id = {siteId:String}
|
|
1019
|
+
)
|
|
1020
|
+
SELECT count() AS total FROM (
|
|
1021
|
+
SELECT if(i.user_id IS NOT NULL AND i.user_id != '', i.user_id, e.visitor_id) AS group_key
|
|
1022
|
+
FROM ${EVENTS_TABLE} e
|
|
1023
|
+
LEFT JOIN identity i ON e.visitor_id = i.visitor_id
|
|
1024
|
+
WHERE e.site_id = {siteId:String}${where.includes("ILIKE") ? ` AND (e.visitor_id ILIKE {search:String} OR i.user_id ILIKE {search:String})` : ""}
|
|
1025
|
+
GROUP BY group_key
|
|
717
1026
|
)`,
|
|
718
1027
|
queryParams
|
|
719
1028
|
)
|
|
@@ -728,9 +1037,19 @@ var ClickHouseAdapter = class {
|
|
|
728
1037
|
totalPageviews: Number(u.totalPageviews),
|
|
729
1038
|
totalSessions: Number(u.totalSessions),
|
|
730
1039
|
lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
|
|
1040
|
+
referrer: u.referrer ? String(u.referrer) : void 0,
|
|
731
1041
|
device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
|
|
732
1042
|
geo: u.country ? { country: String(u.country), city: u.city ? String(u.city) : void 0, region: u.region ? String(u.region) : void 0 } : void 0,
|
|
733
|
-
language: u.language ? String(u.language) : void 0
|
|
1043
|
+
language: u.language ? String(u.language) : void 0,
|
|
1044
|
+
timezone: u.timezone ? String(u.timezone) : void 0,
|
|
1045
|
+
screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
|
|
1046
|
+
utm: u.utm_source ? {
|
|
1047
|
+
source: String(u.utm_source),
|
|
1048
|
+
medium: u.utm_medium ? String(u.utm_medium) : void 0,
|
|
1049
|
+
campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
|
|
1050
|
+
term: u.utm_term ? String(u.utm_term) : void 0,
|
|
1051
|
+
content: u.utm_content ? String(u.utm_content) : void 0
|
|
1052
|
+
} : void 0
|
|
734
1053
|
}));
|
|
735
1054
|
return {
|
|
736
1055
|
users,
|
|
@@ -739,13 +1058,178 @@ var ClickHouseAdapter = class {
|
|
|
739
1058
|
offset
|
|
740
1059
|
};
|
|
741
1060
|
}
|
|
742
|
-
async getUserDetail(siteId,
|
|
743
|
-
const
|
|
744
|
-
|
|
1061
|
+
async getUserDetail(siteId, identifier) {
|
|
1062
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
1063
|
+
if (visitorIds.length > 0) {
|
|
1064
|
+
return this.getMergedUserDetail(siteId, identifier, visitorIds);
|
|
1065
|
+
}
|
|
1066
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
1067
|
+
if (userId) {
|
|
1068
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
1069
|
+
return this.getMergedUserDetail(siteId, userId, allVisitorIds.length > 0 ? allVisitorIds : [identifier]);
|
|
1070
|
+
}
|
|
1071
|
+
const result = await this.listUsers({ siteId, search: identifier, limit: 1 });
|
|
1072
|
+
const user = result.users.find((u) => u.visitorId === identifier);
|
|
745
1073
|
return user ?? null;
|
|
746
1074
|
}
|
|
747
|
-
async getUserEvents(siteId,
|
|
748
|
-
|
|
1075
|
+
async getUserEvents(siteId, identifier, params) {
|
|
1076
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
1077
|
+
if (visitorIds.length > 0) {
|
|
1078
|
+
return this.listEventsForVisitorIds(siteId, visitorIds, params);
|
|
1079
|
+
}
|
|
1080
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
1081
|
+
if (userId) {
|
|
1082
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
1083
|
+
if (allVisitorIds.length > 0) {
|
|
1084
|
+
return this.listEventsForVisitorIds(siteId, allVisitorIds, params);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return this.listEvents({ ...params, siteId, visitorId: identifier });
|
|
1088
|
+
}
|
|
1089
|
+
// ─── Identity Mapping ──────────────────────────────────────
|
|
1090
|
+
async upsertIdentity(siteId, visitorId, userId) {
|
|
1091
|
+
await this.client.insert({
|
|
1092
|
+
table: IDENTITY_MAP_TABLE,
|
|
1093
|
+
values: [{
|
|
1094
|
+
site_id: siteId,
|
|
1095
|
+
visitor_id: visitorId,
|
|
1096
|
+
user_id: userId,
|
|
1097
|
+
identified_at: toCHDateTime(/* @__PURE__ */ new Date())
|
|
1098
|
+
}],
|
|
1099
|
+
format: "JSONEachRow"
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
async getVisitorIdsForUser(siteId, userId) {
|
|
1103
|
+
const rows = await this.queryRows(
|
|
1104
|
+
`SELECT visitor_id FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1105
|
+
WHERE site_id = {siteId:String} AND user_id = {userId:String}`,
|
|
1106
|
+
{ siteId, userId }
|
|
1107
|
+
);
|
|
1108
|
+
return rows.map((r) => r.visitor_id);
|
|
1109
|
+
}
|
|
1110
|
+
async getUserIdForVisitor(siteId, visitorId) {
|
|
1111
|
+
const rows = await this.queryRows(
|
|
1112
|
+
`SELECT user_id FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1113
|
+
WHERE site_id = {siteId:String} AND visitor_id = {visitorId:String}
|
|
1114
|
+
LIMIT 1`,
|
|
1115
|
+
{ siteId, visitorId }
|
|
1116
|
+
);
|
|
1117
|
+
return rows.length > 0 ? rows[0].user_id : null;
|
|
1118
|
+
}
|
|
1119
|
+
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
1120
|
+
const rows = await this.queryRows(
|
|
1121
|
+
`SELECT
|
|
1122
|
+
anyLast(visitor_id) AS visitor_id,
|
|
1123
|
+
anyLast(traits) AS traits,
|
|
1124
|
+
min(timestamp) AS firstSeen,
|
|
1125
|
+
max(timestamp) AS lastSeen,
|
|
1126
|
+
count() AS totalEvents,
|
|
1127
|
+
countIf(type = 'pageview') AS totalPageviews,
|
|
1128
|
+
uniq(session_id) AS totalSessions,
|
|
1129
|
+
anyLast(url) AS lastUrl,
|
|
1130
|
+
anyLast(referrer) AS referrer,
|
|
1131
|
+
anyLast(device_type) AS device_type,
|
|
1132
|
+
anyLast(browser) AS browser,
|
|
1133
|
+
anyLast(os) AS os,
|
|
1134
|
+
anyLast(country) AS country,
|
|
1135
|
+
anyLast(city) AS city,
|
|
1136
|
+
anyLast(region) AS region,
|
|
1137
|
+
anyLast(language) AS language,
|
|
1138
|
+
anyLast(timezone) AS timezone,
|
|
1139
|
+
anyLast(screen_width) AS screen_width,
|
|
1140
|
+
anyLast(screen_height) AS screen_height,
|
|
1141
|
+
anyLast(utm_source) AS utm_source,
|
|
1142
|
+
anyLast(utm_medium) AS utm_medium,
|
|
1143
|
+
anyLast(utm_campaign) AS utm_campaign,
|
|
1144
|
+
anyLast(utm_term) AS utm_term,
|
|
1145
|
+
anyLast(utm_content) AS utm_content
|
|
1146
|
+
FROM ${EVENTS_TABLE}
|
|
1147
|
+
WHERE site_id = {siteId:String}
|
|
1148
|
+
AND visitor_id IN {visitorIds:Array(String)}`,
|
|
1149
|
+
{ siteId, visitorIds }
|
|
1150
|
+
);
|
|
1151
|
+
if (rows.length === 0) return null;
|
|
1152
|
+
const u = rows[0];
|
|
1153
|
+
return {
|
|
1154
|
+
visitorId: String(u.visitor_id),
|
|
1155
|
+
visitorIds,
|
|
1156
|
+
userId,
|
|
1157
|
+
traits: this.parseJSON(u.traits),
|
|
1158
|
+
firstSeen: new Date(String(u.firstSeen)).toISOString(),
|
|
1159
|
+
lastSeen: new Date(String(u.lastSeen)).toISOString(),
|
|
1160
|
+
totalEvents: Number(u.totalEvents),
|
|
1161
|
+
totalPageviews: Number(u.totalPageviews),
|
|
1162
|
+
totalSessions: Number(u.totalSessions),
|
|
1163
|
+
lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
|
|
1164
|
+
referrer: u.referrer ? String(u.referrer) : void 0,
|
|
1165
|
+
device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
|
|
1166
|
+
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,
|
|
1167
|
+
language: u.language ? String(u.language) : void 0,
|
|
1168
|
+
timezone: u.timezone ? String(u.timezone) : void 0,
|
|
1169
|
+
screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
|
|
1170
|
+
utm: u.utm_source ? {
|
|
1171
|
+
source: String(u.utm_source),
|
|
1172
|
+
medium: u.utm_medium ? String(u.utm_medium) : void 0,
|
|
1173
|
+
campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
|
|
1174
|
+
term: u.utm_term ? String(u.utm_term) : void 0,
|
|
1175
|
+
content: u.utm_content ? String(u.utm_content) : void 0
|
|
1176
|
+
} : void 0
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
async listEventsForVisitorIds(siteId, visitorIds, params) {
|
|
1180
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
1181
|
+
const offset = params.offset ?? 0;
|
|
1182
|
+
const conditions = [`site_id = {siteId:String}`, `visitor_id IN {visitorIds:Array(String)}`];
|
|
1183
|
+
const queryParams = { siteId, visitorIds, limit, offset };
|
|
1184
|
+
if (params.type) {
|
|
1185
|
+
conditions.push(`type = {type:String}`);
|
|
1186
|
+
queryParams.type = params.type;
|
|
1187
|
+
}
|
|
1188
|
+
if (params.eventName) {
|
|
1189
|
+
conditions.push(`event_name = {eventName:String}`);
|
|
1190
|
+
queryParams.eventName = params.eventName;
|
|
1191
|
+
}
|
|
1192
|
+
if (params.eventNames && params.eventNames.length > 0) {
|
|
1193
|
+
conditions.push(`event_name IN {eventNames:Array(String)}`);
|
|
1194
|
+
queryParams.eventNames = params.eventNames;
|
|
1195
|
+
}
|
|
1196
|
+
if (params.period || params.dateFrom) {
|
|
1197
|
+
const { dateRange } = resolvePeriod({
|
|
1198
|
+
period: params.period,
|
|
1199
|
+
dateFrom: params.dateFrom,
|
|
1200
|
+
dateTo: params.dateTo
|
|
1201
|
+
});
|
|
1202
|
+
conditions.push(`timestamp >= {from:String} AND timestamp <= {to:String}`);
|
|
1203
|
+
queryParams.from = toCHDateTime(dateRange.from);
|
|
1204
|
+
queryParams.to = toCHDateTime(dateRange.to);
|
|
1205
|
+
}
|
|
1206
|
+
const where = conditions.join(" AND ");
|
|
1207
|
+
const [events, countRows] = await Promise.all([
|
|
1208
|
+
this.queryRows(
|
|
1209
|
+
`SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
|
|
1210
|
+
event_name, properties, event_source, event_subtype, page_path, target_url_path,
|
|
1211
|
+
element_selector, element_text, scroll_depth_pct,
|
|
1212
|
+
user_id, traits, country, city, region,
|
|
1213
|
+
device_type, browser, os, language,
|
|
1214
|
+
utm_source, utm_medium, utm_campaign, utm_term, utm_content
|
|
1215
|
+
FROM ${EVENTS_TABLE}
|
|
1216
|
+
WHERE ${where}
|
|
1217
|
+
ORDER BY timestamp DESC
|
|
1218
|
+
LIMIT {limit:UInt32}
|
|
1219
|
+
OFFSET {offset:UInt32}`,
|
|
1220
|
+
queryParams
|
|
1221
|
+
),
|
|
1222
|
+
this.queryRows(
|
|
1223
|
+
`SELECT count() AS total FROM ${EVENTS_TABLE} WHERE ${where}`,
|
|
1224
|
+
queryParams
|
|
1225
|
+
)
|
|
1226
|
+
]);
|
|
1227
|
+
return {
|
|
1228
|
+
events: events.map((e) => this.toEventListItem(e)),
|
|
1229
|
+
total: Number(countRows[0]?.total ?? 0),
|
|
1230
|
+
limit,
|
|
1231
|
+
offset
|
|
1232
|
+
};
|
|
749
1233
|
}
|
|
750
1234
|
// ─── Site Management ──────────────────────────────────────
|
|
751
1235
|
async createSite(data) {
|
|
@@ -758,6 +1242,7 @@ var ClickHouseAdapter = class {
|
|
|
758
1242
|
name: data.name,
|
|
759
1243
|
domain: data.domain,
|
|
760
1244
|
allowedOrigins: data.allowedOrigins,
|
|
1245
|
+
conversionEvents: data.conversionEvents,
|
|
761
1246
|
createdAt: nowISO,
|
|
762
1247
|
updatedAt: nowISO
|
|
763
1248
|
};
|
|
@@ -769,6 +1254,7 @@ var ClickHouseAdapter = class {
|
|
|
769
1254
|
name: site.name,
|
|
770
1255
|
domain: site.domain ?? null,
|
|
771
1256
|
allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
|
|
1257
|
+
conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
|
|
772
1258
|
created_at: nowCH,
|
|
773
1259
|
updated_at: nowCH,
|
|
774
1260
|
version: 1,
|
|
@@ -780,7 +1266,7 @@ var ClickHouseAdapter = class {
|
|
|
780
1266
|
}
|
|
781
1267
|
async getSite(siteId) {
|
|
782
1268
|
const rows = await this.queryRows(
|
|
783
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1269
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
784
1270
|
FROM ${SITES_TABLE} FINAL
|
|
785
1271
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
786
1272
|
{ siteId }
|
|
@@ -789,7 +1275,7 @@ var ClickHouseAdapter = class {
|
|
|
789
1275
|
}
|
|
790
1276
|
async getSiteBySecret(secretKey) {
|
|
791
1277
|
const rows = await this.queryRows(
|
|
792
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1278
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
793
1279
|
FROM ${SITES_TABLE} FINAL
|
|
794
1280
|
WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
|
|
795
1281
|
{ secretKey }
|
|
@@ -798,7 +1284,7 @@ var ClickHouseAdapter = class {
|
|
|
798
1284
|
}
|
|
799
1285
|
async listSites() {
|
|
800
1286
|
const rows = await this.queryRows(
|
|
801
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1287
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
802
1288
|
FROM ${SITES_TABLE} FINAL
|
|
803
1289
|
WHERE is_deleted = 0
|
|
804
1290
|
ORDER BY created_at DESC`,
|
|
@@ -808,7 +1294,7 @@ var ClickHouseAdapter = class {
|
|
|
808
1294
|
}
|
|
809
1295
|
async updateSite(siteId, data) {
|
|
810
1296
|
const currentRows = await this.queryRows(
|
|
811
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at, version
|
|
1297
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
812
1298
|
FROM ${SITES_TABLE} FINAL
|
|
813
1299
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
814
1300
|
{ siteId }
|
|
@@ -822,6 +1308,7 @@ var ClickHouseAdapter = class {
|
|
|
822
1308
|
const newName = data.name !== void 0 ? data.name : String(current.name);
|
|
823
1309
|
const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
|
|
824
1310
|
const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
|
|
1311
|
+
const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
|
|
825
1312
|
await this.client.insert({
|
|
826
1313
|
table: SITES_TABLE,
|
|
827
1314
|
values: [{
|
|
@@ -830,6 +1317,7 @@ var ClickHouseAdapter = class {
|
|
|
830
1317
|
name: newName,
|
|
831
1318
|
domain: newDomain,
|
|
832
1319
|
allowed_origins: newOrigins,
|
|
1320
|
+
conversion_events: newConversions,
|
|
833
1321
|
created_at: toCHDateTime(String(current.created_at)),
|
|
834
1322
|
updated_at: nowCH,
|
|
835
1323
|
version: newVersion,
|
|
@@ -843,13 +1331,14 @@ var ClickHouseAdapter = class {
|
|
|
843
1331
|
name: newName,
|
|
844
1332
|
domain: newDomain ?? void 0,
|
|
845
1333
|
allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
|
|
1334
|
+
conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
|
|
846
1335
|
createdAt: String(current.created_at),
|
|
847
1336
|
updatedAt: nowISO
|
|
848
1337
|
};
|
|
849
1338
|
}
|
|
850
1339
|
async deleteSite(siteId) {
|
|
851
1340
|
const currentRows = await this.queryRows(
|
|
852
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
|
|
1341
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
853
1342
|
FROM ${SITES_TABLE} FINAL
|
|
854
1343
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
855
1344
|
{ siteId }
|
|
@@ -865,6 +1354,7 @@ var ClickHouseAdapter = class {
|
|
|
865
1354
|
name: String(current.name),
|
|
866
1355
|
domain: current.domain ? String(current.domain) : null,
|
|
867
1356
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1357
|
+
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
868
1358
|
created_at: toCHDateTime(String(current.created_at)),
|
|
869
1359
|
updated_at: nowCH,
|
|
870
1360
|
version: Number(current.version) + 1,
|
|
@@ -876,7 +1366,7 @@ var ClickHouseAdapter = class {
|
|
|
876
1366
|
}
|
|
877
1367
|
async regenerateSecret(siteId) {
|
|
878
1368
|
const currentRows = await this.queryRows(
|
|
879
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
|
|
1369
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
880
1370
|
FROM ${SITES_TABLE} FINAL
|
|
881
1371
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
882
1372
|
{ siteId }
|
|
@@ -895,6 +1385,7 @@ var ClickHouseAdapter = class {
|
|
|
895
1385
|
name: String(current.name),
|
|
896
1386
|
domain: current.domain ? String(current.domain) : null,
|
|
897
1387
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1388
|
+
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
898
1389
|
created_at: toCHDateTime(String(current.created_at)),
|
|
899
1390
|
updated_at: nowCH,
|
|
900
1391
|
version: Number(current.version) + 1,
|
|
@@ -908,6 +1399,7 @@ var ClickHouseAdapter = class {
|
|
|
908
1399
|
name: String(current.name),
|
|
909
1400
|
domain: current.domain ? String(current.domain) : void 0,
|
|
910
1401
|
allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
|
|
1402
|
+
conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
|
|
911
1403
|
createdAt: String(current.created_at),
|
|
912
1404
|
updatedAt: nowISO
|
|
913
1405
|
};
|
|
@@ -928,6 +1420,7 @@ var ClickHouseAdapter = class {
|
|
|
928
1420
|
name: String(row.name),
|
|
929
1421
|
domain: row.domain ? String(row.domain) : void 0,
|
|
930
1422
|
allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
|
|
1423
|
+
conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
|
|
931
1424
|
createdAt: new Date(String(row.created_at)).toISOString(),
|
|
932
1425
|
updatedAt: new Date(String(row.updated_at)).toISOString()
|
|
933
1426
|
};
|
|
@@ -944,6 +1437,13 @@ var ClickHouseAdapter = class {
|
|
|
944
1437
|
title: row.title ? String(row.title) : void 0,
|
|
945
1438
|
name: row.event_name ? String(row.event_name) : void 0,
|
|
946
1439
|
properties: this.parseJSON(row.properties),
|
|
1440
|
+
eventSource: row.event_source ? String(row.event_source) : void 0,
|
|
1441
|
+
eventSubtype: row.event_subtype ? String(row.event_subtype) : void 0,
|
|
1442
|
+
pagePath: row.page_path ? String(row.page_path) : void 0,
|
|
1443
|
+
targetUrlPath: row.target_url_path ? String(row.target_url_path) : void 0,
|
|
1444
|
+
elementSelector: row.element_selector ? String(row.element_selector) : void 0,
|
|
1445
|
+
elementText: row.element_text ? String(row.element_text) : void 0,
|
|
1446
|
+
scrollDepthPct: row.scroll_depth_pct !== null && row.scroll_depth_pct !== void 0 ? Number(row.scroll_depth_pct) : void 0,
|
|
947
1447
|
userId: row.user_id ? String(row.user_id) : void 0,
|
|
948
1448
|
traits: this.parseJSON(row.traits),
|
|
949
1449
|
geo: row.country ? {
|
|
@@ -980,11 +1480,43 @@ var ClickHouseAdapter = class {
|
|
|
980
1480
|
import { MongoClient } from "mongodb";
|
|
981
1481
|
var EVENTS_COLLECTION = "litemetrics_events";
|
|
982
1482
|
var SITES_COLLECTION = "litemetrics_sites";
|
|
1483
|
+
var IDENTITY_MAP_COLLECTION = "litemetrics_identity_map";
|
|
1484
|
+
function buildFilterMatch(filters) {
|
|
1485
|
+
if (!filters) return {};
|
|
1486
|
+
const map = {
|
|
1487
|
+
"geo.country": "country",
|
|
1488
|
+
"geo.city": "city",
|
|
1489
|
+
"geo.region": "region",
|
|
1490
|
+
"language": "language",
|
|
1491
|
+
"device.type": "device_type",
|
|
1492
|
+
"device.browser": "browser",
|
|
1493
|
+
"device.os": "os",
|
|
1494
|
+
"utm.source": "utm_source",
|
|
1495
|
+
"utm.medium": "utm_medium",
|
|
1496
|
+
"utm.campaign": "utm_campaign",
|
|
1497
|
+
"utm.term": "utm_term",
|
|
1498
|
+
"utm.content": "utm_content",
|
|
1499
|
+
"referrer": "referrer",
|
|
1500
|
+
"event_source": "event_source",
|
|
1501
|
+
"event_subtype": "event_subtype",
|
|
1502
|
+
"page_path": "page_path",
|
|
1503
|
+
"target_url_path": "target_url_path",
|
|
1504
|
+
"event_name": "event_name",
|
|
1505
|
+
"type": "type"
|
|
1506
|
+
};
|
|
1507
|
+
const match = {};
|
|
1508
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
1509
|
+
if (!value || !map[key]) continue;
|
|
1510
|
+
match[map[key]] = value;
|
|
1511
|
+
}
|
|
1512
|
+
return match;
|
|
1513
|
+
}
|
|
983
1514
|
var MongoDBAdapter = class {
|
|
984
1515
|
client;
|
|
985
1516
|
db;
|
|
986
1517
|
collection;
|
|
987
1518
|
sites;
|
|
1519
|
+
identityMap;
|
|
988
1520
|
constructor(url) {
|
|
989
1521
|
this.client = new MongoClient(url);
|
|
990
1522
|
}
|
|
@@ -993,13 +1525,16 @@ var MongoDBAdapter = class {
|
|
|
993
1525
|
this.db = this.client.db();
|
|
994
1526
|
this.collection = this.db.collection(EVENTS_COLLECTION);
|
|
995
1527
|
this.sites = this.db.collection(SITES_COLLECTION);
|
|
1528
|
+
this.identityMap = this.db.collection(IDENTITY_MAP_COLLECTION);
|
|
996
1529
|
await Promise.all([
|
|
997
1530
|
this.collection.createIndex({ site_id: 1, timestamp: -1 }),
|
|
998
1531
|
this.collection.createIndex({ site_id: 1, type: 1 }),
|
|
999
1532
|
this.collection.createIndex({ site_id: 1, visitor_id: 1 }),
|
|
1000
1533
|
this.collection.createIndex({ site_id: 1, session_id: 1 }),
|
|
1001
1534
|
this.sites.createIndex({ site_id: 1 }, { unique: true }),
|
|
1002
|
-
this.sites.createIndex({ secret_key: 1 })
|
|
1535
|
+
this.sites.createIndex({ secret_key: 1 }),
|
|
1536
|
+
this.identityMap.createIndex({ site_id: 1, visitor_id: 1 }, { unique: true }),
|
|
1537
|
+
this.identityMap.createIndex({ site_id: 1, user_id: 1 })
|
|
1003
1538
|
]);
|
|
1004
1539
|
}
|
|
1005
1540
|
async insertEvents(events) {
|
|
@@ -1015,6 +1550,13 @@ var MongoDBAdapter = class {
|
|
|
1015
1550
|
title: e.title ?? null,
|
|
1016
1551
|
event_name: e.name ?? null,
|
|
1017
1552
|
properties: e.properties ?? null,
|
|
1553
|
+
event_source: e.eventSource ?? null,
|
|
1554
|
+
event_subtype: e.eventSubtype ?? null,
|
|
1555
|
+
page_path: e.pagePath ?? null,
|
|
1556
|
+
target_url_path: e.targetUrlPath ?? null,
|
|
1557
|
+
element_selector: e.elementSelector ?? null,
|
|
1558
|
+
element_text: e.elementText ?? null,
|
|
1559
|
+
scroll_depth_pct: e.scrollDepthPct ?? null,
|
|
1018
1560
|
user_id: e.userId ?? null,
|
|
1019
1561
|
traits: e.traits ?? null,
|
|
1020
1562
|
country: e.geo?.country ?? null,
|
|
@@ -1045,12 +1587,13 @@ var MongoDBAdapter = class {
|
|
|
1045
1587
|
site_id: siteId,
|
|
1046
1588
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1047
1589
|
};
|
|
1590
|
+
const filterMatch = buildFilterMatch(q.filters);
|
|
1048
1591
|
let data = [];
|
|
1049
1592
|
let total = 0;
|
|
1050
1593
|
switch (q.metric) {
|
|
1051
1594
|
case "pageviews": {
|
|
1052
1595
|
const [result2] = await this.collection.aggregate([
|
|
1053
|
-
{ $match: { ...baseMatch, type: "pageview" } },
|
|
1596
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview" } },
|
|
1054
1597
|
{ $count: "count" }
|
|
1055
1598
|
]).toArray();
|
|
1056
1599
|
total = result2?.count ?? 0;
|
|
@@ -1059,7 +1602,7 @@ var MongoDBAdapter = class {
|
|
|
1059
1602
|
}
|
|
1060
1603
|
case "visitors": {
|
|
1061
1604
|
const [result2] = await this.collection.aggregate([
|
|
1062
|
-
{ $match: baseMatch },
|
|
1605
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1063
1606
|
{ $group: { _id: "$visitor_id" } },
|
|
1064
1607
|
{ $count: "count" }
|
|
1065
1608
|
]).toArray();
|
|
@@ -1069,7 +1612,7 @@ var MongoDBAdapter = class {
|
|
|
1069
1612
|
}
|
|
1070
1613
|
case "sessions": {
|
|
1071
1614
|
const [result2] = await this.collection.aggregate([
|
|
1072
|
-
{ $match: baseMatch },
|
|
1615
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1073
1616
|
{ $group: { _id: "$session_id" } },
|
|
1074
1617
|
{ $count: "count" }
|
|
1075
1618
|
]).toArray();
|
|
@@ -1079,16 +1622,31 @@ var MongoDBAdapter = class {
|
|
|
1079
1622
|
}
|
|
1080
1623
|
case "events": {
|
|
1081
1624
|
const [result2] = await this.collection.aggregate([
|
|
1082
|
-
{ $match: { ...baseMatch, type: "event" } },
|
|
1625
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event" } },
|
|
1083
1626
|
{ $count: "count" }
|
|
1084
1627
|
]).toArray();
|
|
1085
1628
|
total = result2?.count ?? 0;
|
|
1086
1629
|
data = [{ key: "events", value: total }];
|
|
1087
1630
|
break;
|
|
1088
1631
|
}
|
|
1632
|
+
case "conversions": {
|
|
1633
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
1634
|
+
if (conversionEvents.length === 0) {
|
|
1635
|
+
total = 0;
|
|
1636
|
+
data = [{ key: "conversions", value: 0 }];
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1639
|
+
const [result2] = await this.collection.aggregate([
|
|
1640
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
|
|
1641
|
+
{ $count: "count" }
|
|
1642
|
+
]).toArray();
|
|
1643
|
+
total = result2?.count ?? 0;
|
|
1644
|
+
data = [{ key: "conversions", value: total }];
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1089
1647
|
case "top_pages": {
|
|
1090
1648
|
const rows = await this.collection.aggregate([
|
|
1091
|
-
{ $match: { ...baseMatch, type: "pageview", url: { $ne: null } } },
|
|
1649
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1092
1650
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1093
1651
|
{ $sort: { value: -1 } },
|
|
1094
1652
|
{ $limit: limit }
|
|
@@ -1099,7 +1657,7 @@ var MongoDBAdapter = class {
|
|
|
1099
1657
|
}
|
|
1100
1658
|
case "top_referrers": {
|
|
1101
1659
|
const rows = await this.collection.aggregate([
|
|
1102
|
-
{ $match: { ...baseMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
|
|
1660
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
|
|
1103
1661
|
{ $group: { _id: "$referrer", value: { $sum: 1 } } },
|
|
1104
1662
|
{ $sort: { value: -1 } },
|
|
1105
1663
|
{ $limit: limit }
|
|
@@ -1110,7 +1668,7 @@ var MongoDBAdapter = class {
|
|
|
1110
1668
|
}
|
|
1111
1669
|
case "top_countries": {
|
|
1112
1670
|
const rows = await this.collection.aggregate([
|
|
1113
|
-
{ $match: { ...baseMatch, country: { $ne: null } } },
|
|
1671
|
+
{ $match: { ...baseMatch, ...filterMatch, country: { $ne: null } } },
|
|
1114
1672
|
{ $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
|
|
1115
1673
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1116
1674
|
{ $sort: { value: -1 } },
|
|
@@ -1122,7 +1680,7 @@ var MongoDBAdapter = class {
|
|
|
1122
1680
|
}
|
|
1123
1681
|
case "top_cities": {
|
|
1124
1682
|
const rows = await this.collection.aggregate([
|
|
1125
|
-
{ $match: { ...baseMatch, city: { $ne: null } } },
|
|
1683
|
+
{ $match: { ...baseMatch, ...filterMatch, city: { $ne: null } } },
|
|
1126
1684
|
{ $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
|
|
1127
1685
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1128
1686
|
{ $sort: { value: -1 } },
|
|
@@ -1134,7 +1692,24 @@ var MongoDBAdapter = class {
|
|
|
1134
1692
|
}
|
|
1135
1693
|
case "top_events": {
|
|
1136
1694
|
const rows = await this.collection.aggregate([
|
|
1137
|
-
{ $match: { ...baseMatch, type: "event", event_name: { $ne: null } } },
|
|
1695
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $ne: null } } },
|
|
1696
|
+
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1697
|
+
{ $sort: { value: -1 } },
|
|
1698
|
+
{ $limit: limit }
|
|
1699
|
+
]).toArray();
|
|
1700
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1701
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1702
|
+
break;
|
|
1703
|
+
}
|
|
1704
|
+
case "top_conversions": {
|
|
1705
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
1706
|
+
if (conversionEvents.length === 0) {
|
|
1707
|
+
total = 0;
|
|
1708
|
+
data = [];
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
const rows = await this.collection.aggregate([
|
|
1712
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
|
|
1138
1713
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1139
1714
|
{ $sort: { value: -1 } },
|
|
1140
1715
|
{ $limit: limit }
|
|
@@ -1143,9 +1718,92 @@ var MongoDBAdapter = class {
|
|
|
1143
1718
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1144
1719
|
break;
|
|
1145
1720
|
}
|
|
1721
|
+
case "top_exit_pages": {
|
|
1722
|
+
const rows = await this.collection.aggregate([
|
|
1723
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1724
|
+
{ $sort: { timestamp: 1 } },
|
|
1725
|
+
{ $group: { _id: "$session_id", url: { $last: "$url" } } },
|
|
1726
|
+
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1727
|
+
{ $sort: { value: -1 } },
|
|
1728
|
+
{ $limit: limit }
|
|
1729
|
+
]).toArray();
|
|
1730
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1731
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
case "top_transitions": {
|
|
1735
|
+
const rows = await this.collection.aggregate([
|
|
1736
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1737
|
+
{
|
|
1738
|
+
$setWindowFields: {
|
|
1739
|
+
partitionBy: "$session_id",
|
|
1740
|
+
sortBy: { timestamp: 1 },
|
|
1741
|
+
output: {
|
|
1742
|
+
prev_url: { $shift: { output: "$url", by: -1 } }
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
},
|
|
1746
|
+
{ $match: { prev_url: { $ne: null } } },
|
|
1747
|
+
{ $group: { _id: { $concat: ["$prev_url", " \u2192 ", "$url"] }, value: { $sum: 1 } } },
|
|
1748
|
+
{ $sort: { value: -1 } },
|
|
1749
|
+
{ $limit: limit }
|
|
1750
|
+
]).toArray();
|
|
1751
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1752
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1753
|
+
break;
|
|
1754
|
+
}
|
|
1755
|
+
case "top_scroll_pages": {
|
|
1756
|
+
const rows = await this.collection.aggregate([
|
|
1757
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } } },
|
|
1758
|
+
{ $group: { _id: "$page_path", value: { $sum: 1 } } },
|
|
1759
|
+
{ $sort: { value: -1 } },
|
|
1760
|
+
{ $limit: limit }
|
|
1761
|
+
]).toArray();
|
|
1762
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1763
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1764
|
+
break;
|
|
1765
|
+
}
|
|
1766
|
+
case "top_button_clicks": {
|
|
1767
|
+
const rows = await this.collection.aggregate([
|
|
1768
|
+
{
|
|
1769
|
+
$match: {
|
|
1770
|
+
...baseMatch,
|
|
1771
|
+
...filterMatch,
|
|
1772
|
+
type: "event",
|
|
1773
|
+
event_subtype: "button_click",
|
|
1774
|
+
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
1775
|
+
}
|
|
1776
|
+
},
|
|
1777
|
+
{ $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
|
|
1778
|
+
{ $sort: { value: -1 } },
|
|
1779
|
+
{ $limit: limit }
|
|
1780
|
+
]).toArray();
|
|
1781
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1782
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
case "top_link_targets": {
|
|
1786
|
+
const rows = await this.collection.aggregate([
|
|
1787
|
+
{
|
|
1788
|
+
$match: {
|
|
1789
|
+
...baseMatch,
|
|
1790
|
+
...filterMatch,
|
|
1791
|
+
type: "event",
|
|
1792
|
+
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
1793
|
+
target_url_path: { $ne: null }
|
|
1794
|
+
}
|
|
1795
|
+
},
|
|
1796
|
+
{ $group: { _id: "$target_url_path", value: { $sum: 1 } } },
|
|
1797
|
+
{ $sort: { value: -1 } },
|
|
1798
|
+
{ $limit: limit }
|
|
1799
|
+
]).toArray();
|
|
1800
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1801
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1802
|
+
break;
|
|
1803
|
+
}
|
|
1146
1804
|
case "top_devices": {
|
|
1147
1805
|
const rows = await this.collection.aggregate([
|
|
1148
|
-
{ $match: { ...baseMatch, device_type: { $ne: null } } },
|
|
1806
|
+
{ $match: { ...baseMatch, ...filterMatch, device_type: { $ne: null } } },
|
|
1149
1807
|
{ $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
|
|
1150
1808
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1151
1809
|
{ $sort: { value: -1 } },
|
|
@@ -1157,7 +1815,7 @@ var MongoDBAdapter = class {
|
|
|
1157
1815
|
}
|
|
1158
1816
|
case "top_browsers": {
|
|
1159
1817
|
const rows = await this.collection.aggregate([
|
|
1160
|
-
{ $match: { ...baseMatch, browser: { $ne: null } } },
|
|
1818
|
+
{ $match: { ...baseMatch, ...filterMatch, browser: { $ne: null } } },
|
|
1161
1819
|
{ $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
|
|
1162
1820
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1163
1821
|
{ $sort: { value: -1 } },
|
|
@@ -1169,7 +1827,7 @@ var MongoDBAdapter = class {
|
|
|
1169
1827
|
}
|
|
1170
1828
|
case "top_os": {
|
|
1171
1829
|
const rows = await this.collection.aggregate([
|
|
1172
|
-
{ $match: { ...baseMatch, os: { $ne: null } } },
|
|
1830
|
+
{ $match: { ...baseMatch, ...filterMatch, os: { $ne: null } } },
|
|
1173
1831
|
{ $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
|
|
1174
1832
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1175
1833
|
{ $sort: { value: -1 } },
|
|
@@ -1179,9 +1837,45 @@ var MongoDBAdapter = class {
|
|
|
1179
1837
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1180
1838
|
break;
|
|
1181
1839
|
}
|
|
1840
|
+
case "top_utm_sources": {
|
|
1841
|
+
const rows = await this.collection.aggregate([
|
|
1842
|
+
{ $match: { ...baseMatch, ...filterMatch, utm_source: { $nin: [null, ""] } } },
|
|
1843
|
+
{ $group: { _id: "$utm_source", value: { $addToSet: "$visitor_id" } } },
|
|
1844
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1845
|
+
{ $sort: { value: -1 } },
|
|
1846
|
+
{ $limit: limit }
|
|
1847
|
+
]).toArray();
|
|
1848
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1849
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1850
|
+
break;
|
|
1851
|
+
}
|
|
1852
|
+
case "top_utm_mediums": {
|
|
1853
|
+
const rows = await this.collection.aggregate([
|
|
1854
|
+
{ $match: { ...baseMatch, ...filterMatch, utm_medium: { $nin: [null, ""] } } },
|
|
1855
|
+
{ $group: { _id: "$utm_medium", value: { $addToSet: "$visitor_id" } } },
|
|
1856
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1857
|
+
{ $sort: { value: -1 } },
|
|
1858
|
+
{ $limit: limit }
|
|
1859
|
+
]).toArray();
|
|
1860
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1861
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1862
|
+
break;
|
|
1863
|
+
}
|
|
1864
|
+
case "top_utm_campaigns": {
|
|
1865
|
+
const rows = await this.collection.aggregate([
|
|
1866
|
+
{ $match: { ...baseMatch, ...filterMatch, utm_campaign: { $nin: [null, ""] } } },
|
|
1867
|
+
{ $group: { _id: "$utm_campaign", value: { $addToSet: "$visitor_id" } } },
|
|
1868
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1869
|
+
{ $sort: { value: -1 } },
|
|
1870
|
+
{ $limit: limit }
|
|
1871
|
+
]).toArray();
|
|
1872
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1873
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1874
|
+
break;
|
|
1875
|
+
}
|
|
1182
1876
|
}
|
|
1183
1877
|
const result = { metric: q.metric, period, data, total };
|
|
1184
|
-
if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
|
|
1878
|
+
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
1185
1879
|
const prevRange = previousPeriodRange(dateRange);
|
|
1186
1880
|
const prevResult = await this.query({
|
|
1187
1881
|
...q,
|
|
@@ -1213,15 +1907,34 @@ var MongoDBAdapter = class {
|
|
|
1213
1907
|
site_id: params.siteId,
|
|
1214
1908
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1215
1909
|
};
|
|
1910
|
+
const filterMatch = buildFilterMatch(params.filters);
|
|
1216
1911
|
if (params.metric === "pageviews") {
|
|
1217
1912
|
baseMatch.type = "pageview";
|
|
1218
1913
|
}
|
|
1914
|
+
if (params.metric === "events") {
|
|
1915
|
+
baseMatch.type = "event";
|
|
1916
|
+
}
|
|
1917
|
+
if (params.metric === "conversions") {
|
|
1918
|
+
baseMatch.type = "event";
|
|
1919
|
+
const conversionEvents = params.conversionEvents ?? [];
|
|
1920
|
+
if (conversionEvents.length === 0) {
|
|
1921
|
+
const data2 = fillBuckets(
|
|
1922
|
+
new Date(dateRange.from),
|
|
1923
|
+
new Date(dateRange.to),
|
|
1924
|
+
granularity,
|
|
1925
|
+
granularityToDateFormat(granularity),
|
|
1926
|
+
[]
|
|
1927
|
+
);
|
|
1928
|
+
return { metric: params.metric, granularity, data: data2 };
|
|
1929
|
+
}
|
|
1930
|
+
baseMatch.event_name = { $in: conversionEvents };
|
|
1931
|
+
}
|
|
1219
1932
|
const dateFormat = granularityToDateFormat(granularity);
|
|
1220
1933
|
let pipeline;
|
|
1221
1934
|
if (params.metric === "visitors" || params.metric === "sessions") {
|
|
1222
1935
|
const groupField = params.metric === "visitors" ? "$visitor_id" : "$session_id";
|
|
1223
1936
|
pipeline = [
|
|
1224
|
-
{ $match: baseMatch },
|
|
1937
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1225
1938
|
{
|
|
1226
1939
|
$group: {
|
|
1227
1940
|
_id: {
|
|
@@ -1240,7 +1953,7 @@ var MongoDBAdapter = class {
|
|
|
1240
1953
|
];
|
|
1241
1954
|
} else {
|
|
1242
1955
|
pipeline = [
|
|
1243
|
-
{ $match: baseMatch },
|
|
1956
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1244
1957
|
{
|
|
1245
1958
|
$group: {
|
|
1246
1959
|
_id: { $dateToString: { format: dateFormat, date: "$timestamp" } },
|
|
@@ -1321,7 +2034,12 @@ var MongoDBAdapter = class {
|
|
|
1321
2034
|
const offset = params.offset ?? 0;
|
|
1322
2035
|
const match = { site_id: params.siteId };
|
|
1323
2036
|
if (params.type) match.type = params.type;
|
|
1324
|
-
if (params.eventName)
|
|
2037
|
+
if (params.eventName) {
|
|
2038
|
+
match.event_name = params.eventName;
|
|
2039
|
+
} else if (params.eventNames && params.eventNames.length > 0) {
|
|
2040
|
+
match.event_name = { $in: params.eventNames };
|
|
2041
|
+
}
|
|
2042
|
+
if (params.eventSource) match.event_source = params.eventSource;
|
|
1325
2043
|
if (params.visitorId) match.visitor_id = params.visitorId;
|
|
1326
2044
|
if (params.userId) match.user_id = params.userId;
|
|
1327
2045
|
if (params.period || params.dateFrom) {
|
|
@@ -1343,23 +2061,72 @@ var MongoDBAdapter = class {
|
|
|
1343
2061
|
offset
|
|
1344
2062
|
};
|
|
1345
2063
|
}
|
|
2064
|
+
// ─── Identity Mapping ──────────────────────────────────────
|
|
2065
|
+
async upsertIdentity(siteId, visitorId, userId) {
|
|
2066
|
+
await this.identityMap.updateOne(
|
|
2067
|
+
{ site_id: siteId, visitor_id: visitorId },
|
|
2068
|
+
{
|
|
2069
|
+
$set: { user_id: userId, identified_at: /* @__PURE__ */ new Date() },
|
|
2070
|
+
$setOnInsert: { site_id: siteId, visitor_id: visitorId, created_at: /* @__PURE__ */ new Date() }
|
|
2071
|
+
},
|
|
2072
|
+
{ upsert: true }
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
async getVisitorIdsForUser(siteId, userId) {
|
|
2076
|
+
const docs = await this.identityMap.find({ site_id: siteId, user_id: userId }).toArray();
|
|
2077
|
+
return docs.map((d) => d.visitor_id);
|
|
2078
|
+
}
|
|
2079
|
+
async getUserIdForVisitor(siteId, visitorId) {
|
|
2080
|
+
const doc = await this.identityMap.findOne({ site_id: siteId, visitor_id: visitorId });
|
|
2081
|
+
return doc?.user_id ?? null;
|
|
2082
|
+
}
|
|
1346
2083
|
// ─── User Listing ──────────────────────────────────────
|
|
1347
2084
|
async listUsers(params) {
|
|
1348
2085
|
const limit = Math.min(params.limit ?? 50, 200);
|
|
1349
2086
|
const offset = params.offset ?? 0;
|
|
1350
2087
|
const match = { site_id: params.siteId };
|
|
1351
|
-
if (params.search) {
|
|
1352
|
-
match.$or = [
|
|
1353
|
-
{ visitor_id: { $regex: params.search, $options: "i" } },
|
|
1354
|
-
{ user_id: { $regex: params.search, $options: "i" } }
|
|
1355
|
-
];
|
|
1356
|
-
}
|
|
1357
2088
|
const pipeline = [
|
|
1358
2089
|
{ $match: match },
|
|
2090
|
+
// Join with identity map to resolve visitor → user
|
|
2091
|
+
{
|
|
2092
|
+
$lookup: {
|
|
2093
|
+
from: IDENTITY_MAP_COLLECTION,
|
|
2094
|
+
let: { vid: "$visitor_id", sid: "$site_id" },
|
|
2095
|
+
pipeline: [
|
|
2096
|
+
{ $match: { $expr: { $and: [{ $eq: ["$visitor_id", "$$vid"] }, { $eq: ["$site_id", "$$sid"] }] } } }
|
|
2097
|
+
],
|
|
2098
|
+
as: "_identity"
|
|
2099
|
+
}
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
$addFields: {
|
|
2103
|
+
_resolved_id: {
|
|
2104
|
+
$ifNull: [{ $arrayElemAt: ["$_identity.user_id", 0] }, "$visitor_id"]
|
|
2105
|
+
},
|
|
2106
|
+
_resolved_user_id: {
|
|
2107
|
+
$arrayElemAt: ["$_identity.user_id", 0]
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
];
|
|
2112
|
+
if (params.search) {
|
|
2113
|
+
pipeline.push({
|
|
2114
|
+
$match: {
|
|
2115
|
+
$or: [
|
|
2116
|
+
{ visitor_id: { $regex: params.search, $options: "i" } },
|
|
2117
|
+
{ user_id: { $regex: params.search, $options: "i" } },
|
|
2118
|
+
{ _resolved_user_id: { $regex: params.search, $options: "i" } }
|
|
2119
|
+
]
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
pipeline.push(
|
|
2124
|
+
{ $sort: { timestamp: 1 } },
|
|
1359
2125
|
{
|
|
1360
2126
|
$group: {
|
|
1361
|
-
_id: "$
|
|
1362
|
-
|
|
2127
|
+
_id: "$_resolved_id",
|
|
2128
|
+
visitorIds: { $addToSet: "$visitor_id" },
|
|
2129
|
+
userId: { $last: { $ifNull: ["$_resolved_user_id", "$user_id"] } },
|
|
1363
2130
|
traits: { $last: "$traits" },
|
|
1364
2131
|
firstSeen: { $min: "$timestamp" },
|
|
1365
2132
|
lastSeen: { $max: "$timestamp" },
|
|
@@ -1367,13 +2134,22 @@ var MongoDBAdapter = class {
|
|
|
1367
2134
|
totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
|
|
1368
2135
|
sessions: { $addToSet: "$session_id" },
|
|
1369
2136
|
lastUrl: { $last: "$url" },
|
|
2137
|
+
referrer: { $last: "$referrer" },
|
|
1370
2138
|
device_type: { $last: "$device_type" },
|
|
1371
2139
|
browser: { $last: "$browser" },
|
|
1372
2140
|
os: { $last: "$os" },
|
|
1373
2141
|
country: { $last: "$country" },
|
|
1374
2142
|
city: { $last: "$city" },
|
|
1375
2143
|
region: { $last: "$region" },
|
|
1376
|
-
language: { $last: "$language" }
|
|
2144
|
+
language: { $last: "$language" },
|
|
2145
|
+
timezone: { $last: "$timezone" },
|
|
2146
|
+
screen_width: { $last: "$screen_width" },
|
|
2147
|
+
screen_height: { $last: "$screen_height" },
|
|
2148
|
+
utm_source: { $last: "$utm_source" },
|
|
2149
|
+
utm_medium: { $last: "$utm_medium" },
|
|
2150
|
+
utm_campaign: { $last: "$utm_campaign" },
|
|
2151
|
+
utm_term: { $last: "$utm_term" },
|
|
2152
|
+
utm_content: { $last: "$utm_content" }
|
|
1377
2153
|
}
|
|
1378
2154
|
},
|
|
1379
2155
|
{ $sort: { lastSeen: -1 } },
|
|
@@ -1383,10 +2159,11 @@ var MongoDBAdapter = class {
|
|
|
1383
2159
|
count: [{ $count: "total" }]
|
|
1384
2160
|
}
|
|
1385
2161
|
}
|
|
1386
|
-
|
|
2162
|
+
);
|
|
1387
2163
|
const [result] = await this.collection.aggregate(pipeline).toArray();
|
|
1388
2164
|
const users = (result?.data ?? []).map((u) => ({
|
|
1389
|
-
visitorId: u._id,
|
|
2165
|
+
visitorId: u.visitorIds[0] ?? u._id,
|
|
2166
|
+
visitorIds: u.visitorIds.length > 1 ? u.visitorIds : void 0,
|
|
1390
2167
|
userId: u.userId ?? void 0,
|
|
1391
2168
|
traits: u.traits ?? void 0,
|
|
1392
2169
|
firstSeen: u.firstSeen.toISOString(),
|
|
@@ -1395,9 +2172,19 @@ var MongoDBAdapter = class {
|
|
|
1395
2172
|
totalPageviews: u.totalPageviews,
|
|
1396
2173
|
totalSessions: u.sessions.length,
|
|
1397
2174
|
lastUrl: u.lastUrl ?? void 0,
|
|
2175
|
+
referrer: u.referrer ?? void 0,
|
|
1398
2176
|
device: u.device_type ? { type: u.device_type, browser: u.browser ?? "", os: u.os ?? "" } : void 0,
|
|
1399
2177
|
geo: u.country ? { country: u.country, city: u.city ?? void 0, region: u.region ?? void 0 } : void 0,
|
|
1400
|
-
language: u.language ?? void 0
|
|
2178
|
+
language: u.language ?? void 0,
|
|
2179
|
+
timezone: u.timezone ?? void 0,
|
|
2180
|
+
screen: u.screen_width || u.screen_height ? { width: u.screen_width ?? 0, height: u.screen_height ?? 0 } : void 0,
|
|
2181
|
+
utm: u.utm_source ? {
|
|
2182
|
+
source: u.utm_source ?? void 0,
|
|
2183
|
+
medium: u.utm_medium ?? void 0,
|
|
2184
|
+
campaign: u.utm_campaign ?? void 0,
|
|
2185
|
+
term: u.utm_term ?? void 0,
|
|
2186
|
+
content: u.utm_content ?? void 0
|
|
2187
|
+
} : void 0
|
|
1401
2188
|
}));
|
|
1402
2189
|
return {
|
|
1403
2190
|
users,
|
|
@@ -1406,13 +2193,125 @@ var MongoDBAdapter = class {
|
|
|
1406
2193
|
offset
|
|
1407
2194
|
};
|
|
1408
2195
|
}
|
|
1409
|
-
async getUserDetail(siteId,
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
2196
|
+
async getUserDetail(siteId, identifier) {
|
|
2197
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
2198
|
+
if (visitorIds.length > 0) {
|
|
2199
|
+
return this.getMergedUserDetail(siteId, identifier, visitorIds);
|
|
2200
|
+
}
|
|
2201
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
2202
|
+
if (userId) {
|
|
2203
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
2204
|
+
return this.getMergedUserDetail(siteId, userId, allVisitorIds);
|
|
2205
|
+
}
|
|
2206
|
+
return this.getMergedUserDetail(siteId, void 0, [identifier]);
|
|
2207
|
+
}
|
|
2208
|
+
async getUserEvents(siteId, identifier, params) {
|
|
2209
|
+
let visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
2210
|
+
if (visitorIds.length === 0) {
|
|
2211
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
2212
|
+
if (userId) {
|
|
2213
|
+
visitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (visitorIds.length === 0) {
|
|
2217
|
+
visitorIds = [identifier];
|
|
2218
|
+
}
|
|
2219
|
+
return this.listEventsForVisitorIds(siteId, visitorIds, params);
|
|
1413
2220
|
}
|
|
1414
|
-
async
|
|
1415
|
-
|
|
2221
|
+
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
2222
|
+
const pipeline = [
|
|
2223
|
+
{ $match: { site_id: siteId, visitor_id: { $in: visitorIds } } },
|
|
2224
|
+
{ $sort: { timestamp: 1 } },
|
|
2225
|
+
{
|
|
2226
|
+
$group: {
|
|
2227
|
+
_id: null,
|
|
2228
|
+
visitorIds: { $addToSet: "$visitor_id" },
|
|
2229
|
+
traits: { $last: "$traits" },
|
|
2230
|
+
firstSeen: { $min: "$timestamp" },
|
|
2231
|
+
lastSeen: { $max: "$timestamp" },
|
|
2232
|
+
totalEvents: { $sum: 1 },
|
|
2233
|
+
totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
|
|
2234
|
+
sessions: { $addToSet: "$session_id" },
|
|
2235
|
+
lastUrl: { $last: "$url" },
|
|
2236
|
+
referrer: { $last: "$referrer" },
|
|
2237
|
+
device_type: { $last: "$device_type" },
|
|
2238
|
+
browser: { $last: "$browser" },
|
|
2239
|
+
os: { $last: "$os" },
|
|
2240
|
+
country: { $last: "$country" },
|
|
2241
|
+
city: { $last: "$city" },
|
|
2242
|
+
region: { $last: "$region" },
|
|
2243
|
+
language: { $last: "$language" },
|
|
2244
|
+
timezone: { $last: "$timezone" },
|
|
2245
|
+
screen_width: { $last: "$screen_width" },
|
|
2246
|
+
screen_height: { $last: "$screen_height" },
|
|
2247
|
+
utm_source: { $last: "$utm_source" },
|
|
2248
|
+
utm_medium: { $last: "$utm_medium" },
|
|
2249
|
+
utm_campaign: { $last: "$utm_campaign" },
|
|
2250
|
+
utm_term: { $last: "$utm_term" },
|
|
2251
|
+
utm_content: { $last: "$utm_content" }
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
];
|
|
2255
|
+
const [row] = await this.collection.aggregate(pipeline).toArray();
|
|
2256
|
+
if (!row) return null;
|
|
2257
|
+
return {
|
|
2258
|
+
visitorId: visitorIds[0],
|
|
2259
|
+
visitorIds: row.visitorIds.length > 1 ? row.visitorIds : void 0,
|
|
2260
|
+
userId: userId ?? void 0,
|
|
2261
|
+
traits: row.traits ?? void 0,
|
|
2262
|
+
firstSeen: row.firstSeen.toISOString(),
|
|
2263
|
+
lastSeen: row.lastSeen.toISOString(),
|
|
2264
|
+
totalEvents: row.totalEvents,
|
|
2265
|
+
totalPageviews: row.totalPageviews,
|
|
2266
|
+
totalSessions: row.sessions.length,
|
|
2267
|
+
lastUrl: row.lastUrl ?? void 0,
|
|
2268
|
+
referrer: row.referrer ?? void 0,
|
|
2269
|
+
device: row.device_type ? { type: row.device_type, browser: row.browser ?? "", os: row.os ?? "" } : void 0,
|
|
2270
|
+
geo: row.country ? { country: row.country, city: row.city ?? void 0, region: row.region ?? void 0 } : void 0,
|
|
2271
|
+
language: row.language ?? void 0,
|
|
2272
|
+
timezone: row.timezone ?? void 0,
|
|
2273
|
+
screen: row.screen_width || row.screen_height ? { width: row.screen_width ?? 0, height: row.screen_height ?? 0 } : void 0,
|
|
2274
|
+
utm: row.utm_source ? {
|
|
2275
|
+
source: row.utm_source ?? void 0,
|
|
2276
|
+
medium: row.utm_medium ?? void 0,
|
|
2277
|
+
campaign: row.utm_campaign ?? void 0,
|
|
2278
|
+
term: row.utm_term ?? void 0,
|
|
2279
|
+
content: row.utm_content ?? void 0
|
|
2280
|
+
} : void 0
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
async listEventsForVisitorIds(siteId, visitorIds, params) {
|
|
2284
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
2285
|
+
const offset = params.offset ?? 0;
|
|
2286
|
+
const match = {
|
|
2287
|
+
site_id: siteId,
|
|
2288
|
+
visitor_id: { $in: visitorIds }
|
|
2289
|
+
};
|
|
2290
|
+
if (params.type) match.type = params.type;
|
|
2291
|
+
if (params.eventName) {
|
|
2292
|
+
match.event_name = params.eventName;
|
|
2293
|
+
} else if (params.eventNames && params.eventNames.length > 0) {
|
|
2294
|
+
match.event_name = { $in: params.eventNames };
|
|
2295
|
+
}
|
|
2296
|
+
if (params.eventSource) match.event_source = params.eventSource;
|
|
2297
|
+
if (params.period || params.dateFrom) {
|
|
2298
|
+
const { dateRange } = resolvePeriod({
|
|
2299
|
+
period: params.period,
|
|
2300
|
+
dateFrom: params.dateFrom,
|
|
2301
|
+
dateTo: params.dateTo
|
|
2302
|
+
});
|
|
2303
|
+
match.timestamp = { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) };
|
|
2304
|
+
}
|
|
2305
|
+
const [events, countResult] = await Promise.all([
|
|
2306
|
+
this.collection.find(match).sort({ timestamp: -1 }).skip(offset).limit(limit).toArray(),
|
|
2307
|
+
this.collection.countDocuments(match)
|
|
2308
|
+
]);
|
|
2309
|
+
return {
|
|
2310
|
+
events: events.map((e) => this.toEventListItem(e)),
|
|
2311
|
+
total: countResult,
|
|
2312
|
+
limit,
|
|
2313
|
+
offset
|
|
2314
|
+
};
|
|
1416
2315
|
}
|
|
1417
2316
|
toEventListItem(doc) {
|
|
1418
2317
|
return {
|
|
@@ -1426,6 +2325,13 @@ var MongoDBAdapter = class {
|
|
|
1426
2325
|
title: doc.title ?? void 0,
|
|
1427
2326
|
name: doc.event_name ?? void 0,
|
|
1428
2327
|
properties: doc.properties ?? void 0,
|
|
2328
|
+
eventSource: doc.event_source ? doc.event_source : void 0,
|
|
2329
|
+
eventSubtype: doc.event_subtype ? doc.event_subtype : void 0,
|
|
2330
|
+
pagePath: doc.page_path ?? void 0,
|
|
2331
|
+
targetUrlPath: doc.target_url_path ?? void 0,
|
|
2332
|
+
elementSelector: doc.element_selector ?? void 0,
|
|
2333
|
+
elementText: doc.element_text ?? void 0,
|
|
2334
|
+
scrollDepthPct: doc.scroll_depth_pct ?? void 0,
|
|
1429
2335
|
userId: doc.user_id ?? void 0,
|
|
1430
2336
|
traits: doc.traits ?? void 0,
|
|
1431
2337
|
geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
|
|
@@ -1449,6 +2355,7 @@ var MongoDBAdapter = class {
|
|
|
1449
2355
|
name: data.name,
|
|
1450
2356
|
domain: data.domain ?? null,
|
|
1451
2357
|
allowed_origins: data.allowedOrigins ?? null,
|
|
2358
|
+
conversion_events: data.conversionEvents ?? null,
|
|
1452
2359
|
created_at: now,
|
|
1453
2360
|
updated_at: now
|
|
1454
2361
|
};
|
|
@@ -1472,6 +2379,7 @@ var MongoDBAdapter = class {
|
|
|
1472
2379
|
if (data.name !== void 0) updates.name = data.name;
|
|
1473
2380
|
if (data.domain !== void 0) updates.domain = data.domain || null;
|
|
1474
2381
|
if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
|
|
2382
|
+
if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
|
|
1475
2383
|
const result = await this.sites.findOneAndUpdate(
|
|
1476
2384
|
{ site_id: siteId },
|
|
1477
2385
|
{ $set: updates },
|
|
@@ -1502,6 +2410,7 @@ var MongoDBAdapter = class {
|
|
|
1502
2410
|
name: doc.name,
|
|
1503
2411
|
domain: doc.domain ?? void 0,
|
|
1504
2412
|
allowedOrigins: doc.allowed_origins ?? void 0,
|
|
2413
|
+
conversionEvents: doc.conversion_events ?? void 0,
|
|
1505
2414
|
createdAt: doc.created_at.toISOString(),
|
|
1506
2415
|
updatedAt: doc.updated_at.toISOString()
|
|
1507
2416
|
};
|
|
@@ -1755,6 +2664,7 @@ async function createCollector(config) {
|
|
|
1755
2664
|
if (allowed) {
|
|
1756
2665
|
res.setHeader?.("Access-Control-Allow-Origin", origin || "*");
|
|
1757
2666
|
res.setHeader?.("Access-Control-Allow-Methods", methods);
|
|
2667
|
+
res.setHeader?.("Access-Control-Allow-Credentials", "true");
|
|
1758
2668
|
const headers = ["Content-Type", extraHeaders].filter(Boolean).join(", ");
|
|
1759
2669
|
res.setHeader?.("Access-Control-Allow-Headers", headers);
|
|
1760
2670
|
}
|
|
@@ -1772,6 +2682,51 @@ async function createCollector(config) {
|
|
|
1772
2682
|
return { ...event, ip, geo, device };
|
|
1773
2683
|
});
|
|
1774
2684
|
}
|
|
2685
|
+
const identityCache = /* @__PURE__ */ new Map();
|
|
2686
|
+
const IDENTITY_CACHE_TTL = 5 * 60 * 1e3;
|
|
2687
|
+
function getCachedUserId(siteId, visitorId) {
|
|
2688
|
+
const key = `${siteId}:${visitorId}`;
|
|
2689
|
+
const entry = identityCache.get(key);
|
|
2690
|
+
if (!entry) return void 0;
|
|
2691
|
+
if (Date.now() > entry.expires) {
|
|
2692
|
+
identityCache.delete(key);
|
|
2693
|
+
return void 0;
|
|
2694
|
+
}
|
|
2695
|
+
return entry.userId;
|
|
2696
|
+
}
|
|
2697
|
+
function setCachedUserId(siteId, visitorId, userId) {
|
|
2698
|
+
const key = `${siteId}:${visitorId}`;
|
|
2699
|
+
identityCache.set(key, { userId, expires: Date.now() + IDENTITY_CACHE_TTL });
|
|
2700
|
+
if (identityCache.size > 1e4) {
|
|
2701
|
+
const now = Date.now();
|
|
2702
|
+
for (const [k, v] of identityCache) {
|
|
2703
|
+
if (now > v.expires) identityCache.delete(k);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
async function processIdentity(events) {
|
|
2708
|
+
for (const event of events) {
|
|
2709
|
+
if (!event.visitorId || event.visitorId === "server") continue;
|
|
2710
|
+
if (event.type === "identify" && event.userId) {
|
|
2711
|
+
await db.upsertIdentity(event.siteId, event.visitorId, event.userId);
|
|
2712
|
+
setCachedUserId(event.siteId, event.visitorId, event.userId);
|
|
2713
|
+
} else if (!event.userId) {
|
|
2714
|
+
const cached = getCachedUserId(event.siteId, event.visitorId);
|
|
2715
|
+
if (cached) {
|
|
2716
|
+
event.userId = cached;
|
|
2717
|
+
} else {
|
|
2718
|
+
const resolved = await db.getUserIdForVisitor(event.siteId, event.visitorId);
|
|
2719
|
+
if (resolved) {
|
|
2720
|
+
event.userId = resolved;
|
|
2721
|
+
setCachedUserId(event.siteId, event.visitorId, resolved);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
} else if (event.userId) {
|
|
2725
|
+
setCachedUserId(event.siteId, event.visitorId, event.userId);
|
|
2726
|
+
await db.upsertIdentity(event.siteId, event.visitorId, event.userId);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
1775
2730
|
function extractIp(req) {
|
|
1776
2731
|
if (config.trustProxy ?? true) {
|
|
1777
2732
|
const forwarded = req.headers?.["x-forwarded-for"];
|
|
@@ -1785,7 +2740,14 @@ async function createCollector(config) {
|
|
|
1785
2740
|
}
|
|
1786
2741
|
function handler() {
|
|
1787
2742
|
return async (req, res) => {
|
|
1788
|
-
|
|
2743
|
+
res.setHeader?.("Access-Control-Allow-Origin", "*");
|
|
2744
|
+
res.setHeader?.("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
2745
|
+
res.setHeader?.("Access-Control-Allow-Headers", "Content-Type");
|
|
2746
|
+
if (req.method === "OPTIONS") {
|
|
2747
|
+
res.writeHead?.(204);
|
|
2748
|
+
res.end?.();
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
1789
2751
|
if (req.method !== "POST") {
|
|
1790
2752
|
sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
1791
2753
|
return;
|
|
@@ -1826,11 +2788,13 @@ async function createCollector(config) {
|
|
|
1826
2788
|
sendJson(res, 200, { ok: true });
|
|
1827
2789
|
return;
|
|
1828
2790
|
}
|
|
2791
|
+
await processIdentity(filtered);
|
|
1829
2792
|
await db.insertEvents(filtered);
|
|
1830
2793
|
sendJson(res, 200, { ok: true });
|
|
1831
2794
|
return;
|
|
1832
2795
|
}
|
|
1833
2796
|
}
|
|
2797
|
+
await processIdentity(enriched);
|
|
1834
2798
|
await db.insertEvents(enriched);
|
|
1835
2799
|
sendJson(res, 200, { ok: true });
|
|
1836
2800
|
} catch (err) {
|
|
@@ -1864,8 +2828,13 @@ async function createCollector(config) {
|
|
|
1864
2828
|
period: params.period,
|
|
1865
2829
|
dateFrom: params.dateFrom,
|
|
1866
2830
|
dateTo: params.dateTo,
|
|
1867
|
-
granularity: q.granularity
|
|
2831
|
+
granularity: q.granularity,
|
|
2832
|
+
filters: q.filters ? JSON.parse(q.filters) : void 0
|
|
1868
2833
|
};
|
|
2834
|
+
if (tsParams.metric === "conversions") {
|
|
2835
|
+
const site = await db.getSite(params.siteId);
|
|
2836
|
+
tsParams.conversionEvents = site?.conversionEvents ?? [];
|
|
2837
|
+
}
|
|
1869
2838
|
const result2 = await db.queryTimeSeries(tsParams);
|
|
1870
2839
|
sendJson(res, 200, result2);
|
|
1871
2840
|
return;
|
|
@@ -1881,7 +2850,15 @@ async function createCollector(config) {
|
|
|
1881
2850
|
sendJson(res, 200, result2);
|
|
1882
2851
|
return;
|
|
1883
2852
|
}
|
|
1884
|
-
const
|
|
2853
|
+
const isConversionMetric = params.metric === "conversions" || params.metric === "top_conversions";
|
|
2854
|
+
let result;
|
|
2855
|
+
if (isConversionMetric) {
|
|
2856
|
+
const site = await db.getSite(params.siteId);
|
|
2857
|
+
const conversionEvents = site?.conversionEvents ?? [];
|
|
2858
|
+
result = await db.query({ ...params, conversionEvents });
|
|
2859
|
+
} else {
|
|
2860
|
+
result = await db.query(params);
|
|
2861
|
+
}
|
|
1885
2862
|
sendJson(res, 200, result);
|
|
1886
2863
|
} catch (err) {
|
|
1887
2864
|
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : "Internal error" });
|
|
@@ -1978,10 +2955,13 @@ async function createCollector(config) {
|
|
|
1978
2955
|
sendJson(res, 401, { ok: false, error: "Invalid or missing secret key" });
|
|
1979
2956
|
return;
|
|
1980
2957
|
}
|
|
2958
|
+
const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1981
2959
|
const params = {
|
|
1982
2960
|
siteId: q.siteId,
|
|
1983
2961
|
type: q.type,
|
|
1984
2962
|
eventName: q.eventName,
|
|
2963
|
+
eventNames,
|
|
2964
|
+
eventSource: q.eventSource,
|
|
1985
2965
|
visitorId: q.visitorId,
|
|
1986
2966
|
userId: q.userId,
|
|
1987
2967
|
period: q.period,
|
|
@@ -2021,9 +3001,13 @@ async function createCollector(config) {
|
|
|
2021
3001
|
const visitorId = usersIdx >= 0 ? pathSegments[usersIdx + 1] : void 0;
|
|
2022
3002
|
const action = usersIdx >= 0 ? pathSegments[usersIdx + 2] : void 0;
|
|
2023
3003
|
if (visitorId && action === "events") {
|
|
3004
|
+
const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
2024
3005
|
const params2 = {
|
|
2025
3006
|
siteId: q.siteId,
|
|
2026
3007
|
type: q.type,
|
|
3008
|
+
eventName: q.eventName,
|
|
3009
|
+
eventNames,
|
|
3010
|
+
eventSource: q.eventSource,
|
|
2027
3011
|
period: q.period,
|
|
2028
3012
|
dateFrom: q.dateFrom,
|
|
2029
3013
|
dateTo: q.dateTo,
|
|
@@ -2071,11 +3055,11 @@ async function createCollector(config) {
|
|
|
2071
3055
|
async listUsers(params) {
|
|
2072
3056
|
return db.listUsers(params);
|
|
2073
3057
|
},
|
|
2074
|
-
async getUserDetail(siteId,
|
|
2075
|
-
return db.getUserDetail(siteId,
|
|
3058
|
+
async getUserDetail(siteId, identifier) {
|
|
3059
|
+
return db.getUserDetail(siteId, identifier);
|
|
2076
3060
|
},
|
|
2077
|
-
async getUserEvents(siteId,
|
|
2078
|
-
return db.getUserEvents(siteId,
|
|
3061
|
+
async getUserEvents(siteId, identifier, params) {
|
|
3062
|
+
return db.getUserEvents(siteId, identifier, params);
|
|
2079
3063
|
},
|
|
2080
3064
|
async track(siteId, name, properties, options) {
|
|
2081
3065
|
const event = {
|
|
@@ -2130,7 +3114,8 @@ function createAdapter(config) {
|
|
|
2130
3114
|
}
|
|
2131
3115
|
}
|
|
2132
3116
|
async function parseBody(req) {
|
|
2133
|
-
if (req.body) return req.body;
|
|
3117
|
+
if (req.body && typeof req.body === "object") return req.body;
|
|
3118
|
+
if (typeof req.body === "string") return JSON.parse(req.body);
|
|
2134
3119
|
return new Promise((resolve, reject) => {
|
|
2135
3120
|
let data = "";
|
|
2136
3121
|
req.on("data", (chunk) => {
|