@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.js
CHANGED
|
@@ -153,6 +153,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
|
153
153
|
title Nullable(String),
|
|
154
154
|
event_name Nullable(String),
|
|
155
155
|
properties Nullable(String),
|
|
156
|
+
event_source LowCardinality(Nullable(String)),
|
|
157
|
+
event_subtype LowCardinality(Nullable(String)),
|
|
158
|
+
page_path Nullable(String),
|
|
159
|
+
target_url_path Nullable(String),
|
|
160
|
+
element_selector Nullable(String),
|
|
161
|
+
element_text Nullable(String),
|
|
162
|
+
scroll_depth_pct Nullable(UInt8),
|
|
156
163
|
user_id Nullable(String),
|
|
157
164
|
traits Nullable(String),
|
|
158
165
|
country LowCardinality(Nullable(String)),
|
|
@@ -184,6 +191,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
184
191
|
name String,
|
|
185
192
|
domain Nullable(String),
|
|
186
193
|
allowed_origins Nullable(String),
|
|
194
|
+
conversion_events Nullable(String),
|
|
187
195
|
created_at DateTime64(3),
|
|
188
196
|
updated_at DateTime64(3),
|
|
189
197
|
version UInt64,
|
|
@@ -192,6 +200,43 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
192
200
|
ORDER BY (site_id)
|
|
193
201
|
SETTINGS index_granularity = 8192
|
|
194
202
|
`;
|
|
203
|
+
function toCHDateTime(d) {
|
|
204
|
+
const iso = typeof d === "string" ? d : d.toISOString();
|
|
205
|
+
return iso.replace("T", " ").replace("Z", "");
|
|
206
|
+
}
|
|
207
|
+
function buildFilterConditions(filters) {
|
|
208
|
+
if (!filters) return { conditions: [], params: {} };
|
|
209
|
+
const map = {
|
|
210
|
+
"geo.country": "country",
|
|
211
|
+
"geo.city": "city",
|
|
212
|
+
"geo.region": "region",
|
|
213
|
+
"language": "language",
|
|
214
|
+
"device.type": "device_type",
|
|
215
|
+
"device.browser": "browser",
|
|
216
|
+
"device.os": "os",
|
|
217
|
+
"utm.source": "utm_source",
|
|
218
|
+
"utm.medium": "utm_medium",
|
|
219
|
+
"utm.campaign": "utm_campaign",
|
|
220
|
+
"utm.term": "utm_term",
|
|
221
|
+
"utm.content": "utm_content",
|
|
222
|
+
"referrer": "referrer",
|
|
223
|
+
"event_source": "event_source",
|
|
224
|
+
"event_subtype": "event_subtype",
|
|
225
|
+
"page_path": "page_path",
|
|
226
|
+
"target_url_path": "target_url_path",
|
|
227
|
+
"event_name": "event_name",
|
|
228
|
+
"type": "type"
|
|
229
|
+
};
|
|
230
|
+
const conditions = [];
|
|
231
|
+
const params = {};
|
|
232
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
233
|
+
if (!value || !map[key]) continue;
|
|
234
|
+
const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
235
|
+
conditions.push(`${map[key]} = {${paramKey}:String}`);
|
|
236
|
+
params[paramKey] = value;
|
|
237
|
+
}
|
|
238
|
+
return { conditions, params };
|
|
239
|
+
}
|
|
195
240
|
var ClickHouseAdapter = class {
|
|
196
241
|
client;
|
|
197
242
|
constructor(url) {
|
|
@@ -205,6 +250,16 @@ var ClickHouseAdapter = class {
|
|
|
205
250
|
async init() {
|
|
206
251
|
await this.client.command({ query: CREATE_EVENTS_TABLE });
|
|
207
252
|
await this.client.command({ query: CREATE_SITES_TABLE });
|
|
253
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_source LowCardinality(Nullable(String))` });
|
|
254
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_subtype LowCardinality(Nullable(String))` });
|
|
255
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS page_path Nullable(String)` });
|
|
256
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS target_url_path Nullable(String)` });
|
|
257
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_selector Nullable(String)` });
|
|
258
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS element_text Nullable(String)` });
|
|
259
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS scroll_depth_pct Nullable(UInt8)` });
|
|
260
|
+
await this.client.command({
|
|
261
|
+
query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
|
|
262
|
+
});
|
|
208
263
|
}
|
|
209
264
|
async close() {
|
|
210
265
|
await this.client.close();
|
|
@@ -215,7 +270,7 @@ var ClickHouseAdapter = class {
|
|
|
215
270
|
const rows = events.map((e) => ({
|
|
216
271
|
site_id: e.siteId,
|
|
217
272
|
type: e.type,
|
|
218
|
-
timestamp: new Date(e.timestamp)
|
|
273
|
+
timestamp: toCHDateTime(new Date(e.timestamp)),
|
|
219
274
|
session_id: e.sessionId,
|
|
220
275
|
visitor_id: e.visitorId,
|
|
221
276
|
url: e.url ?? null,
|
|
@@ -223,6 +278,13 @@ var ClickHouseAdapter = class {
|
|
|
223
278
|
title: e.title ?? null,
|
|
224
279
|
event_name: e.name ?? null,
|
|
225
280
|
properties: e.properties ? JSON.stringify(e.properties) : null,
|
|
281
|
+
event_source: e.eventSource ?? null,
|
|
282
|
+
event_subtype: e.eventSubtype ?? null,
|
|
283
|
+
page_path: e.pagePath ?? null,
|
|
284
|
+
target_url_path: e.targetUrlPath ?? null,
|
|
285
|
+
element_selector: e.elementSelector ?? null,
|
|
286
|
+
element_text: e.elementText ?? null,
|
|
287
|
+
scroll_depth_pct: e.scrollDepthPct ?? null,
|
|
226
288
|
user_id: e.userId ?? null,
|
|
227
289
|
traits: e.traits ? JSON.stringify(e.traits) : null,
|
|
228
290
|
country: e.geo?.country ?? null,
|
|
@@ -255,10 +317,12 @@ var ClickHouseAdapter = class {
|
|
|
255
317
|
const limit = q.limit ?? 10;
|
|
256
318
|
const params = {
|
|
257
319
|
siteId,
|
|
258
|
-
from: dateRange.from,
|
|
259
|
-
to: dateRange.to,
|
|
320
|
+
from: toCHDateTime(dateRange.from),
|
|
321
|
+
to: toCHDateTime(dateRange.to),
|
|
260
322
|
limit
|
|
261
323
|
};
|
|
324
|
+
const filter = buildFilterConditions(q.filters);
|
|
325
|
+
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
262
326
|
let data = [];
|
|
263
327
|
let total = 0;
|
|
264
328
|
switch (q.metric) {
|
|
@@ -268,8 +332,8 @@ var ClickHouseAdapter = class {
|
|
|
268
332
|
WHERE site_id = {siteId:String}
|
|
269
333
|
AND timestamp >= {from:String}
|
|
270
334
|
AND timestamp <= {to:String}
|
|
271
|
-
AND type = 'pageview'`,
|
|
272
|
-
params
|
|
335
|
+
AND type = 'pageview'${filterSql}`,
|
|
336
|
+
{ ...params, ...filter.params }
|
|
273
337
|
);
|
|
274
338
|
total = Number(rows[0]?.value ?? 0);
|
|
275
339
|
data = [{ key: "pageviews", value: total }];
|
|
@@ -280,8 +344,8 @@ var ClickHouseAdapter = class {
|
|
|
280
344
|
`SELECT uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
281
345
|
WHERE site_id = {siteId:String}
|
|
282
346
|
AND timestamp >= {from:String}
|
|
283
|
-
AND timestamp <= {to:String}`,
|
|
284
|
-
params
|
|
347
|
+
AND timestamp <= {to:String}${filterSql}`,
|
|
348
|
+
{ ...params, ...filter.params }
|
|
285
349
|
);
|
|
286
350
|
total = Number(rows[0]?.value ?? 0);
|
|
287
351
|
data = [{ key: "visitors", value: total }];
|
|
@@ -292,8 +356,8 @@ var ClickHouseAdapter = class {
|
|
|
292
356
|
`SELECT uniq(session_id) AS value FROM ${EVENTS_TABLE}
|
|
293
357
|
WHERE site_id = {siteId:String}
|
|
294
358
|
AND timestamp >= {from:String}
|
|
295
|
-
AND timestamp <= {to:String}`,
|
|
296
|
-
params
|
|
359
|
+
AND timestamp <= {to:String}${filterSql}`,
|
|
360
|
+
{ ...params, ...filter.params }
|
|
297
361
|
);
|
|
298
362
|
total = Number(rows[0]?.value ?? 0);
|
|
299
363
|
data = [{ key: "sessions", value: total }];
|
|
@@ -305,13 +369,33 @@ var ClickHouseAdapter = class {
|
|
|
305
369
|
WHERE site_id = {siteId:String}
|
|
306
370
|
AND timestamp >= {from:String}
|
|
307
371
|
AND timestamp <= {to:String}
|
|
308
|
-
AND type = 'event'`,
|
|
309
|
-
params
|
|
372
|
+
AND type = 'event'${filterSql}`,
|
|
373
|
+
{ ...params, ...filter.params }
|
|
310
374
|
);
|
|
311
375
|
total = Number(rows[0]?.value ?? 0);
|
|
312
376
|
data = [{ key: "events", value: total }];
|
|
313
377
|
break;
|
|
314
378
|
}
|
|
379
|
+
case "conversions": {
|
|
380
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
381
|
+
if (conversionEvents.length === 0) {
|
|
382
|
+
total = 0;
|
|
383
|
+
data = [{ key: "conversions", value: 0 }];
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
const rows = await this.queryRows(
|
|
387
|
+
`SELECT count() AS value FROM ${EVENTS_TABLE}
|
|
388
|
+
WHERE site_id = {siteId:String}
|
|
389
|
+
AND timestamp >= {from:String}
|
|
390
|
+
AND timestamp <= {to:String}
|
|
391
|
+
AND type = 'event'
|
|
392
|
+
AND event_name IN {eventNames:Array(String)}${filterSql}`,
|
|
393
|
+
{ ...params, eventNames: conversionEvents, ...filter.params }
|
|
394
|
+
);
|
|
395
|
+
total = Number(rows[0]?.value ?? 0);
|
|
396
|
+
data = [{ key: "conversions", value: total }];
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
315
399
|
case "top_pages": {
|
|
316
400
|
const rows = await this.queryRows(
|
|
317
401
|
`SELECT url AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
@@ -319,11 +403,11 @@ var ClickHouseAdapter = class {
|
|
|
319
403
|
AND timestamp >= {from:String}
|
|
320
404
|
AND timestamp <= {to:String}
|
|
321
405
|
AND type = 'pageview'
|
|
322
|
-
AND url IS NOT NULL
|
|
406
|
+
AND url IS NOT NULL${filterSql}
|
|
323
407
|
GROUP BY url
|
|
324
408
|
ORDER BY value DESC
|
|
325
409
|
LIMIT {limit:UInt32}`,
|
|
326
|
-
params
|
|
410
|
+
{ ...params, ...filter.params }
|
|
327
411
|
);
|
|
328
412
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
329
413
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -337,11 +421,11 @@ var ClickHouseAdapter = class {
|
|
|
337
421
|
AND timestamp <= {to:String}
|
|
338
422
|
AND type = 'pageview'
|
|
339
423
|
AND referrer IS NOT NULL
|
|
340
|
-
AND referrer != ''
|
|
424
|
+
AND referrer != ''${filterSql}
|
|
341
425
|
GROUP BY referrer
|
|
342
426
|
ORDER BY value DESC
|
|
343
427
|
LIMIT {limit:UInt32}`,
|
|
344
|
-
params
|
|
428
|
+
{ ...params, ...filter.params }
|
|
345
429
|
);
|
|
346
430
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
347
431
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -353,11 +437,11 @@ var ClickHouseAdapter = class {
|
|
|
353
437
|
WHERE site_id = {siteId:String}
|
|
354
438
|
AND timestamp >= {from:String}
|
|
355
439
|
AND timestamp <= {to:String}
|
|
356
|
-
AND country IS NOT NULL
|
|
440
|
+
AND country IS NOT NULL${filterSql}
|
|
357
441
|
GROUP BY country
|
|
358
442
|
ORDER BY value DESC
|
|
359
443
|
LIMIT {limit:UInt32}`,
|
|
360
|
-
params
|
|
444
|
+
{ ...params, ...filter.params }
|
|
361
445
|
);
|
|
362
446
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
363
447
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -369,11 +453,11 @@ var ClickHouseAdapter = class {
|
|
|
369
453
|
WHERE site_id = {siteId:String}
|
|
370
454
|
AND timestamp >= {from:String}
|
|
371
455
|
AND timestamp <= {to:String}
|
|
372
|
-
AND city IS NOT NULL
|
|
456
|
+
AND city IS NOT NULL${filterSql}
|
|
373
457
|
GROUP BY city
|
|
374
458
|
ORDER BY value DESC
|
|
375
459
|
LIMIT {limit:UInt32}`,
|
|
376
|
-
params
|
|
460
|
+
{ ...params, ...filter.params }
|
|
377
461
|
);
|
|
378
462
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
379
463
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -387,10 +471,132 @@ var ClickHouseAdapter = class {
|
|
|
387
471
|
AND timestamp <= {to:String}
|
|
388
472
|
AND type = 'event'
|
|
389
473
|
AND event_name IS NOT NULL
|
|
474
|
+
${filterSql}
|
|
475
|
+
GROUP BY event_name
|
|
476
|
+
ORDER BY value DESC
|
|
477
|
+
LIMIT {limit:UInt32}`,
|
|
478
|
+
{ ...params, ...filter.params }
|
|
479
|
+
);
|
|
480
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
481
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
case "top_conversions": {
|
|
485
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
486
|
+
if (conversionEvents.length === 0) {
|
|
487
|
+
total = 0;
|
|
488
|
+
data = [];
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
const rows = await this.queryRows(
|
|
492
|
+
`SELECT event_name AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
493
|
+
WHERE site_id = {siteId:String}
|
|
494
|
+
AND timestamp >= {from:String}
|
|
495
|
+
AND timestamp <= {to:String}
|
|
496
|
+
AND type = 'event'
|
|
497
|
+
AND event_name IN {eventNames:Array(String)}
|
|
498
|
+
${filterSql}
|
|
390
499
|
GROUP BY event_name
|
|
391
500
|
ORDER BY value DESC
|
|
392
501
|
LIMIT {limit:UInt32}`,
|
|
393
|
-
params
|
|
502
|
+
{ ...params, eventNames: conversionEvents, ...filter.params }
|
|
503
|
+
);
|
|
504
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
505
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
case "top_exit_pages": {
|
|
509
|
+
const rows = await this.queryRows(
|
|
510
|
+
`SELECT url AS key, count() AS value FROM (
|
|
511
|
+
SELECT session_id, argMax(url, timestamp) AS url
|
|
512
|
+
FROM ${EVENTS_TABLE}
|
|
513
|
+
WHERE site_id = {siteId:String}
|
|
514
|
+
AND timestamp >= {from:String}
|
|
515
|
+
AND timestamp <= {to:String}
|
|
516
|
+
AND type = 'pageview'
|
|
517
|
+
AND url IS NOT NULL${filterSql}
|
|
518
|
+
GROUP BY session_id
|
|
519
|
+
)
|
|
520
|
+
GROUP BY url
|
|
521
|
+
ORDER BY value DESC
|
|
522
|
+
LIMIT {limit:UInt32}`,
|
|
523
|
+
{ ...params, ...filter.params }
|
|
524
|
+
);
|
|
525
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
526
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case "top_transitions": {
|
|
530
|
+
const rows = await this.queryRows(
|
|
531
|
+
`SELECT concat(prev_url, ' \u2192 ', url) AS key, count() AS value FROM (
|
|
532
|
+
SELECT session_id, url,
|
|
533
|
+
lag(url) OVER (PARTITION BY session_id ORDER BY timestamp) AS prev_url
|
|
534
|
+
FROM ${EVENTS_TABLE}
|
|
535
|
+
WHERE site_id = {siteId:String}
|
|
536
|
+
AND timestamp >= {from:String}
|
|
537
|
+
AND timestamp <= {to:String}
|
|
538
|
+
AND type = 'pageview'
|
|
539
|
+
AND url IS NOT NULL${filterSql}
|
|
540
|
+
)
|
|
541
|
+
WHERE prev_url IS NOT NULL
|
|
542
|
+
GROUP BY key
|
|
543
|
+
ORDER BY value DESC
|
|
544
|
+
LIMIT {limit:UInt32}`,
|
|
545
|
+
{ ...params, ...filter.params }
|
|
546
|
+
);
|
|
547
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
548
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case "top_scroll_pages": {
|
|
552
|
+
const rows = await this.queryRows(
|
|
553
|
+
`SELECT page_path AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
554
|
+
WHERE site_id = {siteId:String}
|
|
555
|
+
AND timestamp >= {from:String}
|
|
556
|
+
AND timestamp <= {to:String}
|
|
557
|
+
AND type = 'event'
|
|
558
|
+
AND event_subtype = 'scroll_depth'
|
|
559
|
+
AND page_path IS NOT NULL${filterSql}
|
|
560
|
+
GROUP BY page_path
|
|
561
|
+
ORDER BY value DESC
|
|
562
|
+
LIMIT {limit:UInt32}`,
|
|
563
|
+
{ ...params, ...filter.params }
|
|
564
|
+
);
|
|
565
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
566
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
case "top_button_clicks": {
|
|
570
|
+
const rows = await this.queryRows(
|
|
571
|
+
`SELECT ifNull(element_text, element_selector) AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
572
|
+
WHERE site_id = {siteId:String}
|
|
573
|
+
AND timestamp >= {from:String}
|
|
574
|
+
AND timestamp <= {to:String}
|
|
575
|
+
AND type = 'event'
|
|
576
|
+
AND event_subtype = 'button_click'
|
|
577
|
+
AND (element_text IS NOT NULL OR element_selector IS NOT NULL)${filterSql}
|
|
578
|
+
GROUP BY key
|
|
579
|
+
ORDER BY value DESC
|
|
580
|
+
LIMIT {limit:UInt32}`,
|
|
581
|
+
{ ...params, ...filter.params }
|
|
582
|
+
);
|
|
583
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
584
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case "top_link_targets": {
|
|
588
|
+
const rows = await this.queryRows(
|
|
589
|
+
`SELECT target_url_path AS key, count() AS value FROM ${EVENTS_TABLE}
|
|
590
|
+
WHERE site_id = {siteId:String}
|
|
591
|
+
AND timestamp >= {from:String}
|
|
592
|
+
AND timestamp <= {to:String}
|
|
593
|
+
AND type = 'event'
|
|
594
|
+
AND event_subtype IN ('link_click','outbound_click')
|
|
595
|
+
AND target_url_path IS NOT NULL${filterSql}
|
|
596
|
+
GROUP BY target_url_path
|
|
597
|
+
ORDER BY value DESC
|
|
598
|
+
LIMIT {limit:UInt32}`,
|
|
599
|
+
{ ...params, ...filter.params }
|
|
394
600
|
);
|
|
395
601
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
396
602
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -403,10 +609,11 @@ var ClickHouseAdapter = class {
|
|
|
403
609
|
AND timestamp >= {from:String}
|
|
404
610
|
AND timestamp <= {to:String}
|
|
405
611
|
AND device_type IS NOT NULL
|
|
612
|
+
${filterSql}
|
|
406
613
|
GROUP BY device_type
|
|
407
614
|
ORDER BY value DESC
|
|
408
615
|
LIMIT {limit:UInt32}`,
|
|
409
|
-
params
|
|
616
|
+
{ ...params, ...filter.params }
|
|
410
617
|
);
|
|
411
618
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
412
619
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -419,10 +626,11 @@ var ClickHouseAdapter = class {
|
|
|
419
626
|
AND timestamp >= {from:String}
|
|
420
627
|
AND timestamp <= {to:String}
|
|
421
628
|
AND browser IS NOT NULL
|
|
629
|
+
${filterSql}
|
|
422
630
|
GROUP BY browser
|
|
423
631
|
ORDER BY value DESC
|
|
424
632
|
LIMIT {limit:UInt32}`,
|
|
425
|
-
params
|
|
633
|
+
{ ...params, ...filter.params }
|
|
426
634
|
);
|
|
427
635
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
428
636
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -435,10 +643,11 @@ var ClickHouseAdapter = class {
|
|
|
435
643
|
AND timestamp >= {from:String}
|
|
436
644
|
AND timestamp <= {to:String}
|
|
437
645
|
AND os IS NOT NULL
|
|
646
|
+
${filterSql}
|
|
438
647
|
GROUP BY os
|
|
439
648
|
ORDER BY value DESC
|
|
440
649
|
LIMIT {limit:UInt32}`,
|
|
441
|
-
params
|
|
650
|
+
{ ...params, ...filter.params }
|
|
442
651
|
);
|
|
443
652
|
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
444
653
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
@@ -446,7 +655,7 @@ var ClickHouseAdapter = class {
|
|
|
446
655
|
}
|
|
447
656
|
}
|
|
448
657
|
const result = { metric: q.metric, period, data, total };
|
|
449
|
-
if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
|
|
658
|
+
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
450
659
|
const prevRange = previousPeriodRange(dateRange);
|
|
451
660
|
const prevResult = await this.query({
|
|
452
661
|
...q,
|
|
@@ -476,7 +685,12 @@ var ClickHouseAdapter = class {
|
|
|
476
685
|
const granularity = params.granularity ?? autoGranularity(period);
|
|
477
686
|
const bucketFn = this.granularityToClickHouseFunc(granularity);
|
|
478
687
|
const dateFormat = granularityToDateFormat(granularity);
|
|
688
|
+
const filter = buildFilterConditions(params.filters);
|
|
689
|
+
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
479
690
|
const typeFilter = params.metric === "pageviews" ? `AND type = 'pageview'` : "";
|
|
691
|
+
const eventsFilter = params.metric === "events" ? `AND type = 'event'` : "";
|
|
692
|
+
const conversionsFilter = params.metric === "conversions" ? `AND type = 'event' AND event_name IN {eventNames:Array(String)}` : "";
|
|
693
|
+
const extraFilters = [typeFilter, eventsFilter, conversionsFilter, filterSql].filter(Boolean).join(" ");
|
|
480
694
|
let sql;
|
|
481
695
|
if (params.metric === "visitors" || params.metric === "sessions") {
|
|
482
696
|
const field = params.metric === "visitors" ? "visitor_id" : "session_id";
|
|
@@ -486,7 +700,7 @@ var ClickHouseAdapter = class {
|
|
|
486
700
|
WHERE site_id = {siteId:String}
|
|
487
701
|
AND timestamp >= {from:String}
|
|
488
702
|
AND timestamp <= {to:String}
|
|
489
|
-
${
|
|
703
|
+
${extraFilters}
|
|
490
704
|
GROUP BY bucket
|
|
491
705
|
ORDER BY bucket ASC
|
|
492
706
|
`;
|
|
@@ -497,15 +711,17 @@ var ClickHouseAdapter = class {
|
|
|
497
711
|
WHERE site_id = {siteId:String}
|
|
498
712
|
AND timestamp >= {from:String}
|
|
499
713
|
AND timestamp <= {to:String}
|
|
500
|
-
${
|
|
714
|
+
${extraFilters}
|
|
501
715
|
GROUP BY bucket
|
|
502
716
|
ORDER BY bucket ASC
|
|
503
717
|
`;
|
|
504
718
|
}
|
|
505
719
|
const rows = await this.queryRows(sql, {
|
|
506
720
|
siteId: params.siteId,
|
|
507
|
-
from: dateRange.from,
|
|
508
|
-
to: dateRange.to
|
|
721
|
+
from: toCHDateTime(dateRange.from),
|
|
722
|
+
to: toCHDateTime(dateRange.to),
|
|
723
|
+
eventNames: params.conversionEvents ?? [],
|
|
724
|
+
...filter.params
|
|
509
725
|
});
|
|
510
726
|
const mappedRows = rows.map((r) => ({
|
|
511
727
|
_id: this.convertClickHouseBucket(r.bucket, granularity),
|
|
@@ -571,7 +787,7 @@ var ClickHouseAdapter = class {
|
|
|
571
787
|
GROUP BY visitor_id`,
|
|
572
788
|
{
|
|
573
789
|
siteId: params.siteId,
|
|
574
|
-
since: startDate
|
|
790
|
+
since: toCHDateTime(startDate)
|
|
575
791
|
}
|
|
576
792
|
);
|
|
577
793
|
const cohortMap = /* @__PURE__ */ new Map();
|
|
@@ -623,6 +839,14 @@ var ClickHouseAdapter = class {
|
|
|
623
839
|
conditions.push(`event_name = {eventName:String}`);
|
|
624
840
|
queryParams.eventName = params.eventName;
|
|
625
841
|
}
|
|
842
|
+
if (params.eventSource) {
|
|
843
|
+
conditions.push(`event_source = {eventSource:String}`);
|
|
844
|
+
queryParams.eventSource = params.eventSource;
|
|
845
|
+
}
|
|
846
|
+
if (params.eventNames && params.eventNames.length > 0) {
|
|
847
|
+
conditions.push(`event_name IN {eventNames:Array(String)}`);
|
|
848
|
+
queryParams.eventNames = params.eventNames;
|
|
849
|
+
}
|
|
626
850
|
if (params.visitorId) {
|
|
627
851
|
conditions.push(`visitor_id = {visitorId:String}`);
|
|
628
852
|
queryParams.visitorId = params.visitorId;
|
|
@@ -638,14 +862,16 @@ var ClickHouseAdapter = class {
|
|
|
638
862
|
dateTo: params.dateTo
|
|
639
863
|
});
|
|
640
864
|
conditions.push(`timestamp >= {from:String} AND timestamp <= {to:String}`);
|
|
641
|
-
queryParams.from = dateRange.from;
|
|
642
|
-
queryParams.to = dateRange.to;
|
|
865
|
+
queryParams.from = toCHDateTime(dateRange.from);
|
|
866
|
+
queryParams.to = toCHDateTime(dateRange.to);
|
|
643
867
|
}
|
|
644
868
|
const where = conditions.join(" AND ");
|
|
645
869
|
const [events, countRows] = await Promise.all([
|
|
646
870
|
this.queryRows(
|
|
647
871
|
`SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
|
|
648
|
-
event_name, properties,
|
|
872
|
+
event_name, properties, event_source, event_subtype, page_path, target_url_path,
|
|
873
|
+
element_selector, element_text, scroll_depth_pct,
|
|
874
|
+
user_id, traits, country, city, region,
|
|
649
875
|
device_type, browser, os, language,
|
|
650
876
|
utm_source, utm_medium, utm_campaign, utm_term, utm_content
|
|
651
877
|
FROM ${EVENTS_TABLE}
|
|
@@ -690,13 +916,22 @@ var ClickHouseAdapter = class {
|
|
|
690
916
|
countIf(type = 'pageview') AS totalPageviews,
|
|
691
917
|
uniq(session_id) AS totalSessions,
|
|
692
918
|
anyLast(url) AS lastUrl,
|
|
919
|
+
anyLast(referrer) AS referrer,
|
|
693
920
|
anyLast(device_type) AS device_type,
|
|
694
921
|
anyLast(browser) AS browser,
|
|
695
922
|
anyLast(os) AS os,
|
|
696
923
|
anyLast(country) AS country,
|
|
697
924
|
anyLast(city) AS city,
|
|
698
925
|
anyLast(region) AS region,
|
|
699
|
-
anyLast(language) AS language
|
|
926
|
+
anyLast(language) AS language,
|
|
927
|
+
anyLast(timezone) AS timezone,
|
|
928
|
+
anyLast(screen_width) AS screen_width,
|
|
929
|
+
anyLast(screen_height) AS screen_height,
|
|
930
|
+
anyLast(utm_source) AS utm_source,
|
|
931
|
+
anyLast(utm_medium) AS utm_medium,
|
|
932
|
+
anyLast(utm_campaign) AS utm_campaign,
|
|
933
|
+
anyLast(utm_term) AS utm_term,
|
|
934
|
+
anyLast(utm_content) AS utm_content
|
|
700
935
|
FROM ${EVENTS_TABLE}
|
|
701
936
|
WHERE ${where}
|
|
702
937
|
GROUP BY visitor_id
|
|
@@ -724,9 +959,19 @@ var ClickHouseAdapter = class {
|
|
|
724
959
|
totalPageviews: Number(u.totalPageviews),
|
|
725
960
|
totalSessions: Number(u.totalSessions),
|
|
726
961
|
lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
|
|
962
|
+
referrer: u.referrer ? String(u.referrer) : void 0,
|
|
727
963
|
device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
|
|
728
964
|
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,
|
|
729
|
-
language: u.language ? String(u.language) : void 0
|
|
965
|
+
language: u.language ? String(u.language) : void 0,
|
|
966
|
+
timezone: u.timezone ? String(u.timezone) : void 0,
|
|
967
|
+
screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
|
|
968
|
+
utm: u.utm_source ? {
|
|
969
|
+
source: String(u.utm_source),
|
|
970
|
+
medium: u.utm_medium ? String(u.utm_medium) : void 0,
|
|
971
|
+
campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
|
|
972
|
+
term: u.utm_term ? String(u.utm_term) : void 0,
|
|
973
|
+
content: u.utm_content ? String(u.utm_content) : void 0
|
|
974
|
+
} : void 0
|
|
730
975
|
}));
|
|
731
976
|
return {
|
|
732
977
|
users,
|
|
@@ -745,15 +990,18 @@ var ClickHouseAdapter = class {
|
|
|
745
990
|
}
|
|
746
991
|
// ─── Site Management ──────────────────────────────────────
|
|
747
992
|
async createSite(data) {
|
|
748
|
-
const now =
|
|
993
|
+
const now = /* @__PURE__ */ new Date();
|
|
994
|
+
const nowISO = now.toISOString();
|
|
995
|
+
const nowCH = toCHDateTime(now);
|
|
749
996
|
const site = {
|
|
750
997
|
siteId: generateSiteId(),
|
|
751
998
|
secretKey: generateSecretKey(),
|
|
752
999
|
name: data.name,
|
|
753
1000
|
domain: data.domain,
|
|
754
1001
|
allowedOrigins: data.allowedOrigins,
|
|
755
|
-
|
|
756
|
-
|
|
1002
|
+
conversionEvents: data.conversionEvents,
|
|
1003
|
+
createdAt: nowISO,
|
|
1004
|
+
updatedAt: nowISO
|
|
757
1005
|
};
|
|
758
1006
|
await this.client.insert({
|
|
759
1007
|
table: SITES_TABLE,
|
|
@@ -763,8 +1011,9 @@ var ClickHouseAdapter = class {
|
|
|
763
1011
|
name: site.name,
|
|
764
1012
|
domain: site.domain ?? null,
|
|
765
1013
|
allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
|
|
766
|
-
|
|
767
|
-
|
|
1014
|
+
conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
|
|
1015
|
+
created_at: nowCH,
|
|
1016
|
+
updated_at: nowCH,
|
|
768
1017
|
version: 1,
|
|
769
1018
|
is_deleted: 0
|
|
770
1019
|
}],
|
|
@@ -774,7 +1023,7 @@ var ClickHouseAdapter = class {
|
|
|
774
1023
|
}
|
|
775
1024
|
async getSite(siteId) {
|
|
776
1025
|
const rows = await this.queryRows(
|
|
777
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1026
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
778
1027
|
FROM ${SITES_TABLE} FINAL
|
|
779
1028
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
780
1029
|
{ siteId }
|
|
@@ -783,7 +1032,7 @@ var ClickHouseAdapter = class {
|
|
|
783
1032
|
}
|
|
784
1033
|
async getSiteBySecret(secretKey) {
|
|
785
1034
|
const rows = await this.queryRows(
|
|
786
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1035
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
787
1036
|
FROM ${SITES_TABLE} FINAL
|
|
788
1037
|
WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
|
|
789
1038
|
{ secretKey }
|
|
@@ -792,7 +1041,7 @@ var ClickHouseAdapter = class {
|
|
|
792
1041
|
}
|
|
793
1042
|
async listSites() {
|
|
794
1043
|
const rows = await this.queryRows(
|
|
795
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at
|
|
1044
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
796
1045
|
FROM ${SITES_TABLE} FINAL
|
|
797
1046
|
WHERE is_deleted = 0
|
|
798
1047
|
ORDER BY created_at DESC`,
|
|
@@ -802,18 +1051,21 @@ var ClickHouseAdapter = class {
|
|
|
802
1051
|
}
|
|
803
1052
|
async updateSite(siteId, data) {
|
|
804
1053
|
const currentRows = await this.queryRows(
|
|
805
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, updated_at, version
|
|
1054
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
806
1055
|
FROM ${SITES_TABLE} FINAL
|
|
807
1056
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
808
1057
|
{ siteId }
|
|
809
1058
|
);
|
|
810
1059
|
if (currentRows.length === 0) return null;
|
|
811
1060
|
const current = currentRows[0];
|
|
812
|
-
const now =
|
|
1061
|
+
const now = /* @__PURE__ */ new Date();
|
|
1062
|
+
const nowISO = now.toISOString();
|
|
1063
|
+
const nowCH = toCHDateTime(now);
|
|
813
1064
|
const newVersion = Number(current.version) + 1;
|
|
814
1065
|
const newName = data.name !== void 0 ? data.name : String(current.name);
|
|
815
1066
|
const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
|
|
816
1067
|
const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
|
|
1068
|
+
const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
|
|
817
1069
|
await this.client.insert({
|
|
818
1070
|
table: SITES_TABLE,
|
|
819
1071
|
values: [{
|
|
@@ -822,8 +1074,9 @@ var ClickHouseAdapter = class {
|
|
|
822
1074
|
name: newName,
|
|
823
1075
|
domain: newDomain,
|
|
824
1076
|
allowed_origins: newOrigins,
|
|
825
|
-
|
|
826
|
-
|
|
1077
|
+
conversion_events: newConversions,
|
|
1078
|
+
created_at: toCHDateTime(String(current.created_at)),
|
|
1079
|
+
updated_at: nowCH,
|
|
827
1080
|
version: newVersion,
|
|
828
1081
|
is_deleted: 0
|
|
829
1082
|
}],
|
|
@@ -835,20 +1088,21 @@ var ClickHouseAdapter = class {
|
|
|
835
1088
|
name: newName,
|
|
836
1089
|
domain: newDomain ?? void 0,
|
|
837
1090
|
allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
|
|
1091
|
+
conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
|
|
838
1092
|
createdAt: String(current.created_at),
|
|
839
|
-
updatedAt:
|
|
1093
|
+
updatedAt: nowISO
|
|
840
1094
|
};
|
|
841
1095
|
}
|
|
842
1096
|
async deleteSite(siteId) {
|
|
843
1097
|
const currentRows = await this.queryRows(
|
|
844
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
|
|
1098
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
845
1099
|
FROM ${SITES_TABLE} FINAL
|
|
846
1100
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
847
1101
|
{ siteId }
|
|
848
1102
|
);
|
|
849
1103
|
if (currentRows.length === 0) return false;
|
|
850
1104
|
const current = currentRows[0];
|
|
851
|
-
const
|
|
1105
|
+
const nowCH = toCHDateTime(/* @__PURE__ */ new Date());
|
|
852
1106
|
await this.client.insert({
|
|
853
1107
|
table: SITES_TABLE,
|
|
854
1108
|
values: [{
|
|
@@ -857,8 +1111,9 @@ var ClickHouseAdapter = class {
|
|
|
857
1111
|
name: String(current.name),
|
|
858
1112
|
domain: current.domain ? String(current.domain) : null,
|
|
859
1113
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
860
|
-
|
|
861
|
-
|
|
1114
|
+
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
1115
|
+
created_at: toCHDateTime(String(current.created_at)),
|
|
1116
|
+
updated_at: nowCH,
|
|
862
1117
|
version: Number(current.version) + 1,
|
|
863
1118
|
is_deleted: 1
|
|
864
1119
|
}],
|
|
@@ -868,14 +1123,16 @@ var ClickHouseAdapter = class {
|
|
|
868
1123
|
}
|
|
869
1124
|
async regenerateSecret(siteId) {
|
|
870
1125
|
const currentRows = await this.queryRows(
|
|
871
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, created_at, version
|
|
1126
|
+
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
872
1127
|
FROM ${SITES_TABLE} FINAL
|
|
873
1128
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
874
1129
|
{ siteId }
|
|
875
1130
|
);
|
|
876
1131
|
if (currentRows.length === 0) return null;
|
|
877
1132
|
const current = currentRows[0];
|
|
878
|
-
const now =
|
|
1133
|
+
const now = /* @__PURE__ */ new Date();
|
|
1134
|
+
const nowISO = now.toISOString();
|
|
1135
|
+
const nowCH = toCHDateTime(now);
|
|
879
1136
|
const newSecret = generateSecretKey();
|
|
880
1137
|
await this.client.insert({
|
|
881
1138
|
table: SITES_TABLE,
|
|
@@ -885,8 +1142,9 @@ var ClickHouseAdapter = class {
|
|
|
885
1142
|
name: String(current.name),
|
|
886
1143
|
domain: current.domain ? String(current.domain) : null,
|
|
887
1144
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
888
|
-
|
|
889
|
-
|
|
1145
|
+
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
1146
|
+
created_at: toCHDateTime(String(current.created_at)),
|
|
1147
|
+
updated_at: nowCH,
|
|
890
1148
|
version: Number(current.version) + 1,
|
|
891
1149
|
is_deleted: 0
|
|
892
1150
|
}],
|
|
@@ -898,8 +1156,9 @@ var ClickHouseAdapter = class {
|
|
|
898
1156
|
name: String(current.name),
|
|
899
1157
|
domain: current.domain ? String(current.domain) : void 0,
|
|
900
1158
|
allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
|
|
1159
|
+
conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
|
|
901
1160
|
createdAt: String(current.created_at),
|
|
902
|
-
updatedAt:
|
|
1161
|
+
updatedAt: nowISO
|
|
903
1162
|
};
|
|
904
1163
|
}
|
|
905
1164
|
// ─── Helpers ─────────────────────────────────────────────
|
|
@@ -918,6 +1177,7 @@ var ClickHouseAdapter = class {
|
|
|
918
1177
|
name: String(row.name),
|
|
919
1178
|
domain: row.domain ? String(row.domain) : void 0,
|
|
920
1179
|
allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
|
|
1180
|
+
conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
|
|
921
1181
|
createdAt: new Date(String(row.created_at)).toISOString(),
|
|
922
1182
|
updatedAt: new Date(String(row.updated_at)).toISOString()
|
|
923
1183
|
};
|
|
@@ -934,6 +1194,13 @@ var ClickHouseAdapter = class {
|
|
|
934
1194
|
title: row.title ? String(row.title) : void 0,
|
|
935
1195
|
name: row.event_name ? String(row.event_name) : void 0,
|
|
936
1196
|
properties: this.parseJSON(row.properties),
|
|
1197
|
+
eventSource: row.event_source ? String(row.event_source) : void 0,
|
|
1198
|
+
eventSubtype: row.event_subtype ? String(row.event_subtype) : void 0,
|
|
1199
|
+
pagePath: row.page_path ? String(row.page_path) : void 0,
|
|
1200
|
+
targetUrlPath: row.target_url_path ? String(row.target_url_path) : void 0,
|
|
1201
|
+
elementSelector: row.element_selector ? String(row.element_selector) : void 0,
|
|
1202
|
+
elementText: row.element_text ? String(row.element_text) : void 0,
|
|
1203
|
+
scrollDepthPct: row.scroll_depth_pct !== null && row.scroll_depth_pct !== void 0 ? Number(row.scroll_depth_pct) : void 0,
|
|
937
1204
|
userId: row.user_id ? String(row.user_id) : void 0,
|
|
938
1205
|
traits: this.parseJSON(row.traits),
|
|
939
1206
|
geo: row.country ? {
|
|
@@ -970,6 +1237,36 @@ var ClickHouseAdapter = class {
|
|
|
970
1237
|
import { MongoClient } from "mongodb";
|
|
971
1238
|
var EVENTS_COLLECTION = "litemetrics_events";
|
|
972
1239
|
var SITES_COLLECTION = "litemetrics_sites";
|
|
1240
|
+
function buildFilterMatch(filters) {
|
|
1241
|
+
if (!filters) return {};
|
|
1242
|
+
const map = {
|
|
1243
|
+
"geo.country": "country",
|
|
1244
|
+
"geo.city": "city",
|
|
1245
|
+
"geo.region": "region",
|
|
1246
|
+
"language": "language",
|
|
1247
|
+
"device.type": "device_type",
|
|
1248
|
+
"device.browser": "browser",
|
|
1249
|
+
"device.os": "os",
|
|
1250
|
+
"utm.source": "utm_source",
|
|
1251
|
+
"utm.medium": "utm_medium",
|
|
1252
|
+
"utm.campaign": "utm_campaign",
|
|
1253
|
+
"utm.term": "utm_term",
|
|
1254
|
+
"utm.content": "utm_content",
|
|
1255
|
+
"referrer": "referrer",
|
|
1256
|
+
"event_source": "event_source",
|
|
1257
|
+
"event_subtype": "event_subtype",
|
|
1258
|
+
"page_path": "page_path",
|
|
1259
|
+
"target_url_path": "target_url_path",
|
|
1260
|
+
"event_name": "event_name",
|
|
1261
|
+
"type": "type"
|
|
1262
|
+
};
|
|
1263
|
+
const match = {};
|
|
1264
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
1265
|
+
if (!value || !map[key]) continue;
|
|
1266
|
+
match[map[key]] = value;
|
|
1267
|
+
}
|
|
1268
|
+
return match;
|
|
1269
|
+
}
|
|
973
1270
|
var MongoDBAdapter = class {
|
|
974
1271
|
client;
|
|
975
1272
|
db;
|
|
@@ -1005,6 +1302,13 @@ var MongoDBAdapter = class {
|
|
|
1005
1302
|
title: e.title ?? null,
|
|
1006
1303
|
event_name: e.name ?? null,
|
|
1007
1304
|
properties: e.properties ?? null,
|
|
1305
|
+
event_source: e.eventSource ?? null,
|
|
1306
|
+
event_subtype: e.eventSubtype ?? null,
|
|
1307
|
+
page_path: e.pagePath ?? null,
|
|
1308
|
+
target_url_path: e.targetUrlPath ?? null,
|
|
1309
|
+
element_selector: e.elementSelector ?? null,
|
|
1310
|
+
element_text: e.elementText ?? null,
|
|
1311
|
+
scroll_depth_pct: e.scrollDepthPct ?? null,
|
|
1008
1312
|
user_id: e.userId ?? null,
|
|
1009
1313
|
traits: e.traits ?? null,
|
|
1010
1314
|
country: e.geo?.country ?? null,
|
|
@@ -1035,12 +1339,13 @@ var MongoDBAdapter = class {
|
|
|
1035
1339
|
site_id: siteId,
|
|
1036
1340
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1037
1341
|
};
|
|
1342
|
+
const filterMatch = buildFilterMatch(q.filters);
|
|
1038
1343
|
let data = [];
|
|
1039
1344
|
let total = 0;
|
|
1040
1345
|
switch (q.metric) {
|
|
1041
1346
|
case "pageviews": {
|
|
1042
1347
|
const [result2] = await this.collection.aggregate([
|
|
1043
|
-
{ $match: { ...baseMatch, type: "pageview" } },
|
|
1348
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview" } },
|
|
1044
1349
|
{ $count: "count" }
|
|
1045
1350
|
]).toArray();
|
|
1046
1351
|
total = result2?.count ?? 0;
|
|
@@ -1049,7 +1354,7 @@ var MongoDBAdapter = class {
|
|
|
1049
1354
|
}
|
|
1050
1355
|
case "visitors": {
|
|
1051
1356
|
const [result2] = await this.collection.aggregate([
|
|
1052
|
-
{ $match: baseMatch },
|
|
1357
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1053
1358
|
{ $group: { _id: "$visitor_id" } },
|
|
1054
1359
|
{ $count: "count" }
|
|
1055
1360
|
]).toArray();
|
|
@@ -1059,7 +1364,7 @@ var MongoDBAdapter = class {
|
|
|
1059
1364
|
}
|
|
1060
1365
|
case "sessions": {
|
|
1061
1366
|
const [result2] = await this.collection.aggregate([
|
|
1062
|
-
{ $match: baseMatch },
|
|
1367
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1063
1368
|
{ $group: { _id: "$session_id" } },
|
|
1064
1369
|
{ $count: "count" }
|
|
1065
1370
|
]).toArray();
|
|
@@ -1069,16 +1374,31 @@ var MongoDBAdapter = class {
|
|
|
1069
1374
|
}
|
|
1070
1375
|
case "events": {
|
|
1071
1376
|
const [result2] = await this.collection.aggregate([
|
|
1072
|
-
{ $match: { ...baseMatch, type: "event" } },
|
|
1377
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event" } },
|
|
1073
1378
|
{ $count: "count" }
|
|
1074
1379
|
]).toArray();
|
|
1075
1380
|
total = result2?.count ?? 0;
|
|
1076
1381
|
data = [{ key: "events", value: total }];
|
|
1077
1382
|
break;
|
|
1078
1383
|
}
|
|
1384
|
+
case "conversions": {
|
|
1385
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
1386
|
+
if (conversionEvents.length === 0) {
|
|
1387
|
+
total = 0;
|
|
1388
|
+
data = [{ key: "conversions", value: 0 }];
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
const [result2] = await this.collection.aggregate([
|
|
1392
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
|
|
1393
|
+
{ $count: "count" }
|
|
1394
|
+
]).toArray();
|
|
1395
|
+
total = result2?.count ?? 0;
|
|
1396
|
+
data = [{ key: "conversions", value: total }];
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1079
1399
|
case "top_pages": {
|
|
1080
1400
|
const rows = await this.collection.aggregate([
|
|
1081
|
-
{ $match: { ...baseMatch, type: "pageview", url: { $ne: null } } },
|
|
1401
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1082
1402
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1083
1403
|
{ $sort: { value: -1 } },
|
|
1084
1404
|
{ $limit: limit }
|
|
@@ -1089,7 +1409,7 @@ var MongoDBAdapter = class {
|
|
|
1089
1409
|
}
|
|
1090
1410
|
case "top_referrers": {
|
|
1091
1411
|
const rows = await this.collection.aggregate([
|
|
1092
|
-
{ $match: { ...baseMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
|
|
1412
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", referrer: { $nin: [null, ""] } } },
|
|
1093
1413
|
{ $group: { _id: "$referrer", value: { $sum: 1 } } },
|
|
1094
1414
|
{ $sort: { value: -1 } },
|
|
1095
1415
|
{ $limit: limit }
|
|
@@ -1100,7 +1420,7 @@ var MongoDBAdapter = class {
|
|
|
1100
1420
|
}
|
|
1101
1421
|
case "top_countries": {
|
|
1102
1422
|
const rows = await this.collection.aggregate([
|
|
1103
|
-
{ $match: { ...baseMatch, country: { $ne: null } } },
|
|
1423
|
+
{ $match: { ...baseMatch, ...filterMatch, country: { $ne: null } } },
|
|
1104
1424
|
{ $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
|
|
1105
1425
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1106
1426
|
{ $sort: { value: -1 } },
|
|
@@ -1112,7 +1432,7 @@ var MongoDBAdapter = class {
|
|
|
1112
1432
|
}
|
|
1113
1433
|
case "top_cities": {
|
|
1114
1434
|
const rows = await this.collection.aggregate([
|
|
1115
|
-
{ $match: { ...baseMatch, city: { $ne: null } } },
|
|
1435
|
+
{ $match: { ...baseMatch, ...filterMatch, city: { $ne: null } } },
|
|
1116
1436
|
{ $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
|
|
1117
1437
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1118
1438
|
{ $sort: { value: -1 } },
|
|
@@ -1124,7 +1444,7 @@ var MongoDBAdapter = class {
|
|
|
1124
1444
|
}
|
|
1125
1445
|
case "top_events": {
|
|
1126
1446
|
const rows = await this.collection.aggregate([
|
|
1127
|
-
{ $match: { ...baseMatch, type: "event", event_name: { $ne: null } } },
|
|
1447
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $ne: null } } },
|
|
1128
1448
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1129
1449
|
{ $sort: { value: -1 } },
|
|
1130
1450
|
{ $limit: limit }
|
|
@@ -1133,9 +1453,109 @@ var MongoDBAdapter = class {
|
|
|
1133
1453
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1134
1454
|
break;
|
|
1135
1455
|
}
|
|
1456
|
+
case "top_conversions": {
|
|
1457
|
+
const conversionEvents = q.conversionEvents ?? [];
|
|
1458
|
+
if (conversionEvents.length === 0) {
|
|
1459
|
+
total = 0;
|
|
1460
|
+
data = [];
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
const rows = await this.collection.aggregate([
|
|
1464
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_name: { $in: conversionEvents } } },
|
|
1465
|
+
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1466
|
+
{ $sort: { value: -1 } },
|
|
1467
|
+
{ $limit: limit }
|
|
1468
|
+
]).toArray();
|
|
1469
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1470
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1473
|
+
case "top_exit_pages": {
|
|
1474
|
+
const rows = await this.collection.aggregate([
|
|
1475
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1476
|
+
{ $sort: { timestamp: 1 } },
|
|
1477
|
+
{ $group: { _id: "$session_id", url: { $last: "$url" } } },
|
|
1478
|
+
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1479
|
+
{ $sort: { value: -1 } },
|
|
1480
|
+
{ $limit: limit }
|
|
1481
|
+
]).toArray();
|
|
1482
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1483
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
case "top_transitions": {
|
|
1487
|
+
const rows = await this.collection.aggregate([
|
|
1488
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "pageview", url: { $ne: null } } },
|
|
1489
|
+
{
|
|
1490
|
+
$setWindowFields: {
|
|
1491
|
+
partitionBy: "$session_id",
|
|
1492
|
+
sortBy: { timestamp: 1 },
|
|
1493
|
+
output: {
|
|
1494
|
+
prev_url: { $shift: { output: "$url", by: -1 } }
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
1498
|
+
{ $match: { prev_url: { $ne: null } } },
|
|
1499
|
+
{ $group: { _id: { $concat: ["$prev_url", " \u2192 ", "$url"] }, value: { $sum: 1 } } },
|
|
1500
|
+
{ $sort: { value: -1 } },
|
|
1501
|
+
{ $limit: limit }
|
|
1502
|
+
]).toArray();
|
|
1503
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1504
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
case "top_scroll_pages": {
|
|
1508
|
+
const rows = await this.collection.aggregate([
|
|
1509
|
+
{ $match: { ...baseMatch, ...filterMatch, type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } } },
|
|
1510
|
+
{ $group: { _id: "$page_path", value: { $sum: 1 } } },
|
|
1511
|
+
{ $sort: { value: -1 } },
|
|
1512
|
+
{ $limit: limit }
|
|
1513
|
+
]).toArray();
|
|
1514
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1515
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
case "top_button_clicks": {
|
|
1519
|
+
const rows = await this.collection.aggregate([
|
|
1520
|
+
{
|
|
1521
|
+
$match: {
|
|
1522
|
+
...baseMatch,
|
|
1523
|
+
...filterMatch,
|
|
1524
|
+
type: "event",
|
|
1525
|
+
event_subtype: "button_click",
|
|
1526
|
+
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
1527
|
+
}
|
|
1528
|
+
},
|
|
1529
|
+
{ $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
|
|
1530
|
+
{ $sort: { value: -1 } },
|
|
1531
|
+
{ $limit: limit }
|
|
1532
|
+
]).toArray();
|
|
1533
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1534
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1535
|
+
break;
|
|
1536
|
+
}
|
|
1537
|
+
case "top_link_targets": {
|
|
1538
|
+
const rows = await this.collection.aggregate([
|
|
1539
|
+
{
|
|
1540
|
+
$match: {
|
|
1541
|
+
...baseMatch,
|
|
1542
|
+
...filterMatch,
|
|
1543
|
+
type: "event",
|
|
1544
|
+
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
1545
|
+
target_url_path: { $ne: null }
|
|
1546
|
+
}
|
|
1547
|
+
},
|
|
1548
|
+
{ $group: { _id: "$target_url_path", value: { $sum: 1 } } },
|
|
1549
|
+
{ $sort: { value: -1 } },
|
|
1550
|
+
{ $limit: limit }
|
|
1551
|
+
]).toArray();
|
|
1552
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
1553
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1136
1556
|
case "top_devices": {
|
|
1137
1557
|
const rows = await this.collection.aggregate([
|
|
1138
|
-
{ $match: { ...baseMatch, device_type: { $ne: null } } },
|
|
1558
|
+
{ $match: { ...baseMatch, ...filterMatch, device_type: { $ne: null } } },
|
|
1139
1559
|
{ $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
|
|
1140
1560
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1141
1561
|
{ $sort: { value: -1 } },
|
|
@@ -1147,7 +1567,7 @@ var MongoDBAdapter = class {
|
|
|
1147
1567
|
}
|
|
1148
1568
|
case "top_browsers": {
|
|
1149
1569
|
const rows = await this.collection.aggregate([
|
|
1150
|
-
{ $match: { ...baseMatch, browser: { $ne: null } } },
|
|
1570
|
+
{ $match: { ...baseMatch, ...filterMatch, browser: { $ne: null } } },
|
|
1151
1571
|
{ $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
|
|
1152
1572
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1153
1573
|
{ $sort: { value: -1 } },
|
|
@@ -1159,7 +1579,7 @@ var MongoDBAdapter = class {
|
|
|
1159
1579
|
}
|
|
1160
1580
|
case "top_os": {
|
|
1161
1581
|
const rows = await this.collection.aggregate([
|
|
1162
|
-
{ $match: { ...baseMatch, os: { $ne: null } } },
|
|
1582
|
+
{ $match: { ...baseMatch, ...filterMatch, os: { $ne: null } } },
|
|
1163
1583
|
{ $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
|
|
1164
1584
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1165
1585
|
{ $sort: { value: -1 } },
|
|
@@ -1171,7 +1591,7 @@ var MongoDBAdapter = class {
|
|
|
1171
1591
|
}
|
|
1172
1592
|
}
|
|
1173
1593
|
const result = { metric: q.metric, period, data, total };
|
|
1174
|
-
if (q.compare && ["pageviews", "visitors", "sessions", "events"].includes(q.metric)) {
|
|
1594
|
+
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
1175
1595
|
const prevRange = previousPeriodRange(dateRange);
|
|
1176
1596
|
const prevResult = await this.query({
|
|
1177
1597
|
...q,
|
|
@@ -1203,15 +1623,34 @@ var MongoDBAdapter = class {
|
|
|
1203
1623
|
site_id: params.siteId,
|
|
1204
1624
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1205
1625
|
};
|
|
1626
|
+
const filterMatch = buildFilterMatch(params.filters);
|
|
1206
1627
|
if (params.metric === "pageviews") {
|
|
1207
1628
|
baseMatch.type = "pageview";
|
|
1208
1629
|
}
|
|
1630
|
+
if (params.metric === "events") {
|
|
1631
|
+
baseMatch.type = "event";
|
|
1632
|
+
}
|
|
1633
|
+
if (params.metric === "conversions") {
|
|
1634
|
+
baseMatch.type = "event";
|
|
1635
|
+
const conversionEvents = params.conversionEvents ?? [];
|
|
1636
|
+
if (conversionEvents.length === 0) {
|
|
1637
|
+
const data2 = fillBuckets(
|
|
1638
|
+
new Date(dateRange.from),
|
|
1639
|
+
new Date(dateRange.to),
|
|
1640
|
+
granularity,
|
|
1641
|
+
granularityToDateFormat(granularity),
|
|
1642
|
+
[]
|
|
1643
|
+
);
|
|
1644
|
+
return { metric: params.metric, granularity, data: data2 };
|
|
1645
|
+
}
|
|
1646
|
+
baseMatch.event_name = { $in: conversionEvents };
|
|
1647
|
+
}
|
|
1209
1648
|
const dateFormat = granularityToDateFormat(granularity);
|
|
1210
1649
|
let pipeline;
|
|
1211
1650
|
if (params.metric === "visitors" || params.metric === "sessions") {
|
|
1212
1651
|
const groupField = params.metric === "visitors" ? "$visitor_id" : "$session_id";
|
|
1213
1652
|
pipeline = [
|
|
1214
|
-
{ $match: baseMatch },
|
|
1653
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1215
1654
|
{
|
|
1216
1655
|
$group: {
|
|
1217
1656
|
_id: {
|
|
@@ -1230,7 +1669,7 @@ var MongoDBAdapter = class {
|
|
|
1230
1669
|
];
|
|
1231
1670
|
} else {
|
|
1232
1671
|
pipeline = [
|
|
1233
|
-
{ $match: baseMatch },
|
|
1672
|
+
{ $match: { ...baseMatch, ...filterMatch } },
|
|
1234
1673
|
{
|
|
1235
1674
|
$group: {
|
|
1236
1675
|
_id: { $dateToString: { format: dateFormat, date: "$timestamp" } },
|
|
@@ -1311,7 +1750,12 @@ var MongoDBAdapter = class {
|
|
|
1311
1750
|
const offset = params.offset ?? 0;
|
|
1312
1751
|
const match = { site_id: params.siteId };
|
|
1313
1752
|
if (params.type) match.type = params.type;
|
|
1314
|
-
if (params.eventName)
|
|
1753
|
+
if (params.eventName) {
|
|
1754
|
+
match.event_name = params.eventName;
|
|
1755
|
+
} else if (params.eventNames && params.eventNames.length > 0) {
|
|
1756
|
+
match.event_name = { $in: params.eventNames };
|
|
1757
|
+
}
|
|
1758
|
+
if (params.eventSource) match.event_source = params.eventSource;
|
|
1315
1759
|
if (params.visitorId) match.visitor_id = params.visitorId;
|
|
1316
1760
|
if (params.userId) match.user_id = params.userId;
|
|
1317
1761
|
if (params.period || params.dateFrom) {
|
|
@@ -1346,6 +1790,7 @@ var MongoDBAdapter = class {
|
|
|
1346
1790
|
}
|
|
1347
1791
|
const pipeline = [
|
|
1348
1792
|
{ $match: match },
|
|
1793
|
+
{ $sort: { timestamp: 1 } },
|
|
1349
1794
|
{
|
|
1350
1795
|
$group: {
|
|
1351
1796
|
_id: "$visitor_id",
|
|
@@ -1357,13 +1802,22 @@ var MongoDBAdapter = class {
|
|
|
1357
1802
|
totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
|
|
1358
1803
|
sessions: { $addToSet: "$session_id" },
|
|
1359
1804
|
lastUrl: { $last: "$url" },
|
|
1805
|
+
referrer: { $last: "$referrer" },
|
|
1360
1806
|
device_type: { $last: "$device_type" },
|
|
1361
1807
|
browser: { $last: "$browser" },
|
|
1362
1808
|
os: { $last: "$os" },
|
|
1363
1809
|
country: { $last: "$country" },
|
|
1364
1810
|
city: { $last: "$city" },
|
|
1365
1811
|
region: { $last: "$region" },
|
|
1366
|
-
language: { $last: "$language" }
|
|
1812
|
+
language: { $last: "$language" },
|
|
1813
|
+
timezone: { $last: "$timezone" },
|
|
1814
|
+
screen_width: { $last: "$screen_width" },
|
|
1815
|
+
screen_height: { $last: "$screen_height" },
|
|
1816
|
+
utm_source: { $last: "$utm_source" },
|
|
1817
|
+
utm_medium: { $last: "$utm_medium" },
|
|
1818
|
+
utm_campaign: { $last: "$utm_campaign" },
|
|
1819
|
+
utm_term: { $last: "$utm_term" },
|
|
1820
|
+
utm_content: { $last: "$utm_content" }
|
|
1367
1821
|
}
|
|
1368
1822
|
},
|
|
1369
1823
|
{ $sort: { lastSeen: -1 } },
|
|
@@ -1385,9 +1839,19 @@ var MongoDBAdapter = class {
|
|
|
1385
1839
|
totalPageviews: u.totalPageviews,
|
|
1386
1840
|
totalSessions: u.sessions.length,
|
|
1387
1841
|
lastUrl: u.lastUrl ?? void 0,
|
|
1842
|
+
referrer: u.referrer ?? void 0,
|
|
1388
1843
|
device: u.device_type ? { type: u.device_type, browser: u.browser ?? "", os: u.os ?? "" } : void 0,
|
|
1389
1844
|
geo: u.country ? { country: u.country, city: u.city ?? void 0, region: u.region ?? void 0 } : void 0,
|
|
1390
|
-
language: u.language ?? void 0
|
|
1845
|
+
language: u.language ?? void 0,
|
|
1846
|
+
timezone: u.timezone ?? void 0,
|
|
1847
|
+
screen: u.screen_width || u.screen_height ? { width: u.screen_width ?? 0, height: u.screen_height ?? 0 } : void 0,
|
|
1848
|
+
utm: u.utm_source ? {
|
|
1849
|
+
source: u.utm_source ?? void 0,
|
|
1850
|
+
medium: u.utm_medium ?? void 0,
|
|
1851
|
+
campaign: u.utm_campaign ?? void 0,
|
|
1852
|
+
term: u.utm_term ?? void 0,
|
|
1853
|
+
content: u.utm_content ?? void 0
|
|
1854
|
+
} : void 0
|
|
1391
1855
|
}));
|
|
1392
1856
|
return {
|
|
1393
1857
|
users,
|
|
@@ -1416,6 +1880,13 @@ var MongoDBAdapter = class {
|
|
|
1416
1880
|
title: doc.title ?? void 0,
|
|
1417
1881
|
name: doc.event_name ?? void 0,
|
|
1418
1882
|
properties: doc.properties ?? void 0,
|
|
1883
|
+
eventSource: doc.event_source ? doc.event_source : void 0,
|
|
1884
|
+
eventSubtype: doc.event_subtype ? doc.event_subtype : void 0,
|
|
1885
|
+
pagePath: doc.page_path ?? void 0,
|
|
1886
|
+
targetUrlPath: doc.target_url_path ?? void 0,
|
|
1887
|
+
elementSelector: doc.element_selector ?? void 0,
|
|
1888
|
+
elementText: doc.element_text ?? void 0,
|
|
1889
|
+
scrollDepthPct: doc.scroll_depth_pct ?? void 0,
|
|
1419
1890
|
userId: doc.user_id ?? void 0,
|
|
1420
1891
|
traits: doc.traits ?? void 0,
|
|
1421
1892
|
geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
|
|
@@ -1439,6 +1910,7 @@ var MongoDBAdapter = class {
|
|
|
1439
1910
|
name: data.name,
|
|
1440
1911
|
domain: data.domain ?? null,
|
|
1441
1912
|
allowed_origins: data.allowedOrigins ?? null,
|
|
1913
|
+
conversion_events: data.conversionEvents ?? null,
|
|
1442
1914
|
created_at: now,
|
|
1443
1915
|
updated_at: now
|
|
1444
1916
|
};
|
|
@@ -1462,6 +1934,7 @@ var MongoDBAdapter = class {
|
|
|
1462
1934
|
if (data.name !== void 0) updates.name = data.name;
|
|
1463
1935
|
if (data.domain !== void 0) updates.domain = data.domain || null;
|
|
1464
1936
|
if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
|
|
1937
|
+
if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
|
|
1465
1938
|
const result = await this.sites.findOneAndUpdate(
|
|
1466
1939
|
{ site_id: siteId },
|
|
1467
1940
|
{ $set: updates },
|
|
@@ -1492,6 +1965,7 @@ var MongoDBAdapter = class {
|
|
|
1492
1965
|
name: doc.name,
|
|
1493
1966
|
domain: doc.domain ?? void 0,
|
|
1494
1967
|
allowedOrigins: doc.allowed_origins ?? void 0,
|
|
1968
|
+
conversionEvents: doc.conversion_events ?? void 0,
|
|
1495
1969
|
createdAt: doc.created_at.toISOString(),
|
|
1496
1970
|
updatedAt: doc.updated_at.toISOString()
|
|
1497
1971
|
};
|
|
@@ -1662,6 +2136,63 @@ function resolveDeviceType(type) {
|
|
|
1662
2136
|
return "desktop";
|
|
1663
2137
|
}
|
|
1664
2138
|
|
|
2139
|
+
// src/botfilter.ts
|
|
2140
|
+
var BOT_PATTERNS = [
|
|
2141
|
+
// Headless browsers
|
|
2142
|
+
/HeadlessChrome/i,
|
|
2143
|
+
/PhantomJS/i,
|
|
2144
|
+
/Selenium/i,
|
|
2145
|
+
/Puppeteer/i,
|
|
2146
|
+
/Playwright/i,
|
|
2147
|
+
// Common bots
|
|
2148
|
+
/bot\b/i,
|
|
2149
|
+
/spider/i,
|
|
2150
|
+
/crawl/i,
|
|
2151
|
+
/slurp/i,
|
|
2152
|
+
/mediapartners/i,
|
|
2153
|
+
/facebookexternalhit/i,
|
|
2154
|
+
/Twitterbot/i,
|
|
2155
|
+
/LinkedInBot/i,
|
|
2156
|
+
/WhatsApp/i,
|
|
2157
|
+
/Discordbot/i,
|
|
2158
|
+
/TelegramBot/i,
|
|
2159
|
+
/Applebot/i,
|
|
2160
|
+
/Baiduspider/i,
|
|
2161
|
+
/YandexBot/i,
|
|
2162
|
+
/DuckDuckBot/i,
|
|
2163
|
+
/Sogou/i,
|
|
2164
|
+
/Exabot/i,
|
|
2165
|
+
/ia_archiver/i,
|
|
2166
|
+
// HTTP libraries & API tools
|
|
2167
|
+
/PostmanRuntime/i,
|
|
2168
|
+
/axios/i,
|
|
2169
|
+
/node-fetch/i,
|
|
2170
|
+
/python-requests/i,
|
|
2171
|
+
/Go-http-client/i,
|
|
2172
|
+
/Java\//i,
|
|
2173
|
+
/libwww-perl/i,
|
|
2174
|
+
/wget/i,
|
|
2175
|
+
/curl/i,
|
|
2176
|
+
/httpie/i,
|
|
2177
|
+
// Monitoring / uptime
|
|
2178
|
+
/UptimeRobot/i,
|
|
2179
|
+
/Pingdom/i,
|
|
2180
|
+
/StatusCake/i,
|
|
2181
|
+
/Site24x7/i,
|
|
2182
|
+
/NewRelic/i,
|
|
2183
|
+
/Datadog/i,
|
|
2184
|
+
// Preview/embed
|
|
2185
|
+
/Slackbot/i,
|
|
2186
|
+
/Embedly/i,
|
|
2187
|
+
/Quora Link Preview/i,
|
|
2188
|
+
/redditbot/i,
|
|
2189
|
+
/Pinterestbot/i
|
|
2190
|
+
];
|
|
2191
|
+
function isBot(ua) {
|
|
2192
|
+
if (!ua || ua.length === 0) return true;
|
|
2193
|
+
return BOT_PATTERNS.some((re) => re.test(ua));
|
|
2194
|
+
}
|
|
2195
|
+
|
|
1665
2196
|
// src/collector.ts
|
|
1666
2197
|
async function createCollector(config) {
|
|
1667
2198
|
const db = createAdapter(config.db);
|
|
@@ -1688,6 +2219,7 @@ async function createCollector(config) {
|
|
|
1688
2219
|
if (allowed) {
|
|
1689
2220
|
res.setHeader?.("Access-Control-Allow-Origin", origin || "*");
|
|
1690
2221
|
res.setHeader?.("Access-Control-Allow-Methods", methods);
|
|
2222
|
+
res.setHeader?.("Access-Control-Allow-Credentials", "true");
|
|
1691
2223
|
const headers = ["Content-Type", extraHeaders].filter(Boolean).join(", ");
|
|
1692
2224
|
res.setHeader?.("Access-Control-Allow-Headers", headers);
|
|
1693
2225
|
}
|
|
@@ -1718,7 +2250,14 @@ async function createCollector(config) {
|
|
|
1718
2250
|
}
|
|
1719
2251
|
function handler() {
|
|
1720
2252
|
return async (req, res) => {
|
|
1721
|
-
|
|
2253
|
+
res.setHeader?.("Access-Control-Allow-Origin", "*");
|
|
2254
|
+
res.setHeader?.("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
2255
|
+
res.setHeader?.("Access-Control-Allow-Headers", "Content-Type");
|
|
2256
|
+
if (req.method === "OPTIONS") {
|
|
2257
|
+
res.writeHead?.(204);
|
|
2258
|
+
res.end?.();
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
1722
2261
|
if (req.method !== "POST") {
|
|
1723
2262
|
sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
1724
2263
|
return;
|
|
@@ -1734,9 +2273,36 @@ async function createCollector(config) {
|
|
|
1734
2273
|
sendJson(res, 400, { ok: false, error: "Too many events (max 100)" });
|
|
1735
2274
|
return;
|
|
1736
2275
|
}
|
|
1737
|
-
const ip = extractIp(req);
|
|
1738
2276
|
const userAgent = req.headers?.["user-agent"] || "";
|
|
2277
|
+
if (isBot(userAgent)) {
|
|
2278
|
+
sendJson(res, 200, { ok: true });
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
const ip = extractIp(req);
|
|
1739
2282
|
const enriched = enrichEvents(payload.events, ip, userAgent);
|
|
2283
|
+
const siteId = enriched[0]?.siteId;
|
|
2284
|
+
if (siteId) {
|
|
2285
|
+
const site = await db.getSite(siteId);
|
|
2286
|
+
if (site?.allowedOrigins && site.allowedOrigins.length > 0) {
|
|
2287
|
+
const allowed = new Set(site.allowedOrigins.map((h) => h.toLowerCase()));
|
|
2288
|
+
const filtered = enriched.filter((event) => {
|
|
2289
|
+
if (!event.url) return true;
|
|
2290
|
+
try {
|
|
2291
|
+
const hostname = new URL(event.url).hostname.toLowerCase();
|
|
2292
|
+
return allowed.has(hostname);
|
|
2293
|
+
} catch {
|
|
2294
|
+
return true;
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
if (filtered.length === 0) {
|
|
2298
|
+
sendJson(res, 200, { ok: true });
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
await db.insertEvents(filtered);
|
|
2302
|
+
sendJson(res, 200, { ok: true });
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
1740
2306
|
await db.insertEvents(enriched);
|
|
1741
2307
|
sendJson(res, 200, { ok: true });
|
|
1742
2308
|
} catch (err) {
|
|
@@ -1770,8 +2336,13 @@ async function createCollector(config) {
|
|
|
1770
2336
|
period: params.period,
|
|
1771
2337
|
dateFrom: params.dateFrom,
|
|
1772
2338
|
dateTo: params.dateTo,
|
|
1773
|
-
granularity: q.granularity
|
|
2339
|
+
granularity: q.granularity,
|
|
2340
|
+
filters: q.filters ? JSON.parse(q.filters) : void 0
|
|
1774
2341
|
};
|
|
2342
|
+
if (tsParams.metric === "conversions") {
|
|
2343
|
+
const site = await db.getSite(params.siteId);
|
|
2344
|
+
tsParams.conversionEvents = site?.conversionEvents ?? [];
|
|
2345
|
+
}
|
|
1775
2346
|
const result2 = await db.queryTimeSeries(tsParams);
|
|
1776
2347
|
sendJson(res, 200, result2);
|
|
1777
2348
|
return;
|
|
@@ -1787,7 +2358,15 @@ async function createCollector(config) {
|
|
|
1787
2358
|
sendJson(res, 200, result2);
|
|
1788
2359
|
return;
|
|
1789
2360
|
}
|
|
1790
|
-
const
|
|
2361
|
+
const isConversionMetric = params.metric === "conversions" || params.metric === "top_conversions";
|
|
2362
|
+
let result;
|
|
2363
|
+
if (isConversionMetric) {
|
|
2364
|
+
const site = await db.getSite(params.siteId);
|
|
2365
|
+
const conversionEvents = site?.conversionEvents ?? [];
|
|
2366
|
+
result = await db.query({ ...params, conversionEvents });
|
|
2367
|
+
} else {
|
|
2368
|
+
result = await db.query(params);
|
|
2369
|
+
}
|
|
1791
2370
|
sendJson(res, 200, result);
|
|
1792
2371
|
} catch (err) {
|
|
1793
2372
|
sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : "Internal error" });
|
|
@@ -1884,10 +2463,13 @@ async function createCollector(config) {
|
|
|
1884
2463
|
sendJson(res, 401, { ok: false, error: "Invalid or missing secret key" });
|
|
1885
2464
|
return;
|
|
1886
2465
|
}
|
|
2466
|
+
const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1887
2467
|
const params = {
|
|
1888
2468
|
siteId: q.siteId,
|
|
1889
2469
|
type: q.type,
|
|
1890
2470
|
eventName: q.eventName,
|
|
2471
|
+
eventNames,
|
|
2472
|
+
eventSource: q.eventSource,
|
|
1891
2473
|
visitorId: q.visitorId,
|
|
1892
2474
|
userId: q.userId,
|
|
1893
2475
|
period: q.period,
|
|
@@ -1927,9 +2509,13 @@ async function createCollector(config) {
|
|
|
1927
2509
|
const visitorId = usersIdx >= 0 ? pathSegments[usersIdx + 1] : void 0;
|
|
1928
2510
|
const action = usersIdx >= 0 ? pathSegments[usersIdx + 2] : void 0;
|
|
1929
2511
|
if (visitorId && action === "events") {
|
|
2512
|
+
const eventNames = typeof q.eventNames === "string" ? q.eventNames.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1930
2513
|
const params2 = {
|
|
1931
2514
|
siteId: q.siteId,
|
|
1932
2515
|
type: q.type,
|
|
2516
|
+
eventName: q.eventName,
|
|
2517
|
+
eventNames,
|
|
2518
|
+
eventSource: q.eventSource,
|
|
1933
2519
|
period: q.period,
|
|
1934
2520
|
dateFrom: q.dateFrom,
|
|
1935
2521
|
dateTo: q.dateTo,
|
|
@@ -2036,7 +2622,8 @@ function createAdapter(config) {
|
|
|
2036
2622
|
}
|
|
2037
2623
|
}
|
|
2038
2624
|
async function parseBody(req) {
|
|
2039
|
-
if (req.body) return req.body;
|
|
2625
|
+
if (req.body && typeof req.body === "object") return req.body;
|
|
2626
|
+
if (typeof req.body === "string") return JSON.parse(req.body);
|
|
2040
2627
|
return new Promise((resolve, reject) => {
|
|
2041
2628
|
let data = "";
|
|
2042
2629
|
req.on("data", (chunk) => {
|
|
@@ -2076,6 +2663,7 @@ function sendJson(res, status, body) {
|
|
|
2076
2663
|
export {
|
|
2077
2664
|
ClickHouseAdapter,
|
|
2078
2665
|
MongoDBAdapter,
|
|
2079
|
-
createCollector
|
|
2666
|
+
createCollector,
|
|
2667
|
+
isBot
|
|
2080
2668
|
};
|
|
2081
2669
|
//# sourceMappingURL=index.js.map
|