@litemetrics/node 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/dist/index.cjs +670 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +668 -80
- package/dist/index.js.map +1 -1
- package/package.json +13 -4
package/dist/index.cjs
CHANGED
|
@@ -32,7 +32,8 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ClickHouseAdapter: () => ClickHouseAdapter,
|
|
34
34
|
MongoDBAdapter: () => MongoDBAdapter,
|
|
35
|
-
createCollector: () => createCollector
|
|
35
|
+
createCollector: () => createCollector,
|
|
36
|
+
isBot: () => isBot
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(index_exports);
|
|
38
39
|
|
|
@@ -191,6 +192,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
|
191
192
|
title Nullable(String),
|
|
192
193
|
event_name Nullable(String),
|
|
193
194
|
properties Nullable(String),
|
|
195
|
+
event_source LowCardinality(Nullable(String)),
|
|
196
|
+
event_subtype LowCardinality(Nullable(String)),
|
|
197
|
+
page_path Nullable(String),
|
|
198
|
+
target_url_path Nullable(String),
|
|
199
|
+
element_selector Nullable(String),
|
|
200
|
+
element_text Nullable(String),
|
|
201
|
+
scroll_depth_pct Nullable(UInt8),
|
|
194
202
|
user_id Nullable(String),
|
|
195
203
|
traits Nullable(String),
|
|
196
204
|
country LowCardinality(Nullable(String)),
|
|
@@ -222,6 +230,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
222
230
|
name String,
|
|
223
231
|
domain Nullable(String),
|
|
224
232
|
allowed_origins Nullable(String),
|
|
233
|
+
conversion_events Nullable(String),
|
|
225
234
|
created_at DateTime64(3),
|
|
226
235
|
updated_at DateTime64(3),
|
|
227
236
|
version UInt64,
|
|
@@ -230,6 +239,43 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
230
239
|
ORDER BY (site_id)
|
|
231
240
|
SETTINGS index_granularity = 8192
|
|
232
241
|
`;
|
|
242
|
+
function toCHDateTime(d) {
|
|
243
|
+
const iso = typeof d === "string" ? d : d.toISOString();
|
|
244
|
+
return iso.replace("T", " ").replace("Z", "");
|
|
245
|
+
}
|
|
246
|
+
function buildFilterConditions(filters) {
|
|
247
|
+
if (!filters) return { conditions: [], params: {} };
|
|
248
|
+
const map = {
|
|
249
|
+
"geo.country": "country",
|
|
250
|
+
"geo.city": "city",
|
|
251
|
+
"geo.region": "region",
|
|
252
|
+
"language": "language",
|
|
253
|
+
"device.type": "device_type",
|
|
254
|
+
"device.browser": "browser",
|
|
255
|
+
"device.os": "os",
|
|
256
|
+
"utm.source": "utm_source",
|
|
257
|
+
"utm.medium": "utm_medium",
|
|
258
|
+
"utm.campaign": "utm_campaign",
|
|
259
|
+
"utm.term": "utm_term",
|
|
260
|
+
"utm.content": "utm_content",
|
|
261
|
+
"referrer": "referrer",
|
|
262
|
+
"event_source": "event_source",
|
|
263
|
+
"event_subtype": "event_subtype",
|
|
264
|
+
"page_path": "page_path",
|
|
265
|
+
"target_url_path": "target_url_path",
|
|
266
|
+
"event_name": "event_name",
|
|
267
|
+
"type": "type"
|
|
268
|
+
};
|
|
269
|
+
const conditions = [];
|
|
270
|
+
const params = {};
|
|
271
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
272
|
+
if (!value || !map[key]) continue;
|
|
273
|
+
const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
274
|
+
conditions.push(`${map[key]} = {${paramKey}:String}`);
|
|
275
|
+
params[paramKey] = value;
|
|
276
|
+
}
|
|
277
|
+
return { conditions, params };
|
|
278
|
+
}
|
|
233
279
|
var ClickHouseAdapter = class {
|
|
234
280
|
client;
|
|
235
281
|
constructor(url) {
|
|
@@ -243,6 +289,16 @@ var ClickHouseAdapter = class {
|
|
|
243
289
|
async init() {
|
|
244
290
|
await this.client.command({ query: CREATE_EVENTS_TABLE });
|
|
245
291
|
await this.client.command({ query: CREATE_SITES_TABLE });
|
|
292
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_source LowCardinality(Nullable(String))` });
|
|
293
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_subtype LowCardinality(Nullable(String))` });
|
|
294
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS page_path Nullable(String)` });
|
|
295
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS target_url_path Nullable(String)` });
|
|
296
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_selector Nullable(String)` });
|
|
297
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_text Nullable(String)` });
|
|
298
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS scroll_depth_pct Nullable(UInt8)` });
|
|
299
|
+
await this.client.command({
|
|
300
|
+
query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
|
|
301
|
+
});
|
|
246
302
|
}
|
|
247
303
|
async close() {
|
|
248
304
|
await this.client.close();
|
|
@@ -253,7 +309,7 @@ var ClickHouseAdapter = class {
|
|
|
253
309
|
const rows = events.map((e) => ({
|
|
254
310
|
site_id: e.siteId,
|
|
255
311
|
type: e.type,
|
|
256
|
-
timestamp: new Date(e.timestamp)
|
|
312
|
+
timestamp: toCHDateTime(new Date(e.timestamp)),
|
|
257
313
|
session_id: e.sessionId,
|
|
258
314
|
visitor_id: e.visitorId,
|
|
259
315
|
url: e.url ?? null,
|
|
@@ -261,6 +317,13 @@ var ClickHouseAdapter = class {
|
|
|
261
317
|
title: e.title ?? null,
|
|
262
318
|
event_name: e.name ?? null,
|
|
263
319
|
properties: e.properties ? JSON.stringify(e.properties) : null,
|
|
320
|
+
event_source: e.eventSource ?? null,
|
|
321
|
+
event_subtype: e.eventSubtype ?? null,
|
|
322
|
+
page_path: e.pagePath ?? null,
|
|
323
|
+
target_url_path: e.targetUrlPath ?? null,
|
|
324
|
+
element_selector: e.elementSelector ?? null,
|
|
325
|
+
element_text: e.elementText ?? null,
|
|
326
|
+
scroll_depth_pct: e.scrollDepthPct ?? null,
|
|
264
327
|
user_id: e.userId ?? null,
|
|
265
328
|
traits: e.traits ? JSON.stringify(e.traits) : null,
|
|
266
329
|
country: e.geo?.country ?? null,
|
|
@@ -293,10 +356,12 @@ var ClickHouseAdapter = class {
|
|
|
293
356
|
const limit = q.limit ?? 10;
|
|
294
357
|
const params = {
|
|
295
358
|
siteId,
|
|
296
|
-
from: dateRange.from,
|
|
297
|
-
to: dateRange.to,
|
|
359
|
+
from: toCHDateTime(dateRange.from),
|
|
360
|
+
to: toCHDateTime(dateRange.to),
|
|
298
361
|
limit
|
|
299
362
|
};
|
|
363
|
+
const filter = buildFilterConditions(q.filters);
|
|
364
|
+
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
300
365
|
let data = [];
|
|
301
366
|
let total = 0;
|
|
302
367
|
switch (q.metric) {
|
|
@@ -306,8 +371,8 @@ var ClickHouseAdapter = class {
|
|
|
306
371
|
WHERE site_id = {siteId:String}
|
|
307
372
|
AND timestamp >= {from:String}
|
|
308
373
|
AND timestamp <= {to:String}
|
|
309
|
-
AND type = 'pageview'`,
|
|
310
|
-
params
|
|
374
|
+
AND type = 'pageview'${filterSql}`,
|
|
375
|
+
{ ...params, ...filter.params }
|
|
311
376
|
);
|
|
312
377
|
total = Number(rows[0]?.value ?? 0);
|
|
313
378
|
data = [{ key: "pageviews", value: total }];
|
|
@@ -318,8 +383,8 @@ var ClickHouseAdapter = class {
|
|
|
318
383
|
`SELECT uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
319
384
|
WHERE site_id = {siteId:String}
|
|
320
385
|
AND timestamp >= {from:String}
|
|
321
|
-
AND timestamp <= {to:String}`,
|
|
322
|
-
params
|
|
386
|
+
AND timestamp <= {to:String}${filterSql}`,
|
|
387
|
+
{ ...params, ...filter.params }
|
|
323
388
|
);
|
|
324
389
|
total = Number(rows[0]?.value ?? 0);
|
|
325
390
|
data = [{ key: "visitors", value: total }];
|
|
@@ -330,8 +395,8 @@ var ClickHouseAdapter = class {
|
|
|
330
395
|
`SELECT uniq(session_id) AS value FROM ${EVENTS_TABLE}
|
|
331
396
|
WHERE site_id = {siteId:String}
|
|
332
397
|
AND timestamp >= {from:String}
|
|
333
|
-
AND timestamp <= {to:String}`,
|
|
334
|
-
params
|
|
398
|
+
AND timestamp <= {to:String}${filterSql}`,
|
|
399
|
+
{ ...params, ...filter.params }
|
|
335
400
|
);
|
|
336
401
|
total = Number(rows[0]?.value ?? 0);
|
|
337
402
|
data = [{ key: "sessions", value: total }];
|
|
@@ -343,13 +408,33 @@ var ClickHouseAdapter = class {
|
|
|
343
408
|
WHERE site_id = {siteId:String}
|
|
344
409
|
AND timestamp >= {from:String}
|
|
345
410
|
AND timestamp <= {to:String}
|
|
346
|
-
AND type = 'event'`,
|
|
347
|
-
params
|
|
411
|
+
AND type = 'event'${filterSql}`,
|
|
412
|
+
{ ...params, ...filter.params }
|
|
348
413
|
);
|
|
349
414
|
total = Number(rows[0]?.value ?? 0);
|
|
350
415
|
data = [{ key: "events", value: total }];
|
|
351
416
|
break;
|
|
352
417
|
}
|
|
418
|
+
case "conversions": {
|
|
419
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
420
|
+
if (conversionEvents.length === 0) {
|
|
421
|
+
total = 0;
|
|
422
|
+
data = [{ key: "conversions", value: 0 }];
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
const rows = await this.queryRows(
|
|
426
|
+
`SELECT count() AS value FROM ${EVENTS_TABLE}
|
|
427
|
+
WHERE site_id = {siteId:String}
|
|
428
|
+
AND timestamp >= {from:String}
|
|
429
|
+
AND timestamp <= {to:String}
|
|
430
|
+
AND type = 'event'
|
|
431
|
+
AND event_name IN {eventNames:Array(String)}${filterSql}`,
|
|
432
|
+
{ ...params, eventNames: conversionEvents, ...filter.params }
|
|
433
|
+
);
|
|
434
|
+
total = Number(rows[0]?.value ?? 0);
|
|
435
|
+
data = [{ key: "conversions", value: total }];
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
353
438
|
case "top_pages": {
|
|
354
439
|
const rows = await this.queryRows(
|
|
355
440
|
`SELECT url AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
@@ -357,11 +442,11 @@ var ClickHouseAdapter = class {
|
|
|
357
442
|
AND timestamp >= {from:String}
|
|
358
443
|
AND timestamp <= {to:String}
|
|
359
444
|
AND type = 'pageview'
|
|
360
|
-
AND url IS NOT NULL
|
|
445
|
+
AND url IS NOT NULL${filterSql}
|
|
361
446
|
GROUP BY url
|
|
362
447
|
ORDER BY value DESC
|
|
363
448
|
LIMIT {limit:UInt32}`,
|
|
364
|
-
params
|
|
449
|
+
{ ...params, ...filter.params }
|
|
365
450
|
);
|
|
366
451
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
367
452
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -375,11 +460,11 @@ var ClickHouseAdapter = class {
|
|
|
375
460
|
AND timestamp <= {to:String}
|
|
376
461
|
AND type = 'pageview'
|
|
377
462
|
AND referrer IS NOT NULL
|
|
378
|
-
AND referrer != ''
|
|
463
|
+
AND referrer != ''${filterSql}
|
|
379
464
|
GROUP BY referrer
|
|
380
465
|
ORDER BY value DESC
|
|
381
466
|
LIMIT {limit:UInt32}`,
|
|
382
|
-
params
|
|
467
|
+
{ ...params, ...filter.params }
|
|
383
468
|
);
|
|
384
469
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
385
470
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -391,11 +476,11 @@ var ClickHouseAdapter = class {
|
|
|
391
476
|
WHERE site_id = {siteId:String}
|
|
392
477
|
AND timestamp >= {from:String}
|
|
393
478
|
AND timestamp <= {to:String}
|
|
394
|
-
AND country IS NOT NULL
|
|
479
|
+
AND country IS NOT NULL${filterSql}
|
|
395
480
|
GROUP BY country
|
|
396
481
|
ORDER BY value DESC
|
|
397
482
|
LIMIT {limit:UInt32}`,
|
|
398
|
-
params
|
|
483
|
+
{ ...params, ...filter.params }
|
|
399
484
|
);
|
|
400
485
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
401
486
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -407,11 +492,11 @@ var ClickHouseAdapter = class {
|
|
|
407
492
|
WHERE site_id = {siteId:String}
|
|
408
493
|
AND timestamp >= {from:String}
|
|
409
494
|
AND timestamp <= {to:String}
|
|
410
|
-
AND city IS NOT NULL
|
|
495
|
+
AND city IS NOT NULL${filterSql}
|
|
411
496
|
GROUP BY city
|
|
412
497
|
ORDER BY value DESC
|
|
413
498
|
LIMIT {limit:UInt32}`,
|
|
414
|
-
params
|
|
499
|
+
{ ...params, ...filter.params }
|
|
415
500
|
);
|
|
416
501
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
417
502
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -425,10 +510,132 @@ var ClickHouseAdapter = class {
|
|
|
425
510
|
AND timestamp <= {to:String}
|
|
426
511
|
AND type = 'event'
|
|
427
512
|
AND event_name IS NOT NULL
|
|
513
|
+
${filterSql}
|
|
514
|
+
GROUP BY event_name
|
|
515
|
+
ORDER BY value DESC
|
|
516
|
+
LIMIT {limit:UInt32}`,
|
|
517
|
+
{ ...params, ...filter.params }
|
|
518
|
+
);
|
|
519
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
520
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case "top_conversions": {
|
|
524
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
525
|
+
if (conversionEvents.length === 0) {
|
|
526
|
+
total = 0;
|
|
527
|
+
data = [];
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
const rows = await this.queryRows(
|
|
531
|
+
`SELECT event_name AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
532
|
+
WHERE site_id = {siteId:String}
|
|
533
|
+
AND timestamp >= {from:String}
|
|
534
|
+
AND timestamp <= {to:String}
|
|
535
|
+
AND type = 'event'
|
|
536
|
+
AND event_name IN {eventNames:Array(String)}
|
|
537
|
+
${filterSql}
|
|
428
538
|
GROUP BY event_name
|
|
429
539
|
ORDER BY value DESC
|
|
430
540
|
LIMIT {limit:UInt32}`,
|
|
431
|
-
params
|
|
541
|
+
{ ...params, eventNames: conversionEvents, ...filter.params }
|
|
542
|
+
);
|
|
543
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
544
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "top_exit_pages": {
|
|
548
|
+
const rows = await this.queryRows(
|
|
549
|
+
`SELECT url AS key, count() AS value FROM (
|
|
550
|
+
SELECT session_id, argMax(url, timestamp) AS url
|
|
551
|
+
FROM ${EVENTS_TABLE}
|
|
552
|
+
WHERE site_id = {siteId:String}
|
|
553
|
+
AND timestamp >= {from:String}
|
|
554
|
+
AND timestamp <= {to:String}
|
|
555
|
+
AND type = 'pageview'
|
|
556
|
+
AND url IS NOT NULL${filterSql}
|
|
557
|
+
GROUP BY session_id
|
|
558
|
+
)
|
|
559
|
+
GROUP BY url
|
|
560
|
+
ORDER BY value DESC
|
|
561
|
+
LIMIT {limit:UInt32}`,
|
|
562
|
+
{ ...params, ...filter.params }
|
|
563
|
+
);
|
|
564
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
565
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case "top_transitions": {
|
|
569
|
+
const rows = await this.queryRows(
|
|
570
|
+
`SELECT concat(prev_url, ' \u2192 ', url) AS key, count() AS value FROM (
|
|
571
|
+
SELECT session_id, url,
|
|
572
|
+
lag(url) OVER (PARTITION BY session_id ORDER BY timestamp) AS prev_url
|
|
573
|
+
FROM ${EVENTS_TABLE}
|
|
574
|
+
WHERE site_id = {siteId:String}
|
|
575
|
+
AND timestamp >= {from:String}
|
|
576
|
+
AND timestamp <= {to:String}
|
|
577
|
+
AND type = 'pageview'
|
|
578
|
+
AND url IS NOT NULL${filterSql}
|
|
579
|
+
)
|
|
580
|
+
WHERE prev_url IS NOT NULL
|
|
581
|
+
GROUP BY key
|
|
582
|
+
ORDER BY value DESC
|
|
583
|
+
LIMIT {limit:UInt32}`,
|
|
584
|
+
{ ...params, ...filter.params }
|
|
585
|
+
);
|
|
586
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
587
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case "top_scroll_pages": {
|
|
591
|
+
const rows = await this.queryRows(
|
|
592
|
+
`SELECT page_path AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
593
|
+
WHERE site_id = {siteId:String}
|
|
594
|
+
AND timestamp >= {from:String}
|
|
595
|
+
AND timestamp <= {to:String}
|
|
596
|
+
AND type = 'event'
|
|
597
|
+
AND event_subtype = 'scroll_depth'
|
|
598
|
+
AND page_path IS NOT NULL${filterSql}
|
|
599
|
+
GROUP BY page_path
|
|
600
|
+
ORDER BY value DESC
|
|
601
|
+
LIMIT {limit:UInt32}`,
|
|
602
|
+
{ ...params, ...filter.params }
|
|
603
|
+
);
|
|
604
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
605
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case "top_button_clicks": {
|
|
609
|
+
const rows = await this.queryRows(
|
|
610
|
+
`SELECT ifNull(element_text, element_selector) AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
611
|
+
WHERE site_id = {siteId:String}
|
|
612
|
+
AND timestamp >= {from:String}
|
|
613
|
+
AND timestamp <= {to:String}
|
|
614
|
+
AND type = 'event'
|
|
615
|
+
AND event_subtype = 'button_click'
|
|
616
|
+
AND (element_text IS NOT NULL OR element_selector IS NOT NULL)${filterSql}
|
|
617
|
+
GROUP BY key
|
|
618
|
+
ORDER BY value DESC
|
|
619
|
+
LIMIT {limit:UInt32}`,
|
|
620
|
+
{ ...params, ...filter.params }
|
|
621
|
+
);
|
|
622
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
623
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case "top_link_targets": {
|
|
627
|
+
const rows = await this.queryRows(
|
|
628
|
+
`SELECT target_url_path AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
629
|
+
WHERE site_id = {siteId:String}
|
|
630
|
+
AND timestamp >= {from:String}
|
|
631
|
+
AND timestamp <= {to:String}
|
|
632
|
+
AND type = 'event'
|
|
633
|
+
AND event_subtype IN ('link_click','outbound_click')
|
|
634
|
+
AND target_url_path IS NOT NULL${filterSql}
|
|
635
|
+
GROUP BY target_url_path
|
|
636
|
+
ORDER BY value DESC
|
|
637
|
+
LIMIT {limit:UInt32}`,
|
|
638
|
+
{ ...params, ...filter.params }
|
|
432
639
|
);
|
|
433
640
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
434
641
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -441,10 +648,11 @@ var ClickHouseAdapter = class {
|
|
|
441
648
|
AND timestamp >= {from:String}
|
|
442
649
|
AND timestamp <= {to:String}
|
|
443
650
|
AND device_type IS NOT NULL
|
|
651
|
+
${filterSql}
|
|
444
652
|
GROUP BY device_type
|
|
445
653
|
ORDER BY value DESC
|
|
446
654
|
LIMIT {limit:UInt32}`,
|
|
447
|
-
params
|
|
655
|
+
{ ...params, ...filter.params }
|
|
448
656
|
);
|
|
449
657
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
450
658
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -457,10 +665,11 @@ var ClickHouseAdapter = class {
|
|
|
457
665
|
AND timestamp >= {from:String}
|
|
458
666
|
AND timestamp <= {to:String}
|
|
459
667
|
AND browser IS NOT NULL
|
|
668
|
+
${filterSql}
|
|
460
669
|
GROUP BY browser
|
|
461
670
|
ORDER BY value DESC
|
|
462
671
|
LIMIT {limit:UInt32}`,
|
|
463
|
-
params
|
|
672
|
+
{ ...params, ...filter.params }
|
|
464
673
|
);
|
|
465
674
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
466
675
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -473,10 +682,11 @@ var ClickHouseAdapter = class {
|
|
|
473
682
|
AND timestamp >= {from:String}
|
|
474
683
|
AND timestamp <= {to:String}
|
|
475
684
|
AND os IS NOT NULL
|
|
685
|
+
${filterSql}
|
|
476
686
|
GROUP BY os
|
|
477
687
|
ORDER BY value DESC
|
|
478
688
|
LIMIT {limit:UInt32}`,
|
|
479
|
-
params
|
|
689
|
+
{ ...params, ...filter.params }
|
|
480
690
|
);
|
|
481
691
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
482
692
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -484,7 +694,7 @@ var ClickHouseAdapter = class {
|
|
|
484
694
|
}
|
|
485
695
|
}
|
|
486
696
|
const result = { metric: q.metric, period, data, total };
|
|
487
|
-
if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
|
|
697
|
+
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
488
698
|
const prevRange = previousPeriodRange(dateRange);
|
|
489
699
|
const prevResult = await this.query({
|
|
490
700
|
...q,
|
|
@@ -514,7 +724,12 @@ var ClickHouseAdapter = class {
|
|
|
514
724
|
const granularity = params.granularity ?? autoGranularity(period);
|
|
515
725
|
const bucketFn = this.granularityToClickHouseFunc(granularity);
|
|
516
726
|
const dateFormat = granularityToDateFormat(granularity);
|
|
727
|
+
const filter = buildFilterConditions(params.filters);
|
|
728
|
+
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
517
729
|
const typeFilter = params.metric === "pageviews" ? `AND type = 'pageview'` : "";
|
|
730
|
+
const eventsFilter = params.metric === "events" ? `AND type = 'event'` : "";
|
|
731
|
+
const conversionsFilter = params.metric === "conversions" ? `AND type = 'event' AND event_name IN {eventNames:Array(String)}` : "";
|
|
732
|
+
const extraFilters = [typeFilter, eventsFilter, conversionsFilter, filterSql].filter(Boolean).join(" ");
|
|
518
733
|
let sql;
|
|
519
734
|
if (params.metric === "visitors" || params.metric === "sessions") {
|
|
520
735
|
const field = params.metric === "visitors" ? "visitor_id" : "session_id";
|
|
@@ -524,7 +739,7 @@ var ClickHouseAdapter = class {
|
|
|
524
739
|
WHERE site_id = {siteId:String}
|
|
525
740
|
AND timestamp >= {from:String}
|
|
526
741
|
AND timestamp <= {to:String}
|
|
527
|
-
${
|
|
742
|
+
${extraFilters}
|
|
528
743
|
GROUP BY bucket
|
|
529
744
|
ORDER BY bucket ASC
|
|
530
745
|
`;
|
|
@@ -535,15 +750,17 @@ var ClickHouseAdapter = class {
|
|
|
535
750
|
WHERE site_id = {siteId:String}
|
|
536
751
|
AND timestamp >= {from:String}
|
|
537
752
|
AND timestamp <= {to:String}
|
|
538
|
-
${
|
|
753
|
+
${extraFilters}
|
|
539
754
|
GROUP BY bucket
|
|
540
755
|
ORDER BY bucket ASC
|
|
541
756
|
`;
|
|
542
757
|
}
|
|
543
758
|
const rows = await this.queryRows(sql, {
|
|
544
759
|
siteId: params.siteId,
|
|
545
|
-
from: dateRange.from,
|
|
546
|
-
to: dateRange.to
|
|
760
|
+
from: toCHDateTime(dateRange.from),
|
|
761
|
+
to: toCHDateTime(dateRange.to),
|
|
762
|
+
eventNames: params.conversionEvents ?? [],
|
|
763
|
+
...filter.params
|
|
547
764
|
});
|
|
548
765
|
const mappedRows = rows.map((r) => ({
|
|
549
766
|
_id: this.convertClickHouseBucket(r.bucket, granularity),
|
|
@@ -609,7 +826,7 @@ var ClickHouseAdapter = class {
|
|
|
609
826
|
GROUP BY visitor_id`,
|
|
610
827
|
{
|
|
611
828
|
siteId: params.siteId,
|
|
612
|
-
since: startDate
|
|
829
|
+
since: toCHDateTime(startDate)
|
|
613
830
|
}
|
|
614
831
|
);
|
|
615
832
|
const cohortMap = /* @__PURE__ */ new Map();
|
|
@@ -661,6 +878,14 @@ var ClickHouseAdapter = class {
|
|
|
661
878
|
conditions.push(`event_name = {eventName:String}`);
|
|
662
879
|
queryParams.eventName = params.eventName;
|
|
663
880
|
}
|
|
881
|
+
if (params.eventSource) {
|
|
882
|
+
conditions.push(`event_source = {eventSource:String}`);
|
|
883
|
+
queryParams.eventSource = params.eventSource;
|
|
884
|
+
}
|
|
885
|
+
if (params.eventNames && params.eventNames.length > 0) {
|
|
886
|
+
conditions.push(`event_name IN {eventNames:Array(String)}`);
|
|
887
|
+
queryParams.eventNames = params.eventNames;
|
|
888
|
+
}
|
|
664
889
|
if (params.visitorId) {
|
|
665
890
|
conditions.push(`visitor_id = {visitorId:String}`);
|
|
666
891
|
queryParams.visitorId = params.visitorId;
|
|
@@ -676,14 +901,16 @@ var ClickHouseAdapter = class {
|
|
|
676
901
|
dateTo: params.dateTo
|
|
677
902
|
});
|
|
678
903
|
conditions.push(`timestamp >= {from:String} AND timestamp <= {to:String}`);
|
|
679
|
-
queryParams.from = dateRange.from;
|
|
680
|
-
queryParams.to = dateRange.to;
|
|
904
|
+
queryParams.from = toCHDateTime(dateRange.from);
|
|
905
|
+
queryParams.to = toCHDateTime(dateRange.to);
|
|
681
906
|
}
|
|
682
907
|
const where = conditions.join(" AND ");
|
|
683
908
|
const [events, countRows] = await Promise.all([
|
|
684
909
|
this.queryRows(
|
|
685
910
|
`SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
|
|
686
|
-
event_name, properties,
|
|
911
|
+
event_name, properties, event_source, event_subtype, page_path, target_url_path,
|
|
912
|
+
element_selector, element_text, scroll_depth_pct,
|
|
913
|
+
user_id, traits, country, city, region,
|
|
687
914
|
device_type, browser, os, language,
|
|
688
915
|
utm_source, utm_medium, utm_campaign, utm_term, utm_content
|
|
689
916
|
FROM ${EVENTS_TABLE}
|
|
@@ -728,13 +955,22 @@ var ClickHouseAdapter = class {
|
|
|
728
955
|
countIf(type = 'pageview') AS totalPageviews,
|
|
729
956
|
uniq(session_id) AS totalSessions,
|
|
730
957
|
anyLast(url) AS lastUrl,
|
|
958
|
+
anyLast(referrer) AS referrer,
|
|
731
959
|
anyLast(device_type) AS device_type,
|
|
732
960
|
anyLast(browser) AS browser,
|
|
733
961
|
anyLast(os) AS os,
|
|
734
962
|
anyLast(country) AS country,
|
|
735
963
|
anyLast(city) AS city,
|
|
736
964
|
anyLast(region) AS region,
|
|
737
|
-
anyLast(language) AS language
|
|
965
|
+
anyLast(language) AS language,
|
|
966
|
+
anyLast(timezone) AS timezone,
|
|
967
|
+
anyLast(screen_width) AS screen_width,
|
|
968
|
+
anyLast(screen_height) AS screen_height,
|
|
969
|
+
anyLast(utm_source) AS utm_source,
|
|
970
|
+
anyLast(utm_medium) AS utm_medium,
|
|
971
|
+
anyLast(utm_campaign) AS utm_campaign,
|
|
972
|
+
anyLast(utm_term) AS utm_term,
|
|
973
|
+
anyLast(utm_content) AS utm_content
|
|
738
974
|
FROM ${EVENTS_TABLE}
|
|
739
975
|
WHERE ${where}
|
|
740
976
|
GROUP BY visitor_id
|
|
@@ -762,9 +998,19 @@ var ClickHouseAdapter = class {
|
|
|
762
998
|
totalPageviews: Number(u.totalPageviews),
|
|
763
999
|
totalSessions: Number(u.totalSessions),
|
|
764
1000
|
lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
|
|
1001
|
+
referrer: u.referrer ? String(u.referrer) : void 0,
|
|
765
1002
|
device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
|
|
766
1003
|
geo: u.country ? { country: String(u.country), city: u.city ? String(u.city) : void 0, region: u.region ? String(u.region) : void 0 } : void 0,
|
|
767
|
-
language: u.language ? String(u.language) : void 0
|
|
1004
|
+
language: u.language ? String(u.language) : void 0,
|
|
1005
|
+
timezone: u.timezone ? String(u.timezone) : void 0,
|
|
1006
|
+
screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
|
|
1007
|
+
utm: u.utm_source ? {
|
|
1008
|
+
source: String(u.utm_source),
|
|
1009
|
+
medium: u.utm_medium ? String(u.utm_medium) : void 0,
|
|
1010
|
+
campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
|
|
1011
|
+
term: u.utm_term ? String(u.utm_term) : void 0,
|
|
1012
|
+
content: u.utm_content ? String(u.utm_content) : void 0
|
|
1013
|
+
} : void 0
|
|
768
1014
|
}));
|
|
769
1015
|
return {
|
|
770
1016
|
users,
|
|
@@ -783,15 +1029,18 @@ var ClickHouseAdapter = class {
|
|
|
783
1029
|
}
|
|
784
1030
|
// ─── Site Management ──────────────────────────────────────
|
|
785
1031
|
async createSite(data) {
|
|
786
|
-
const now =
|
|
1032
|
+
const now = /* @__PURE__ */ new Date();
|
|
1033
|
+
const nowISO = now.toISOString();
|
|
1034
|
+
const nowCH = toCHDateTime(now);
|
|
787
1035
|
const site = {
|
|
788
1036
|
siteId: generateSiteId(),
|
|
789
1037
|
secretKey: generateSecretKey(),
|
|
790
1038
|
name: data.name,
|
|
791
1039
|
domain: data.domain,
|
|
792
1040
|
allowedOrigins: data.allowedOrigins,
|
|
793
|
-
|
|
794
|
-
|
|
1041
|
+
conversionEvents: data.conversionEvents,
|
|
1042
|
+
createdAt: nowISO,
|
|
1043
|
+
updatedAt: nowISO
|
|
795
1044
|
};
|
|
796
1045
|
await this.client.insert({
|
|
797
1046
|
table: SITES_TABLE,
|
|
@@ -801,8 +1050,9 @@ var ClickHouseAdapter = class {
|
|
|
801
1050
|
name: site.name,
|
|
802
1051
|
domain: site.domain ?? null,
|
|
803
1052
|
allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
|
|
804
|
-
|
|
805
|
-
|
|
1053
|
+
conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
|
|
1054
|
+
created_at: nowCH,
|
|
1055
|
+
updated_at: nowCH,
|
|
806
1056
|
version: 1,
|
|
807
1057
|
is_deleted: 0
|
|
808
1058
|
}],
|
|
@@ -812,7 +1062,7 @@ var ClickHouseAdapter = class {
|
|
|
812
1062
|
}
|
|
813
1063
|
async getSite(siteId) {
|
|
814
1064
|
const rows = await this.queryRows(
|
|
815
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1065
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
816
1066
|
FROM ${SITES_TABLE} FINAL
|
|
817
1067
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
818
1068
|
{ siteId }
|
|
@@ -821,7 +1071,7 @@ var ClickHouseAdapter = class {
|
|
|
821
1071
|
}
|
|
822
1072
|
async getSiteBySecret(secretKey) {
|
|
823
1073
|
const rows = await this.queryRows(
|
|
824
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1074
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
825
1075
|
FROM ${SITES_TABLE} FINAL
|
|
826
1076
|
WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
|
|
827
1077
|
{ secretKey }
|
|
@@ -830,7 +1080,7 @@ var ClickHouseAdapter = class {
|
|
|
830
1080
|
}
|
|
831
1081
|
async listSites() {
|
|
832
1082
|
const rows = await this.queryRows(
|
|
833
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1083
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
834
1084
|
FROM ${SITES_TABLE} FINAL
|
|
835
1085
|
WHERE is_deleted = 0
|
|
836
1086
|
ORDER BY created_at DESC`,
|
|
@@ -840,18 +1090,21 @@ var ClickHouseAdapter = class {
|
|
|
840
1090
|
}
|
|
841
1091
|
async updateSite(siteId, data) {
|
|
842
1092
|
const currentRows = await this.queryRows(
|
|
843
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at, version
|
|
1093
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
844
1094
|
FROM ${SITES_TABLE} FINAL
|
|
845
1095
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
846
1096
|
{ siteId }
|
|
847
1097
|
);
|
|
848
1098
|
if (currentRows.length === 0) return null;
|
|
849
1099
|
const current = currentRows[0];
|
|
850
|
-
const now =
|
|
1100
|
+
const now = /* @__PURE__ */ new Date();
|
|
1101
|
+
const nowISO = now.toISOString();
|
|
1102
|
+
const nowCH = toCHDateTime(now);
|
|
851
1103
|
const newVersion = Number(current.version) + 1;
|
|
852
1104
|
const newName = data.name !== void 0 ? data.name : String(current.name);
|
|
853
1105
|
const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
|
|
854
1106
|
const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
|
|
1107
|
+
const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
|
|
855
1108
|
await this.client.insert({
|
|
856
1109
|
table: SITES_TABLE,
|
|
857
1110
|
values: [{
|
|
@@ -860,8 +1113,9 @@ var ClickHouseAdapter = class {
|
|
|
860
1113
|
name: newName,
|
|
861
1114
|
domain: newDomain,
|
|
862
1115
|
allowed_origins: newOrigins,
|
|
863
|
-
|
|
864
|
-
|
|
1116
|
+
conversion_events: newConversions,
|
|
1117
|
+
created_at: toCHDateTime(String(current.created_at)),
|
|
1118
|
+
updated_at: nowCH,
|
|
865
1119
|
version: newVersion,
|
|
866
1120
|
is_deleted: 0
|
|
867
1121
|
}],
|
|
@@ -873,20 +1127,21 @@ var ClickHouseAdapter = class {
|
|
|
873
1127
|
name: newName,
|
|
874
1128
|
domain: newDomain ?? void 0,
|
|
875
1129
|
allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
|
|
1130
|
+
conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
|
|
876
1131
|
createdAt: String(current.created_at),
|
|
877
|
-
updatedAt:
|
|
1132
|
+
updatedAt: nowISO
|
|
878
1133
|
};
|
|
879
1134
|
}
|
|
880
1135
|
async deleteSite(siteId) {
|
|
881
1136
|
const currentRows = await this.queryRows(
|
|
882
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
|
|
1137
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
883
1138
|
FROM ${SITES_TABLE} FINAL
|
|
884
1139
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
885
1140
|
{ siteId }
|
|
886
1141
|
);
|
|
887
1142
|
if (currentRows.length === 0) return false;
|
|
888
1143
|
const current = currentRows[0];
|
|
889
|
-
const
|
|
1144
|
+
const nowCH = toCHDateTime(/* @__PURE__ */ new Date());
|
|
890
1145
|
await this.client.insert({
|
|
891
1146
|
table: SITES_TABLE,
|
|
892
1147
|
values: [{
|
|
@@ -895,8 +1150,9 @@ var ClickHouseAdapter = class {
|
|
|
895
1150
|
name: String(current.name),
|
|
896
1151
|
domain: current.domain ? String(current.domain) : null,
|
|
897
1152
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
898
|
-
|
|
899
|
-
|
|
1153
|
+
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
1154
|
+
created_at: toCHDateTime(String(current.created_at)),
|
|
1155
|
+
updated_at: nowCH,
|
|
900
1156
|
version: Number(current.version) + 1,
|
|
901
1157
|
is_deleted: 1
|
|
902
1158
|
}],
|
|
@@ -906,14 +1162,16 @@ var ClickHouseAdapter = class {
|
|
|
906
1162
|
}
|
|
907
1163
|
async regenerateSecret(siteId) {
|
|
908
1164
|
const currentRows = await this.queryRows(
|
|
909
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
|
|
1165
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
910
1166
|
FROM ${SITES_TABLE} FINAL
|
|
911
1167
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
912
1168
|
{ siteId }
|
|
913
1169
|
);
|
|
914
1170
|
if (currentRows.length === 0) return null;
|
|
915
1171
|
const current = currentRows[0];
|
|
916
|
-
const now =
|
|
1172
|
+
const now = /* @__PURE__ */ new Date();
|
|
1173
|
+
const nowISO = now.toISOString();
|
|
1174
|
+
const nowCH = toCHDateTime(now);
|
|
917
1175
|
const newSecret = generateSecretKey();
|
|
918
1176
|
await this.client.insert({
|
|
919
1177
|
table: SITES_TABLE,
|
|
@@ -923,8 +1181,9 @@ var ClickHouseAdapter = class {
|
|
|
923
1181
|
name: String(current.name),
|
|
924
1182
|
domain: current.domain ? String(current.domain) : null,
|
|
925
1183
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
926
|
-
|
|
927
|
-
|
|
1184
|
+
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
1185
|
+
created_at: toCHDateTime(String(current.created_at)),
|
|
1186
|
+
updated_at: nowCH,
|
|
928
1187
|
version: Number(current.version) + 1,
|
|
929
1188
|
is_deleted: 0
|
|
930
1189
|
}],
|
|
@@ -936,8 +1195,9 @@ var ClickHouseAdapter = class {
|
|
|
936
1195
|
name: String(current.name),
|
|
937
1196
|
domain: current.domain ? String(current.domain) : void 0,
|
|
938
1197
|
allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
|
|
1198
|
+
conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
|
|
939
1199
|
createdAt: String(current.created_at),
|
|
940
|
-
updatedAt:
|
|
1200
|
+
updatedAt: nowISO
|
|
941
1201
|
};
|
|
942
1202
|
}
|
|
943
1203
|
// ─── Helpers ─────────────────────────────────────────────
|
|
@@ -956,6 +1216,7 @@ var ClickHouseAdapter = class {
|
|
|
956
1216
|
name: String(row.name),
|
|
957
1217
|
domain: row.domain ? String(row.domain) : void 0,
|
|
958
1218
|
allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
|
|
1219
|
+
conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
|
|
959
1220
|
createdAt: new Date(String(row.created_at)).toISOString(),
|
|
960
1221
|
updatedAt: new Date(String(row.updated_at)).toISOString()
|
|
961
1222
|
};
|
|
@@ -972,6 +1233,13 @@ var ClickHouseAdapter = class {
|
|
|
972
1233
|
title: row.title ? String(row.title) : void 0,
|
|
973
1234
|
name: row.event_name ? String(row.event_name) : void 0,
|
|
974
1235
|
properties: this.parseJSON(row.properties),
|
|
1236
|
+
eventSource: row.event_source ? String(row.event_source) : void 0,
|
|
1237
|
+
eventSubtype: row.event_subtype ? String(row.event_subtype) : void 0,
|
|
1238
|
+
pagePath: row.page_path ? String(row.page_path) : void 0,
|
|
1239
|
+
targetUrlPath: row.target_url_path ? String(row.target_url_path) : void 0,
|
|
1240
|
+
elementSelector: row.element_selector ? String(row.element_selector) : void 0,
|
|
1241
|
+
elementText: row.element_text ? String(row.element_text) : void 0,
|
|
1242
|
+
scrollDepthPct: row.scroll_depth_pct !== null && row.scroll_depth_pct !== void 0 ? Number(row.scroll_depth_pct) : void 0,
|
|
975
1243
|
userId: row.user_id ? String(row.user_id) : void 0,
|
|
976
1244
|
traits: this.parseJSON(row.traits),
|
|
977
1245
|
geo: row.country ? {
|
|
@@ -1008,6 +1276,36 @@ var ClickHouseAdapter = class {
|
|
|
1008
1276
|
var import_mongodb = require("mongodb");
|
|
1009
1277
|
var EVENTS_COLLECTION = "litemetrics_events";
|
|
1010
1278
|
var SITES_COLLECTION = "litemetrics_sites";
|
|
1279
|
+
function buildFilterMatch(filters) {
|
|
1280
|
+
if (!filters) return {};
|
|
1281
|
+
const map = {
|
|
1282
|
+
"geo.country": "country",
|
|
1283
|
+
"geo.city": "city",
|
|
1284
|
+
"geo.region": "region",
|
|
1285
|
+
"language": "language",
|
|
1286
|
+
"device.type": "device_type",
|
|
1287
|
+
"device.browser": "browser",
|
|
1288
|
+
"device.os": "os",
|
|
1289
|
+
"utm.source": "utm_source",
|
|
1290
|
+
"utm.medium": "utm_medium",
|
|
1291
|
+
"utm.campaign": "utm_campaign",
|
|
1292
|
+
"utm.term": "utm_term",
|
|
1293
|
+
"utm.content": "utm_content",
|
|
1294
|
+
"referrer": "referrer",
|
|
1295
|
+
"event_source": "event_source",
|
|
1296
|
+
"event_subtype": "event_subtype",
|
|
1297
|
+
"page_path": "page_path",
|
|
1298
|
+
"target_url_path": "target_url_path",
|
|
1299
|
+
"event_name": "event_name",
|
|
1300
|
+
"type": "type"
|
|
1301
|
+
};
|
|
1302
|
+
const match = {};
|
|
1303
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
1304
|
+
if (!value || !map[key]) continue;
|
|
1305
|
+
match[map[key]] = value;
|
|
1306
|
+
}
|
|
1307
|
+
return match;
|
|
1308
|
+
}
|
|
1011
1309
|
var MongoDBAdapter = class {
|
|
1012
1310
|
client;
|
|
1013
1311
|
db;
|
|
@@ -1043,6 +1341,13 @@ var MongoDBAdapter = class {
|
|
|
1043
1341
|
title: e.title ?? null,
|
|
1044
1342
|
event_name: e.name ?? null,
|
|
1045
1343
|
properties: e.properties ?? null,
|
|
1344
|
+
event_source: e.eventSource ?? null,
|
|
1345
|
+
event_subtype: e.eventSubtype ?? null,
|
|
1346
|
+
page_path: e.pagePath ?? null,
|
|
1347
|
+
target_url_path: e.targetUrlPath ?? null,
|
|
1348
|
+
element_selector: e.elementSelector ?? null,
|
|
1349
|
+
element_text: e.elementText ?? null,
|
|
1350
|
+
scroll_depth_pct: e.scrollDepthPct ?? null,
|
|
1046
1351
|
user_id: e.userId ?? null,
|
|
1047
1352
|
traits: e.traits ?? null,
|
|
1048
1353
|
country: e.geo?.country ?? null,
|
|
@@ -1073,12 +1378,13 @@ var MongoDBAdapter = class {
|
|
|
1073
1378
|
site_id: siteId,
|
|
1074
1379
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1075
1380
|
};
|
|
1381
|
+
const filterMatch = buildFilterMatch(q.filters);
|
|
1076
1382
|
let data = [];
|
|
1077
1383
|
let total = 0;
|
|
1078
1384
|
switch (q.metric) {
|
|
1079
1385
|
case "pageviews": {
|
|
1080
1386
|
const [result2] = await this.collection.aggregate([
|
|
1081
|
-
{ $match: { ...baseMatch, type: "pageview" } },
|
|
1387
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview" } },
|
|
1082
1388
|
{ $count: "count" }
|
|
1083
1389
|
]).toArray();
|
|
1084
1390
|
total = result2?.count ?? 0;
|
|
@@ -1087,7 +1393,7 @@ var MongoDBAdapter = class {
|
|
|
1087
1393
|
}
|
|
1088
1394
|
case "visitors": {
|
|
1089
1395
|
const [result2] = await this.collection.aggregate([
|
|
1090
|
-
{ $match: baseMatch },
|
|
1396
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1091
1397
|
{ $group: { _id: "$visitor_id" } },
|
|
1092
1398
|
{ $count: "count" }
|
|
1093
1399
|
]).toArray();
|
|
@@ -1097,7 +1403,7 @@ var MongoDBAdapter = class {
|
|
|
1097
1403
|
}
|
|
1098
1404
|
case "sessions": {
|
|
1099
1405
|
const [result2] = await this.collection.aggregate([
|
|
1100
|
-
{ $match: baseMatch },
|
|
1406
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1101
1407
|
{ $group: { _id: "$session_id" } },
|
|
1102
1408
|
{ $count: "count" }
|
|
1103
1409
|
]).toArray();
|
|
@@ -1107,16 +1413,31 @@ var MongoDBAdapter = class {
|
|
|
1107
1413
|
}
|
|
1108
1414
|
case "events": {
|
|
1109
1415
|
const [result2] = await this.collection.aggregate([
|
|
1110
|
-
{ $match: { ...baseMatch, type: "event" } },
|
|
1416
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event" } },
|
|
1111
1417
|
{ $count: "count" }
|
|
1112
1418
|
]).toArray();
|
|
1113
1419
|
total = result2?.count ?? 0;
|
|
1114
1420
|
data = [{ key: "events", value: total }];
|
|
1115
1421
|
break;
|
|
1116
1422
|
}
|
|
1423
|
+
case "conversions": {
|
|
1424
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
1425
|
+
if (conversionEvents.length === 0) {
|
|
1426
|
+
total = 0;
|
|
1427
|
+
data = [{ key: "conversions", value: 0 }];
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
const [result2] = await this.collection.aggregate([
|
|
1431
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
|
|
1432
|
+
{ $count: "count" }
|
|
1433
|
+
]).toArray();
|
|
1434
|
+
total = result2?.count ?? 0;
|
|
1435
|
+
data = [{ key: "conversions", value: total }];
|
|
1436
|
+
break;
|
|
1437
|
+
}
|
|
1117
1438
|
case "top_pages": {
|
|
1118
1439
|
const rows = await this.collection.aggregate([
|
|
1119
|
-
{ $match: { ...baseMatch, type: "pageview", url: { $ne: null } } },
|
|
1440
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1120
1441
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1121
1442
|
{ $sort: { value: -1 } },
|
|
1122
1443
|
{ $limit: limit }
|
|
@@ -1127,7 +1448,7 @@ var MongoDBAdapter = class {
|
|
|
1127
1448
|
}
|
|
1128
1449
|
case "top_referrers": {
|
|
1129
1450
|
const rows = await this.collection.aggregate([
|
|
1130
|
-
{ $match: { ...baseMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
|
|
1451
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
|
|
1131
1452
|
{ $group: { _id: "$referrer", value: { $sum: 1 } } },
|
|
1132
1453
|
{ $sort: { value: -1 } },
|
|
1133
1454
|
{ $limit: limit }
|
|
@@ -1138,7 +1459,7 @@ var MongoDBAdapter = class {
|
|
|
1138
1459
|
}
|
|
1139
1460
|
case "top_countries": {
|
|
1140
1461
|
const rows = await this.collection.aggregate([
|
|
1141
|
-
{ $match: { ...baseMatch, country: { $ne: null } } },
|
|
1462
|
+
{ $match: { ...baseMatch, ...filterMatch, country: { $ne: null } } },
|
|
1142
1463
|
{ $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
|
|
1143
1464
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1144
1465
|
{ $sort: { value: -1 } },
|
|
@@ -1150,7 +1471,7 @@ var MongoDBAdapter = class {
|
|
|
1150
1471
|
}
|
|
1151
1472
|
case "top_cities": {
|
|
1152
1473
|
const rows = await this.collection.aggregate([
|
|
1153
|
-
{ $match: { ...baseMatch, city: { $ne: null } } },
|
|
1474
|
+
{ $match: { ...baseMatch, ...filterMatch, city: { $ne: null } } },
|
|
1154
1475
|
{ $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
|
|
1155
1476
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1156
1477
|
{ $sort: { value: -1 } },
|
|
@@ -1162,7 +1483,7 @@ var MongoDBAdapter = class {
|
|
|
1162
1483
|
}
|
|
1163
1484
|
case "top_events": {
|
|
1164
1485
|
const rows = await this.collection.aggregate([
|
|
1165
|
-
{ $match: { ...baseMatch, type: "event", event_name: { $ne: null } } },
|
|
1486
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $ne: null } } },
|
|
1166
1487
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1167
1488
|
{ $sort: { value: -1 } },
|
|
1168
1489
|
{ $limit: limit }
|
|
@@ -1171,9 +1492,109 @@ var MongoDBAdapter = class {
|
|
|
1171
1492
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1172
1493
|
break;
|
|
1173
1494
|
}
|
|
1495
|
+
case "top_conversions": {
|
|
1496
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
1497
|
+
if (conversionEvents.length === 0) {
|
|
1498
|
+
total = 0;
|
|
1499
|
+
data = [];
|
|
1500
|
+
break;
|
|
1501
|
+
}
|
|
1502
|
+
const rows = await this.collection.aggregate([
|
|
1503
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
|
|
1504
|
+
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1505
|
+
{ $sort: { value: -1 } },
|
|
1506
|
+
{ $limit: limit }
|
|
1507
|
+
]).toArray();
|
|
1508
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1509
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
case "top_exit_pages": {
|
|
1513
|
+
const rows = await this.collection.aggregate([
|
|
1514
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1515
|
+
{ $sort: { timestamp: 1 } },
|
|
1516
|
+
{ $group: { _id: "$session_id", url: { $last: "$url" } } },
|
|
1517
|
+
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1518
|
+
{ $sort: { value: -1 } },
|
|
1519
|
+
{ $limit: limit }
|
|
1520
|
+
]).toArray();
|
|
1521
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1522
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
case "top_transitions": {
|
|
1526
|
+
const rows = await this.collection.aggregate([
|
|
1527
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1528
|
+
{
|
|
1529
|
+
$setWindowFields: {
|
|
1530
|
+
partitionBy: "$session_id",
|
|
1531
|
+
sortBy: { timestamp: 1 },
|
|
1532
|
+
output: {
|
|
1533
|
+
prev_url: { $shift: { output: "$url", by: -1 } }
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1537
|
+
{ $match: { prev_url: { $ne: null } } },
|
|
1538
|
+
{ $group: { _id: { $concat: ["$prev_url", " \u2192 ", "$url"] }, value: { $sum: 1 } } },
|
|
1539
|
+
{ $sort: { value: -1 } },
|
|
1540
|
+
{ $limit: limit }
|
|
1541
|
+
]).toArray();
|
|
1542
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1543
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1544
|
+
break;
|
|
1545
|
+
}
|
|
1546
|
+
case "top_scroll_pages": {
|
|
1547
|
+
const rows = await this.collection.aggregate([
|
|
1548
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } } },
|
|
1549
|
+
{ $group: { _id: "$page_path", value: { $sum: 1 } } },
|
|
1550
|
+
{ $sort: { value: -1 } },
|
|
1551
|
+
{ $limit: limit }
|
|
1552
|
+
]).toArray();
|
|
1553
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1554
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1557
|
+
case "top_button_clicks": {
|
|
1558
|
+
const rows = await this.collection.aggregate([
|
|
1559
|
+
{
|
|
1560
|
+
$match: {
|
|
1561
|
+
...baseMatch,
|
|
1562
|
+
...filterMatch,
|
|
1563
|
+
type: "event",
|
|
1564
|
+
event_subtype: "button_click",
|
|
1565
|
+
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
1566
|
+
}
|
|
1567
|
+
},
|
|
1568
|
+
{ $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
|
|
1569
|
+
{ $sort: { value: -1 } },
|
|
1570
|
+
{ $limit: limit }
|
|
1571
|
+
]).toArray();
|
|
1572
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1573
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1576
|
+
case "top_link_targets": {
|
|
1577
|
+
const rows = await this.collection.aggregate([
|
|
1578
|
+
{
|
|
1579
|
+
$match: {
|
|
1580
|
+
...baseMatch,
|
|
1581
|
+
...filterMatch,
|
|
1582
|
+
type: "event",
|
|
1583
|
+
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
1584
|
+
target_url_path: { $ne: null }
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
{ $group: { _id: "$target_url_path", value: { $sum: 1 } } },
|
|
1588
|
+
{ $sort: { value: -1 } },
|
|
1589
|
+
{ $limit: limit }
|
|
1590
|
+
]).toArray();
|
|
1591
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1592
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1593
|
+
break;
|
|
1594
|
+
}
|
|
1174
1595
|
case "top_devices": {
|
|
1175
1596
|
const rows = await this.collection.aggregate([
|
|
1176
|
-
{ $match: { ...baseMatch, device_type: { $ne: null } } },
|
|
1597
|
+
{ $match: { ...baseMatch, ...filterMatch, device_type: { $ne: null } } },
|
|
1177
1598
|
{ $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
|
|
1178
1599
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1179
1600
|
{ $sort: { value: -1 } },
|
|
@@ -1185,7 +1606,7 @@ var MongoDBAdapter = class {
|
|
|
1185
1606
|
}
|
|
1186
1607
|
case "top_browsers": {
|
|
1187
1608
|
const rows = await this.collection.aggregate([
|
|
1188
|
-
{ $match: { ...baseMatch, browser: { $ne: null } } },
|
|
1609
|
+
{ $match: { ...baseMatch, ...filterMatch, browser: { $ne: null } } },
|
|
1189
1610
|
{ $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
|
|
1190
1611
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1191
1612
|
{ $sort: { value: -1 } },
|
|
@@ -1197,7 +1618,7 @@ var MongoDBAdapter = class {
|
|
|
1197
1618
|
}
|
|
1198
1619
|
case "top_os": {
|
|
1199
1620
|
const rows = await this.collection.aggregate([
|
|
1200
|
-
{ $match: { ...baseMatch, os: { $ne: null } } },
|
|
1621
|
+
{ $match: { ...baseMatch, ...filterMatch, os: { $ne: null } } },
|
|
1201
1622
|
{ $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
|
|
1202
1623
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1203
1624
|
{ $sort: { value: -1 } },
|
|
@@ -1209,7 +1630,7 @@ var MongoDBAdapter = class {
|
|
|
1209
1630
|
}
|
|
1210
1631
|
}
|
|
1211
1632
|
const result = { metric: q.metric, period, data, total };
|
|
1212
|
-
if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
|
|
1633
|
+
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
1213
1634
|
const prevRange = previousPeriodRange(dateRange);
|
|
1214
1635
|
const prevResult = await this.query({
|
|
1215
1636
|
...q,
|
|
@@ -1241,15 +1662,34 @@ var MongoDBAdapter = class {
|
|
|
1241
1662
|
site_id: params.siteId,
|
|
1242
1663
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1243
1664
|
};
|
|
1665
|
+
const filterMatch = buildFilterMatch(params.filters);
|
|
1244
1666
|
if (params.metric === "pageviews") {
|
|
1245
1667
|
baseMatch.type = "pageview";
|
|
1246
1668
|
}
|
|
1669
|
+
if (params.metric === "events") {
|
|
1670
|
+
baseMatch.type = "event";
|
|
1671
|
+
}
|
|
1672
|
+
if (params.metric === "conversions") {
|
|
1673
|
+
baseMatch.type = "event";
|
|
1674
|
+
const conversionEvents = params.conversionEvents ?? [];
|
|
1675
|
+
if (conversionEvents.length === 0) {
|
|
1676
|
+
const data2 = fillBuckets(
|
|
1677
|
+
new Date(dateRange.from),
|
|
1678
|
+
new Date(dateRange.to),
|
|
1679
|
+
granularity,
|
|
1680
|
+
granularityToDateFormat(granularity),
|
|
1681
|
+
[]
|
|
1682
|
+
);
|
|
1683
|
+
return { metric: params.metric, granularity, data: data2 };
|
|
1684
|
+
}
|
|
1685
|
+
baseMatch.event_name = { $in: conversionEvents };
|
|
1686
|
+
}
|
|
1247
1687
|
const dateFormat = granularityToDateFormat(granularity);
|
|
1248
1688
|
let pipeline;
|
|
1249
1689
|
if (params.metric === "visitors" || params.metric === "sessions") {
|
|
1250
1690
|
const groupField = params.metric === "visitors" ? "$visitor_id" : "$session_id";
|
|
1251
1691
|
pipeline = [
|
|
1252
|
-
{ $match: baseMatch },
|
|
1692
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1253
1693
|
{
|
|
1254
1694
|
$group: {
|
|
1255
1695
|
_id: {
|
|
@@ -1268,7 +1708,7 @@ var MongoDBAdapter = class {
|
|
|
1268
1708
|
];
|
|
1269
1709
|
} else {
|
|
1270
1710
|
pipeline = [
|
|
1271
|
-
{ $match: baseMatch },
|
|
1711
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1272
1712
|
{
|
|
1273
1713
|
$group: {
|
|
1274
1714
|
_id: { $dateToString: { format: dateFormat, date: "$timestamp" } },
|
|
@@ -1349,7 +1789,12 @@ var MongoDBAdapter = class {
|
|
|
1349
1789
|
const offset = params.offset ?? 0;
|
|
1350
1790
|
const match = { site_id: params.siteId };
|
|
1351
1791
|
if (params.type) match.type = params.type;
|
|
1352
|
-
if (params.eventName)
|
|
1792
|
+
if (params.eventName) {
|
|
1793
|
+
match.event_name = params.eventName;
|
|
1794
|
+
} else if (params.eventNames && params.eventNames.length > 0) {
|
|
1795
|
+
match.event_name = { $in: params.eventNames };
|
|
1796
|
+
}
|
|
1797
|
+
if (params.eventSource) match.event_source = params.eventSource;
|
|
1353
1798
|
if (params.visitorId) match.visitor_id = params.visitorId;
|
|
1354
1799
|
if (params.userId) match.user_id = params.userId;
|
|
1355
1800
|
if (params.period || params.dateFrom) {
|
|
@@ -1384,6 +1829,7 @@ var MongoDBAdapter = class {
|
|
|
1384
1829
|
}
|
|
1385
1830
|
const pipeline = [
|
|
1386
1831
|
{ $match: match },
|
|
1832
|
+
{ $sort: { timestamp: 1 } },
|
|
1387
1833
|
{
|
|
1388
1834
|
$group: {
|
|
1389
1835
|
_id: "$visitor_id",
|
|
@@ -1395,13 +1841,22 @@ var MongoDBAdapter = class {
|
|
|
1395
1841
|
totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
|
|
1396
1842
|
sessions: { $addToSet: "$session_id" },
|
|
1397
1843
|
lastUrl: { $last: "$url" },
|
|
1844
|
+
referrer: { $last: "$referrer" },
|
|
1398
1845
|
device_type: { $last: "$device_type" },
|
|
1399
1846
|
browser: { $last: "$browser" },
|
|
1400
1847
|
os: { $last: "$os" },
|
|
1401
1848
|
country: { $last: "$country" },
|
|
1402
1849
|
city: { $last: "$city" },
|
|
1403
1850
|
region: { $last: "$region" },
|
|
1404
|
-
language: { $last: "$language" }
|
|
1851
|
+
language: { $last: "$language" },
|
|
1852
|
+
timezone: { $last: "$timezone" },
|
|
1853
|
+
screen_width: { $last: "$screen_width" },
|
|
1854
|
+
screen_height: { $last: "$screen_height" },
|
|
1855
|
+
utm_source: { $last: "$utm_source" },
|
|
1856
|
+
utm_medium: { $last: "$utm_medium" },
|
|
1857
|
+
utm_campaign: { $last: "$utm_campaign" },
|
|
1858
|
+
utm_term: { $last: "$utm_term" },
|
|
1859
|
+
utm_content: { $last: "$utm_content" }
|
|
1405
1860
|
}
|
|
1406
1861
|
},
|
|
1407
1862
|
{ $sort: { lastSeen: -1 } },
|
|
@@ -1423,9 +1878,19 @@ var MongoDBAdapter = class {
|
|
|
1423
1878
|
totalPageviews: u.totalPageviews,
|
|
1424
1879
|
totalSessions: u.sessions.length,
|
|
1425
1880
|
lastUrl: u.lastUrl ?? void 0,
|
|
1881
|
+
referrer: u.referrer ?? void 0,
|
|
1426
1882
|
device: u.device_type ? { type: u.device_type, browser: u.browser ?? "", os: u.os ?? "" } : void 0,
|
|
1427
1883
|
geo: u.country ? { country: u.country, city: u.city ?? void 0, region: u.region ?? void 0 } : void 0,
|
|
1428
|
-
language: u.language ?? void 0
|
|
1884
|
+
language: u.language ?? void 0,
|
|
1885
|
+
timezone: u.timezone ?? void 0,
|
|
1886
|
+
screen: u.screen_width || u.screen_height ? { width: u.screen_width ?? 0, height: u.screen_height ?? 0 } : void 0,
|
|
1887
|
+
utm: u.utm_source ? {
|
|
1888
|
+
source: u.utm_source ?? void 0,
|
|
1889
|
+
medium: u.utm_medium ?? void 0,
|
|
1890
|
+
campaign: u.utm_campaign ?? void 0,
|
|
1891
|
+
term: u.utm_term ?? void 0,
|
|
1892
|
+
content: u.utm_content ?? void 0
|
|
1893
|
+
} : void 0
|
|
1429
1894
|
}));
|
|
1430
1895
|
return {
|
|
1431
1896
|
users,
|
|
@@ -1454,6 +1919,13 @@ var MongoDBAdapter = class {
|
|
|
1454
1919
|
title: doc.title ?? void 0,
|
|
1455
1920
|
name: doc.event_name ?? void 0,
|
|
1456
1921
|
properties: doc.properties ?? void 0,
|
|
1922
|
+
eventSource: doc.event_source ? doc.event_source : void 0,
|
|
1923
|
+
eventSubtype: doc.event_subtype ? doc.event_subtype : void 0,
|
|
1924
|
+
pagePath: doc.page_path ?? void 0,
|
|
1925
|
+
targetUrlPath: doc.target_url_path ?? void 0,
|
|
1926
|
+
elementSelector: doc.element_selector ?? void 0,
|
|
1927
|
+
elementText: doc.element_text ?? void 0,
|
|
1928
|
+
scrollDepthPct: doc.scroll_depth_pct ?? void 0,
|
|
1457
1929
|
userId: doc.user_id ?? void 0,
|
|
1458
1930
|
traits: doc.traits ?? void 0,
|
|
1459
1931
|
geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
|
|
@@ -1477,6 +1949,7 @@ var MongoDBAdapter = class {
|
|
|
1477
1949
|
name: data.name,
|
|
1478
1950
|
domain: data.domain ?? null,
|
|
1479
1951
|
allowed_origins: data.allowedOrigins ?? null,
|
|
1952
|
+
conversion_events: data.conversionEvents ?? null,
|
|
1480
1953
|
created_at: now,
|
|
1481
1954
|
updated_at: now
|
|
1482
1955
|
};
|
|
@@ -1500,6 +1973,7 @@ var MongoDBAdapter = class {
|
|
|
1500
1973
|
if (data.name !== void 0) updates.name = data.name;
|
|
1501
1974
|
if (data.domain !== void 0) updates.domain = data.domain || null;
|
|
1502
1975
|
if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
|
|
1976
|
+
if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
|
|
1503
1977
|
const result = await this.sites.findOneAndUpdate(
|
|
1504
1978
|
{ site_id: siteId },
|
|
1505
1979
|
{ $set: updates },
|
|
@@ -1530,6 +2004,7 @@ var MongoDBAdapter = class {
|
|
|
1530
2004
|
name: doc.name,
|
|
1531
2005
|
domain: doc.domain ?? void 0,
|
|
1532
2006
|
allowedOrigins: doc.allowed_origins ?? void 0,
|
|
2007
|
+
conversionEvents: doc.conversion_events ?? void 0,
|
|
1533
2008
|
createdAt: doc.created_at.toISOString(),
|
|
1534
2009
|
updatedAt: doc.updated_at.toISOString()
|
|
1535
2010
|
};
|
|
@@ -1700,6 +2175,63 @@ function resolveDeviceType(type) {
|
|
|
1700
2175
|
return "desktop";
|
|
1701
2176
|
}
|
|
1702
2177
|
|
|
2178
|
+
// src/botfilter.ts
|
|
2179
|
+
var BOT_PATTERNS = [
|
|
2180
|
+
// Headless browsers
|
|
2181
|
+
/HeadlessChrome/i,
|
|
2182
|
+
/PhantomJS/i,
|
|
2183
|
+
/Selenium/i,
|
|
2184
|
+
/Puppeteer/i,
|
|
2185
|
+
/Playwright/i,
|
|
2186
|
+
// Common bots
|
|
2187
|
+
/bot\b/i,
|
|
2188
|
+
/spider/i,
|
|
2189
|
+
/crawl/i,
|
|
2190
|
+
/slurp/i,
|
|
2191
|
+
/mediapartners/i,
|
|
2192
|
+
/facebookexternalhit/i,
|
|
2193
|
+
/Twitterbot/i,
|
|
2194
|
+
/LinkedInBot/i,
|
|
2195
|
+
/WhatsApp/i,
|
|
2196
|
+
/Discordbot/i,
|
|
2197
|
+
/TelegramBot/i,
|
|
2198
|
+
/Applebot/i,
|
|
2199
|
+
/Baiduspider/i,
|
|
2200
|
+
/YandexBot/i,
|
|
2201
|
+
/DuckDuckBot/i,
|
|
2202
|
+
/Sogou/i,
|
|
2203
|
+
/Exabot/i,
|
|
2204
|
+
/ia_archiver/i,
|
|
2205
|
+
// HTTP libraries & API tools
|
|
2206
|
+
/PostmanRuntime/i,
|
|
2207
|
+
/axios/i,
|
|
2208
|
+
/node-fetch/i,
|
|
2209
|
+
/python-requests/i,
|
|
2210
|
+
/Go-http-client/i,
|
|
2211
|
+
/Java\//i,
|
|
2212
|
+
/libwww-perl/i,
|
|
2213
|
+
/wget/i,
|
|
2214
|
+
/curl/i,
|
|
2215
|
+
/httpie/i,
|
|
2216
|
+
// Monitoring / uptime
|
|
2217
|
+
/UptimeRobot/i,
|
|
2218
|
+
/Pingdom/i,
|
|
2219
|
+
/StatusCake/i,
|
|
2220
|
+
/Site24x7/i,
|
|
2221
|
+
/NewRelic/i,
|
|
2222
|
+
/Datadog/i,
|
|
2223
|
+
// Preview/embed
|
|
2224
|
+
/Slackbot/i,
|
|
2225
|
+
/Embedly/i,
|
|
2226
|
+
/Quora Link Preview/i,
|
|
2227
|
+
/redditbot/i,
|
|
2228
|
+
/Pinterestbot/i
|
|
2229
|
+
];
|
|
2230
|
+
function isBot(ua) {
|
|
2231
|
+
if (!ua || ua.length === 0) return true;
|
|
2232
|
+
return BOT_PATTERNS.some((re) => re.test(ua));
|
|
2233
|
+
}
|
|
2234
|
+
|
|
1703
2235
|
// src/collector.ts
|
|
1704
2236
|
async function createCollector(config) {
|
|
1705
2237
|
const db = createAdapter(config.db);
|
|
@@ -1726,6 +2258,7 @@ async function createCollector(config) {
|
|
|
1726
2258
|
if (allowed) {
|
|
1727
2259
|
res.setHeader?.("Access-Control-Allow-Origin", origin || "*");
|
|
1728
2260
|
res.setHeader?.("Access-Control-Allow-Methods", methods);
|
|
2261
|
+
res.setHeader?.("Access-Control-Allow-Credentials", "true");
|
|
1729
2262
|
const headers = ["Content-Type", extraHeaders].filter(Boolean).join(", ");
|
|
1730
2263
|
res.setHeader?.("Access-Control-Allow-Headers", headers);
|
|
1731
2264
|
}
|
|
@@ -1756,7 +2289,14 @@ async function createCollector(config) {
|
|
|
1756
2289
|
}
|
|
1757
2290
|
function handler() {
|
|
1758
2291
|
return async (req, res) => {
|
|
1759
|
-
|
|
2292
|
+
res.setHeader?.("Access-Control-Allow-Origin", "*");
|
|
2293
|
+
res.setHeader?.("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
2294
|
+
res.setHeader?.("Access-Control-Allow-Headers", "Content-Type");
|
|
2295
|
+
if (req.method === "OPTIONS") {
|
|
2296
|
+
res.writeHead?.(204);
|
|
2297
|
+
res.end?.();
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
1760
2300
|
if (req.method !== "POST") {
|
|
1761
2301
|
sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
1762
2302
|
return;
|
|
@@ -1772,9 +2312,36 @@ async function createCollector(config) {
|
|
|
1772
2312
|
sendJson(res, 400, { ok: false, error: "Too many events (max 100)" });
|
|
1773
2313
|
return;
|
|
1774
2314
|
}
|
|
1775
|
-
const ip = extractIp(req);
|
|
1776
2315
|
const userAgent = req.headers?.["user-agent"] || "";
|
|
2316
|
+
if (isBot(userAgent)) {
|
|
2317
|
+
sendJson(res, 200, { ok: true });
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
const ip = extractIp(req);
|
|
1777
2321
|
const enriched = enrichEvents(payload.events, ip, userAgent);
|
|
2322
|
+
const siteId = enriched[0]?.siteId;
|
|
2323
|
+
if (siteId) {
|
|
2324
|
+
const site = await db.getSite(siteId);
|
|
2325
|
+
if (site?.allowedOrigins && site.allowedOrigins.length > 0) {
|
|
2326
|
+
const allowed = new Set(site.allowedOrigins.map((h) => h.toLowerCase()));
|
|
2327
|
+
const filtered = enriched.filter((event) => {
|
|
2328
|
+
if (!event.url) return true;
|
|
2329
|
+
try {
|
|
2330
|
+
const hostname = new URL(event.url).hostname.toLowerCase();
|
|
2331
|
+
return allowed.has(hostname);
|
|
2332
|
+
} catch {
|
|
2333
|
+
return true;
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
if (filtered.length === 0) {
|
|
2337
|
+
sendJson(res, 200, { ok: true });
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
await db.insertEvents(filtered);
|
|
2341
|
+
sendJson(res, 200, { ok: true });
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
1778
2345
|
await db.insertEvents(enriched);
|
|
1779
2346
|
sendJson(res, 200, { ok: true });
|
|
1780
2347
|
} catch (err) {
|
|
@@ -1808,8 +2375,13 @@ async function createCollector(config) {
|
|
|
1808
2375
|
period: params.period,
|
|
1809
2376
|
dateFrom: params.dateFrom,
|
|
1810
2377
|
dateTo: params.dateTo,
|
|
1811
|
-
granularity: q.granularity
|
|
2378
|
+
granularity: q.granularity,
|
|
2379
|
+
filters: q.filters ? JSON.parse(q.filters) : void 0
|
|
1812
2380
|
};
|
|
2381
|
+
if (tsParams.metric === "conversions") {
|
|
2382
|
+
const site = await db.getSite(params.siteId);
|
|
2383
|
+
tsParams.conversionEvents = site?.conversionEvents ?? [];
|
|
2384
|
+
}
|
|
1813
2385
|
const result2 = await db.queryTimeSeries(tsParams);
|
|
1814
2386
|
sendJson(res, 200, result2);
|
|
1815
2387
|
return;
|
|
@@ -1825,7 +2397,15 @@ async function createCollector(config) {
|
|
|
1825
2397
|
sendJson(res, 200, result2);
|
|
1826
2398
|
return;
|
|
1827
2399
|
}
|
|
1828
|
-
const
|
|
2400
|
+
const isConversionMetric = params.metric === "conversions" || params.metric === "top_conversions";
|
|
2401
|
+
let result;
|
|
2402
|
+
if (isConversionMetric) {
|
|
2403
|
+
const site = await db.getSite(params.siteId);
|
|
2404
|
+
const conversionEvents = site?.conversionEvents ?? [];
|
|
2405
|
+
result = await db.query({ ...params, conversionEvents });
|
|
2406
|
+
} else {
|
|
2407
|
+
result = await db.query(params);
|
|
2408
|
+
}
|
|
1829
2409
|
sendJson(res, 200, result);
|
|
1830
2410
|
} catch (err) {
|
|
1831
2411
|
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : "Internal error" });
|
|
@@ -1922,10 +2502,13 @@ async function createCollector(config) {
|
|
|
1922
2502
|
sendJson(res, 401, { ok: false, error: "Invalid or missing secret key" });
|
|
1923
2503
|
return;
|
|
1924
2504
|
}
|
|
2505
|
+
const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1925
2506
|
const params = {
|
|
1926
2507
|
siteId: q.siteId,
|
|
1927
2508
|
type: q.type,
|
|
1928
2509
|
eventName: q.eventName,
|
|
2510
|
+
eventNames,
|
|
2511
|
+
eventSource: q.eventSource,
|
|
1929
2512
|
visitorId: q.visitorId,
|
|
1930
2513
|
userId: q.userId,
|
|
1931
2514
|
period: q.period,
|
|
@@ -1965,9 +2548,13 @@ async function createCollector(config) {
|
|
|
1965
2548
|
const visitorId = usersIdx >= 0 ? pathSegments[usersIdx + 1] : void 0;
|
|
1966
2549
|
const action = usersIdx >= 0 ? pathSegments[usersIdx + 2] : void 0;
|
|
1967
2550
|
if (visitorId && action === "events") {
|
|
2551
|
+
const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1968
2552
|
const params2 = {
|
|
1969
2553
|
siteId: q.siteId,
|
|
1970
2554
|
type: q.type,
|
|
2555
|
+
eventName: q.eventName,
|
|
2556
|
+
eventNames,
|
|
2557
|
+
eventSource: q.eventSource,
|
|
1971
2558
|
period: q.period,
|
|
1972
2559
|
dateFrom: q.dateFrom,
|
|
1973
2560
|
dateTo: q.dateTo,
|
|
@@ -2074,7 +2661,8 @@ function createAdapter(config) {
|
|
|
2074
2661
|
}
|
|
2075
2662
|
}
|
|
2076
2663
|
async function parseBody(req) {
|
|
2077
|
-
if (req.body) return req.body;
|
|
2664
|
+
if (req.body && typeof req.body === "object") return req.body;
|
|
2665
|
+
if (typeof req.body === "string") return JSON.parse(req.body);
|
|
2078
2666
|
return new Promise((resolve, reject) => {
|
|
2079
2667
|
let data = "";
|
|
2080
2668
|
req.on("data", (chunk) => {
|
|
@@ -2115,6 +2703,7 @@ function sendJson(res, status, body) {
|
|
|
2115
2703
|
0 && (module.exports = {
|
|
2116
2704
|
ClickHouseAdapter,
|
|
2117
2705
|
MongoDBAdapter,
|
|
2118
|
-
createCollector
|
|
2706
|
+
createCollector,
|
|
2707
|
+
isBot
|
|
2119
2708
|
});
|
|
2120
2709
|
//# sourceMappingURL=index.cjs.map
|