@litemetrics/node 0.1.2 → 0.2.0
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 +17 -1
- package/dist/index.cjs +1082 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -6
- package/dist/index.d.ts +17 -6
- package/dist/index.js +1082 -130
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -179,6 +179,18 @@ function generateSecretKey() {
|
|
|
179
179
|
// src/adapters/clickhouse.ts
|
|
180
180
|
var EVENTS_TABLE = "litemetrics_events";
|
|
181
181
|
var SITES_TABLE = "litemetrics_sites";
|
|
182
|
+
var IDENTITY_MAP_TABLE = "litemetrics_identity_map";
|
|
183
|
+
var CREATE_IDENTITY_MAP_TABLE = `
|
|
184
|
+
CREATE TABLE IF NOT EXISTS ${IDENTITY_MAP_TABLE} (
|
|
185
|
+
site_id LowCardinality(String),
|
|
186
|
+
visitor_id String,
|
|
187
|
+
user_id String,
|
|
188
|
+
identified_at DateTime64(3),
|
|
189
|
+
created_at DateTime64(3) DEFAULT now64(3)
|
|
190
|
+
) ENGINE = ReplacingMergeTree(created_at)
|
|
191
|
+
ORDER BY (site_id, visitor_id)
|
|
192
|
+
SETTINGS index_granularity = 8192
|
|
193
|
+
`;
|
|
182
194
|
var CREATE_EVENTS_TABLE = `
|
|
183
195
|
CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
184
196
|
event_id UUID DEFAULT generateUUIDv4(),
|
|
@@ -217,6 +229,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
|
217
229
|
utm_term Nullable(String),
|
|
218
230
|
utm_content Nullable(String),
|
|
219
231
|
ip Nullable(String),
|
|
232
|
+
os_version LowCardinality(Nullable(String)),
|
|
233
|
+
device_model LowCardinality(Nullable(String)),
|
|
234
|
+
device_brand LowCardinality(Nullable(String)),
|
|
235
|
+
app_version LowCardinality(Nullable(String)),
|
|
236
|
+
app_build Nullable(String),
|
|
237
|
+
sdk_name LowCardinality(Nullable(String)),
|
|
238
|
+
sdk_version LowCardinality(Nullable(String)),
|
|
220
239
|
created_at DateTime64(3) DEFAULT now64(3)
|
|
221
240
|
) ENGINE = MergeTree()
|
|
222
241
|
PARTITION BY toYYYYMM(timestamp)
|
|
@@ -228,6 +247,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
228
247
|
site_id String,
|
|
229
248
|
secret_key String,
|
|
230
249
|
name String,
|
|
250
|
+
type LowCardinality(Nullable(String)) DEFAULT 'web',
|
|
231
251
|
domain Nullable(String),
|
|
232
252
|
allowed_origins Nullable(String),
|
|
233
253
|
conversion_events Nullable(String),
|
|
@@ -243,6 +263,65 @@ function toCHDateTime(d) {
|
|
|
243
263
|
const iso = typeof d === "string" ? d : d.toISOString();
|
|
244
264
|
return iso.replace("T", " ").replace("Z", "");
|
|
245
265
|
}
|
|
266
|
+
function normalizedUtmSourceExpr() {
|
|
267
|
+
return `multiIf(
|
|
268
|
+
lower(utm_source) IN ('ig','instagram','instagram.com'), 'Instagram',
|
|
269
|
+
lower(utm_source) IN ('fb','facebook','facebook.com','fb.com'), 'Facebook',
|
|
270
|
+
lower(utm_source) IN ('tw','twitter','twitter.com','x','x.com','t.co'), 'X (Twitter)',
|
|
271
|
+
lower(utm_source) IN ('li','linkedin','linkedin.com'), 'LinkedIn',
|
|
272
|
+
lower(utm_source) IN ('yt','youtube','youtube.com'), 'YouTube',
|
|
273
|
+
lower(utm_source) IN ('goog','google','google.com'), 'Google',
|
|
274
|
+
lower(utm_source) IN ('gh','github','github.com'), 'GitHub',
|
|
275
|
+
lower(utm_source) IN ('reddit','reddit.com'), 'Reddit',
|
|
276
|
+
lower(utm_source) IN ('pinterest','pinterest.com'), 'Pinterest',
|
|
277
|
+
lower(utm_source) IN ('tiktok','tiktok.com'), 'TikTok',
|
|
278
|
+
lower(utm_source) IN ('openai','chatgpt','chat.openai.com'), 'OpenAI',
|
|
279
|
+
lower(utm_source) IN ('perplexity','perplexity.ai'), 'Perplexity',
|
|
280
|
+
utm_source
|
|
281
|
+
)`;
|
|
282
|
+
}
|
|
283
|
+
function normalizedUtmMediumExpr() {
|
|
284
|
+
return `multiIf(
|
|
285
|
+
lower(utm_medium) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid'), 'Paid',
|
|
286
|
+
lower(utm_medium) IN ('organic'), 'Organic',
|
|
287
|
+
lower(utm_medium) IN ('social','social-media','social_media'), 'Social',
|
|
288
|
+
lower(utm_medium) IN ('email','e-mail','e_mail'), 'Email',
|
|
289
|
+
lower(utm_medium) IN ('display','banner','cpm'), 'Display',
|
|
290
|
+
lower(utm_medium) IN ('affiliate'), 'Affiliate',
|
|
291
|
+
lower(utm_medium) IN ('referral'), 'Referral',
|
|
292
|
+
utm_medium
|
|
293
|
+
)`;
|
|
294
|
+
}
|
|
295
|
+
function channelClassificationExpr() {
|
|
296
|
+
return `multiIf(
|
|
297
|
+
lower(ifNull(utm_medium,'')) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')
|
|
298
|
+
AND (lower(ifNull(utm_source,'')) IN ('google','goog','bing','yahoo','duckduckgo','ecosia','baidu','yandex')
|
|
299
|
+
OR multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['google','bing','yahoo','duckduckgo','ecosia','baidu','yandex','search.brave']) > 0),
|
|
300
|
+
'Paid Search',
|
|
301
|
+
lower(ifNull(utm_medium,'')) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')
|
|
302
|
+
AND (lower(ifNull(utm_source,'')) IN ('instagram','ig','facebook','fb','twitter','tw','x','linkedin','li','youtube','yt','tiktok','pinterest','reddit','snapchat')
|
|
303
|
+
OR multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['instagram','facebook','twitter','x.com','t.co','linkedin','youtube','tiktok','pinterest','reddit','snapchat']) > 0),
|
|
304
|
+
'Paid Social',
|
|
305
|
+
lower(ifNull(utm_medium,'')) IN ('email','e-mail','e_mail'),
|
|
306
|
+
'Email',
|
|
307
|
+
lower(ifNull(utm_medium,'')) IN ('display','banner','cpm'),
|
|
308
|
+
'Display',
|
|
309
|
+
lower(ifNull(utm_medium,'')) IN ('affiliate'),
|
|
310
|
+
'Affiliate',
|
|
311
|
+
multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['google','bing','yahoo','duckduckgo','ecosia','baidu','yandex','search.brave']) > 0
|
|
312
|
+
AND (ifNull(utm_medium,'') = '' OR lower(utm_medium) NOT IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')),
|
|
313
|
+
'Organic Search',
|
|
314
|
+
(multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['instagram','facebook','twitter','x.com','t.co','linkedin','youtube','tiktok','pinterest','reddit','snapchat','mastodon','tumblr']) > 0
|
|
315
|
+
OR lower(ifNull(utm_source,'')) IN ('instagram','ig','facebook','fb','twitter','tw','x','linkedin','li','youtube','yt','tiktok','pinterest','reddit','snapchat'))
|
|
316
|
+
AND (ifNull(utm_medium,'') = '' OR lower(utm_medium) NOT IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')),
|
|
317
|
+
'Organic Social',
|
|
318
|
+
ifNull(referrer,'') != '' AND length(ifNull(referrer,'')) > 0,
|
|
319
|
+
'Referral',
|
|
320
|
+
(ifNull(utm_source,'') != '' OR ifNull(utm_medium,'') != '' OR ifNull(utm_campaign,'') != ''),
|
|
321
|
+
'Other',
|
|
322
|
+
'Direct'
|
|
323
|
+
)`;
|
|
324
|
+
}
|
|
246
325
|
function buildFilterConditions(filters) {
|
|
247
326
|
if (!filters) return { conditions: [], params: {} };
|
|
248
327
|
const map = {
|
|
@@ -253,6 +332,10 @@ function buildFilterConditions(filters) {
|
|
|
253
332
|
"device.type": "device_type",
|
|
254
333
|
"device.browser": "browser",
|
|
255
334
|
"device.os": "os",
|
|
335
|
+
"device.osVersion": "os_version",
|
|
336
|
+
"device.deviceModel": "device_model",
|
|
337
|
+
"device.deviceBrand": "device_brand",
|
|
338
|
+
"device.appVersion": "app_version",
|
|
256
339
|
"utm.source": "utm_source",
|
|
257
340
|
"utm.medium": "utm_medium",
|
|
258
341
|
"utm.campaign": "utm_campaign",
|
|
@@ -269,7 +352,14 @@ function buildFilterConditions(filters) {
|
|
|
269
352
|
const conditions = [];
|
|
270
353
|
const params = {};
|
|
271
354
|
for (const [key, value] of Object.entries(filters)) {
|
|
272
|
-
if (!value
|
|
355
|
+
if (!value) continue;
|
|
356
|
+
if (key === "channel") {
|
|
357
|
+
const paramKey2 = "f_channel";
|
|
358
|
+
conditions.push(`${channelClassificationExpr()} = {${paramKey2}:String}`);
|
|
359
|
+
params[paramKey2] = value;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (!map[key]) continue;
|
|
273
363
|
const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
274
364
|
conditions.push(`${map[key]} = {${paramKey}:String}`);
|
|
275
365
|
params[paramKey] = value;
|
|
@@ -289,6 +379,7 @@ var ClickHouseAdapter = class {
|
|
|
289
379
|
async init() {
|
|
290
380
|
await this.client.command({ query: CREATE_EVENTS_TABLE });
|
|
291
381
|
await this.client.command({ query: CREATE_SITES_TABLE });
|
|
382
|
+
await this.client.command({ query: CREATE_IDENTITY_MAP_TABLE });
|
|
292
383
|
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_source LowCardinality(Nullable(String))` });
|
|
293
384
|
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_subtype LowCardinality(Nullable(String))` });
|
|
294
385
|
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS page_path Nullable(String)` });
|
|
@@ -299,6 +390,14 @@ var ClickHouseAdapter = class {
|
|
|
299
390
|
await this.client.command({
|
|
300
391
|
query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
|
|
301
392
|
});
|
|
393
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS os_version LowCardinality(Nullable(String))` });
|
|
394
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS device_model LowCardinality(Nullable(String))` });
|
|
395
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS device_brand LowCardinality(Nullable(String))` });
|
|
396
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS app_version LowCardinality(Nullable(String))` });
|
|
397
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS app_build Nullable(String)` });
|
|
398
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS sdk_name LowCardinality(Nullable(String))` });
|
|
399
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS sdk_version LowCardinality(Nullable(String))` });
|
|
400
|
+
await this.client.command({ query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS type LowCardinality(Nullable(String)) DEFAULT 'web'` });
|
|
302
401
|
}
|
|
303
402
|
async close() {
|
|
304
403
|
await this.client.close();
|
|
@@ -341,7 +440,14 @@ var ClickHouseAdapter = class {
|
|
|
341
440
|
utm_campaign: e.utm?.campaign ?? null,
|
|
342
441
|
utm_term: e.utm?.term ?? null,
|
|
343
442
|
utm_content: e.utm?.content ?? null,
|
|
344
|
-
ip: e.ip ?? null
|
|
443
|
+
ip: e.ip ?? null,
|
|
444
|
+
os_version: e.device?.osVersion ?? null,
|
|
445
|
+
device_model: e.device?.deviceModel ?? null,
|
|
446
|
+
device_brand: e.device?.deviceBrand ?? null,
|
|
447
|
+
app_version: e.device?.appVersion ?? null,
|
|
448
|
+
app_build: e.device?.appBuild ?? null,
|
|
449
|
+
sdk_name: e.device?.sdkName ?? null,
|
|
450
|
+
sdk_version: e.device?.sdkVersion ?? null
|
|
345
451
|
}));
|
|
346
452
|
await this.client.insert({
|
|
347
453
|
table: EVENTS_TABLE,
|
|
@@ -546,8 +652,8 @@ var ClickHouseAdapter = class {
|
|
|
546
652
|
}
|
|
547
653
|
case "top_exit_pages": {
|
|
548
654
|
const rows = await this.queryRows(
|
|
549
|
-
`SELECT
|
|
550
|
-
SELECT session_id, argMax(url, timestamp) AS
|
|
655
|
+
`SELECT exit_url AS key, count() AS value FROM (
|
|
656
|
+
SELECT session_id, argMax(url, timestamp) AS exit_url
|
|
551
657
|
FROM ${EVENTS_TABLE}
|
|
552
658
|
WHERE site_id = {siteId:String}
|
|
553
659
|
AND timestamp >= {from:String}
|
|
@@ -556,7 +662,7 @@ var ClickHouseAdapter = class {
|
|
|
556
662
|
AND url IS NOT NULL${filterSql}
|
|
557
663
|
GROUP BY session_id
|
|
558
664
|
)
|
|
559
|
-
GROUP BY
|
|
665
|
+
GROUP BY exit_url
|
|
560
666
|
ORDER BY value DESC
|
|
561
667
|
LIMIT {limit:UInt32}`,
|
|
562
668
|
{ ...params, ...filter.params }
|
|
@@ -567,9 +673,9 @@ var ClickHouseAdapter = class {
|
|
|
567
673
|
}
|
|
568
674
|
case "top_transitions": {
|
|
569
675
|
const rows = await this.queryRows(
|
|
570
|
-
`SELECT concat(prev_url, ' \u2192 ',
|
|
571
|
-
SELECT session_id, url,
|
|
572
|
-
|
|
676
|
+
`SELECT concat(prev_url, ' \u2192 ', curr_url) AS key, count() AS value FROM (
|
|
677
|
+
SELECT session_id, url AS curr_url,
|
|
678
|
+
lagInFrame(url, 1) OVER (PARTITION BY session_id ORDER BY timestamp ASC) AS prev_url
|
|
573
679
|
FROM ${EVENTS_TABLE}
|
|
574
680
|
WHERE site_id = {siteId:String}
|
|
575
681
|
AND timestamp >= {from:String}
|
|
@@ -577,7 +683,7 @@ var ClickHouseAdapter = class {
|
|
|
577
683
|
AND type = 'pageview'
|
|
578
684
|
AND url IS NOT NULL${filterSql}
|
|
579
685
|
)
|
|
580
|
-
WHERE prev_url IS NOT NULL
|
|
686
|
+
WHERE prev_url IS NOT NULL AND prev_url != ''
|
|
581
687
|
GROUP BY key
|
|
582
688
|
ORDER BY value DESC
|
|
583
689
|
LIMIT {limit:UInt32}`,
|
|
@@ -692,6 +798,159 @@ var ClickHouseAdapter = class {
|
|
|
692
798
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
693
799
|
break;
|
|
694
800
|
}
|
|
801
|
+
case "top_os_versions": {
|
|
802
|
+
const rows = await this.queryRows(
|
|
803
|
+
`SELECT concat(os, ' ', ifNull(os_version, '')) AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
804
|
+
WHERE site_id = {siteId:String}
|
|
805
|
+
AND timestamp >= {from:String}
|
|
806
|
+
AND timestamp <= {to:String}
|
|
807
|
+
AND os IS NOT NULL
|
|
808
|
+
AND os_version IS NOT NULL
|
|
809
|
+
${filterSql}
|
|
810
|
+
GROUP BY key
|
|
811
|
+
ORDER BY value DESC
|
|
812
|
+
LIMIT {limit:UInt32}`,
|
|
813
|
+
{ ...params, ...filter.params }
|
|
814
|
+
);
|
|
815
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
816
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
case "top_device_models": {
|
|
820
|
+
const rows = await this.queryRows(
|
|
821
|
+
`SELECT trim(concat(ifNull(device_brand, ''), ' ', device_model)) AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
822
|
+
WHERE site_id = {siteId:String}
|
|
823
|
+
AND timestamp >= {from:String}
|
|
824
|
+
AND timestamp <= {to:String}
|
|
825
|
+
AND device_model IS NOT NULL
|
|
826
|
+
${filterSql}
|
|
827
|
+
GROUP BY key
|
|
828
|
+
ORDER BY value DESC
|
|
829
|
+
LIMIT {limit:UInt32}`,
|
|
830
|
+
{ ...params, ...filter.params }
|
|
831
|
+
);
|
|
832
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
833
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
case "top_app_versions": {
|
|
837
|
+
const rows = await this.queryRows(
|
|
838
|
+
`SELECT app_version AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
839
|
+
WHERE site_id = {siteId:String}
|
|
840
|
+
AND timestamp >= {from:String}
|
|
841
|
+
AND timestamp <= {to:String}
|
|
842
|
+
AND app_version IS NOT NULL
|
|
843
|
+
${filterSql}
|
|
844
|
+
GROUP BY app_version
|
|
845
|
+
ORDER BY value DESC
|
|
846
|
+
LIMIT {limit:UInt32}`,
|
|
847
|
+
{ ...params, ...filter.params }
|
|
848
|
+
);
|
|
849
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
850
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
case "top_utm_sources": {
|
|
854
|
+
const rows = await this.queryRows(
|
|
855
|
+
`SELECT ${normalizedUtmSourceExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
856
|
+
WHERE site_id = {siteId:String}
|
|
857
|
+
AND timestamp >= {from:String}
|
|
858
|
+
AND timestamp <= {to:String}
|
|
859
|
+
AND utm_source IS NOT NULL AND utm_source != ''
|
|
860
|
+
${filterSql}
|
|
861
|
+
GROUP BY key
|
|
862
|
+
ORDER BY value DESC
|
|
863
|
+
LIMIT {limit:UInt32}`,
|
|
864
|
+
{ ...params, ...filter.params }
|
|
865
|
+
);
|
|
866
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
867
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
case "top_utm_mediums": {
|
|
871
|
+
const rows = await this.queryRows(
|
|
872
|
+
`SELECT ${normalizedUtmMediumExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
873
|
+
WHERE site_id = {siteId:String}
|
|
874
|
+
AND timestamp >= {from:String}
|
|
875
|
+
AND timestamp <= {to:String}
|
|
876
|
+
AND utm_medium IS NOT NULL AND utm_medium != ''
|
|
877
|
+
${filterSql}
|
|
878
|
+
GROUP BY key
|
|
879
|
+
ORDER BY value DESC
|
|
880
|
+
LIMIT {limit:UInt32}`,
|
|
881
|
+
{ ...params, ...filter.params }
|
|
882
|
+
);
|
|
883
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
884
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
case "top_utm_campaigns": {
|
|
888
|
+
const rows = await this.queryRows(
|
|
889
|
+
`SELECT utm_campaign AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
890
|
+
WHERE site_id = {siteId:String}
|
|
891
|
+
AND timestamp >= {from:String}
|
|
892
|
+
AND timestamp <= {to:String}
|
|
893
|
+
AND utm_campaign IS NOT NULL AND utm_campaign != ''
|
|
894
|
+
${filterSql}
|
|
895
|
+
GROUP BY utm_campaign
|
|
896
|
+
ORDER BY value DESC
|
|
897
|
+
LIMIT {limit:UInt32}`,
|
|
898
|
+
{ ...params, ...filter.params }
|
|
899
|
+
);
|
|
900
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
901
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
case "top_utm_terms": {
|
|
905
|
+
const rows = await this.queryRows(
|
|
906
|
+
`SELECT utm_term AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
907
|
+
WHERE site_id = {siteId:String}
|
|
908
|
+
AND timestamp >= {from:String}
|
|
909
|
+
AND timestamp <= {to:String}
|
|
910
|
+
AND utm_term IS NOT NULL AND utm_term != ''
|
|
911
|
+
${filterSql}
|
|
912
|
+
GROUP BY utm_term
|
|
913
|
+
ORDER BY value DESC
|
|
914
|
+
LIMIT {limit:UInt32}`,
|
|
915
|
+
{ ...params, ...filter.params }
|
|
916
|
+
);
|
|
917
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
918
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
case "top_utm_contents": {
|
|
922
|
+
const rows = await this.queryRows(
|
|
923
|
+
`SELECT utm_content AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
924
|
+
WHERE site_id = {siteId:String}
|
|
925
|
+
AND timestamp >= {from:String}
|
|
926
|
+
AND timestamp <= {to:String}
|
|
927
|
+
AND utm_content IS NOT NULL AND utm_content != ''
|
|
928
|
+
${filterSql}
|
|
929
|
+
GROUP BY utm_content
|
|
930
|
+
ORDER BY value DESC
|
|
931
|
+
LIMIT {limit:UInt32}`,
|
|
932
|
+
{ ...params, ...filter.params }
|
|
933
|
+
);
|
|
934
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
935
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
case "top_channels": {
|
|
939
|
+
const rows = await this.queryRows(
|
|
940
|
+
`SELECT ${channelClassificationExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
941
|
+
WHERE site_id = {siteId:String}
|
|
942
|
+
AND timestamp >= {from:String}
|
|
943
|
+
AND timestamp <= {to:String}
|
|
944
|
+
${filterSql}
|
|
945
|
+
GROUP BY key
|
|
946
|
+
ORDER BY value DESC
|
|
947
|
+
LIMIT {limit:UInt32}`,
|
|
948
|
+
{ ...params, ...filter.params }
|
|
949
|
+
);
|
|
950
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
951
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
695
954
|
}
|
|
696
955
|
const result = { metric: q.metric, period, data, total };
|
|
697
956
|
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
@@ -945,45 +1204,59 @@ var ClickHouseAdapter = class {
|
|
|
945
1204
|
const where = conditions.join(" AND ");
|
|
946
1205
|
const [userRows, countRows] = await Promise.all([
|
|
947
1206
|
this.queryRows(
|
|
948
|
-
`
|
|
949
|
-
visitor_id,
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1207
|
+
`WITH identity AS (
|
|
1208
|
+
SELECT visitor_id, user_id
|
|
1209
|
+
FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1210
|
+
WHERE site_id = {siteId:String}
|
|
1211
|
+
)
|
|
1212
|
+
SELECT
|
|
1213
|
+
if(i.user_id IS NOT NULL AND i.user_id != '', i.user_id, e.visitor_id) AS group_key,
|
|
1214
|
+
anyLast(e.visitor_id) AS visitor_id,
|
|
1215
|
+
anyLast(i.user_id) AS userId,
|
|
1216
|
+
anyLast(e.traits) AS traits,
|
|
1217
|
+
min(e.timestamp) AS firstSeen,
|
|
1218
|
+
max(e.timestamp) AS lastSeen,
|
|
954
1219
|
count() AS totalEvents,
|
|
955
|
-
countIf(type = 'pageview') AS totalPageviews,
|
|
956
|
-
uniq(session_id) AS totalSessions,
|
|
957
|
-
anyLast(url) AS lastUrl,
|
|
958
|
-
anyLast(referrer) AS referrer,
|
|
959
|
-
anyLast(device_type) AS device_type,
|
|
960
|
-
anyLast(browser) AS browser,
|
|
961
|
-
anyLast(os) AS os,
|
|
962
|
-
anyLast(country) AS country,
|
|
963
|
-
anyLast(city) AS city,
|
|
964
|
-
anyLast(region) AS region,
|
|
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
|
|
974
|
-
FROM ${EVENTS_TABLE}
|
|
975
|
-
|
|
976
|
-
|
|
1220
|
+
countIf(e.type = 'pageview') AS totalPageviews,
|
|
1221
|
+
uniq(e.session_id) AS totalSessions,
|
|
1222
|
+
anyLast(e.url) AS lastUrl,
|
|
1223
|
+
anyLast(e.referrer) AS referrer,
|
|
1224
|
+
anyLast(e.device_type) AS device_type,
|
|
1225
|
+
anyLast(e.browser) AS browser,
|
|
1226
|
+
anyLast(e.os) AS os,
|
|
1227
|
+
anyLast(e.country) AS country,
|
|
1228
|
+
anyLast(e.city) AS city,
|
|
1229
|
+
anyLast(e.region) AS region,
|
|
1230
|
+
anyLast(e.language) AS language,
|
|
1231
|
+
anyLast(e.timezone) AS timezone,
|
|
1232
|
+
anyLast(e.screen_width) AS screen_width,
|
|
1233
|
+
anyLast(e.screen_height) AS screen_height,
|
|
1234
|
+
anyLast(e.utm_source) AS utm_source,
|
|
1235
|
+
anyLast(e.utm_medium) AS utm_medium,
|
|
1236
|
+
anyLast(e.utm_campaign) AS utm_campaign,
|
|
1237
|
+
anyLast(e.utm_term) AS utm_term,
|
|
1238
|
+
anyLast(e.utm_content) AS utm_content
|
|
1239
|
+
FROM ${EVENTS_TABLE} e
|
|
1240
|
+
LEFT JOIN identity i ON e.visitor_id = i.visitor_id
|
|
1241
|
+
WHERE e.site_id = {siteId:String}${where.includes("ILIKE") ? ` AND (e.visitor_id ILIKE {search:String} OR i.user_id ILIKE {search:String})` : ""}
|
|
1242
|
+
GROUP BY group_key
|
|
977
1243
|
ORDER BY lastSeen DESC
|
|
978
1244
|
LIMIT {limit:UInt32}
|
|
979
1245
|
OFFSET {offset:UInt32}`,
|
|
980
1246
|
queryParams
|
|
981
1247
|
),
|
|
982
1248
|
this.queryRows(
|
|
983
|
-
`
|
|
984
|
-
SELECT visitor_id
|
|
985
|
-
|
|
986
|
-
|
|
1249
|
+
`WITH identity AS (
|
|
1250
|
+
SELECT visitor_id, user_id
|
|
1251
|
+
FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1252
|
+
WHERE site_id = {siteId:String}
|
|
1253
|
+
)
|
|
1254
|
+
SELECT count() AS total FROM (
|
|
1255
|
+
SELECT if(i.user_id IS NOT NULL AND i.user_id != '', i.user_id, e.visitor_id) AS group_key
|
|
1256
|
+
FROM ${EVENTS_TABLE} e
|
|
1257
|
+
LEFT JOIN identity i ON e.visitor_id = i.visitor_id
|
|
1258
|
+
WHERE e.site_id = {siteId:String}${where.includes("ILIKE") ? ` AND (e.visitor_id ILIKE {search:String} OR i.user_id ILIKE {search:String})` : ""}
|
|
1259
|
+
GROUP BY group_key
|
|
987
1260
|
)`,
|
|
988
1261
|
queryParams
|
|
989
1262
|
)
|
|
@@ -1019,13 +1292,178 @@ var ClickHouseAdapter = class {
|
|
|
1019
1292
|
offset
|
|
1020
1293
|
};
|
|
1021
1294
|
}
|
|
1022
|
-
async getUserDetail(siteId,
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1295
|
+
async getUserDetail(siteId, identifier) {
|
|
1296
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
1297
|
+
if (visitorIds.length > 0) {
|
|
1298
|
+
return this.getMergedUserDetail(siteId, identifier, visitorIds);
|
|
1299
|
+
}
|
|
1300
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
1301
|
+
if (userId) {
|
|
1302
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
1303
|
+
return this.getMergedUserDetail(siteId, userId, allVisitorIds.length > 0 ? allVisitorIds : [identifier]);
|
|
1304
|
+
}
|
|
1305
|
+
const result = await this.listUsers({ siteId, search: identifier, limit: 1 });
|
|
1306
|
+
const user = result.users.find((u) => u.visitorId === identifier);
|
|
1025
1307
|
return user ?? null;
|
|
1026
1308
|
}
|
|
1027
|
-
async getUserEvents(siteId,
|
|
1028
|
-
|
|
1309
|
+
async getUserEvents(siteId, identifier, params) {
|
|
1310
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
1311
|
+
if (visitorIds.length > 0) {
|
|
1312
|
+
return this.listEventsForVisitorIds(siteId, visitorIds, params);
|
|
1313
|
+
}
|
|
1314
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
1315
|
+
if (userId) {
|
|
1316
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
1317
|
+
if (allVisitorIds.length > 0) {
|
|
1318
|
+
return this.listEventsForVisitorIds(siteId, allVisitorIds, params);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return this.listEvents({ ...params, siteId, visitorId: identifier });
|
|
1322
|
+
}
|
|
1323
|
+
// ─── Identity Mapping ──────────────────────────────────────
|
|
1324
|
+
async upsertIdentity(siteId, visitorId, userId) {
|
|
1325
|
+
await this.client.insert({
|
|
1326
|
+
table: IDENTITY_MAP_TABLE,
|
|
1327
|
+
values: [{
|
|
1328
|
+
site_id: siteId,
|
|
1329
|
+
visitor_id: visitorId,
|
|
1330
|
+
user_id: userId,
|
|
1331
|
+
identified_at: toCHDateTime(/* @__PURE__ */ new Date())
|
|
1332
|
+
}],
|
|
1333
|
+
format: "JSONEachRow"
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
async getVisitorIdsForUser(siteId, userId) {
|
|
1337
|
+
const rows = await this.queryRows(
|
|
1338
|
+
`SELECT visitor_id FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1339
|
+
WHERE site_id = {siteId:String} AND user_id = {userId:String}`,
|
|
1340
|
+
{ siteId, userId }
|
|
1341
|
+
);
|
|
1342
|
+
return rows.map((r) => r.visitor_id);
|
|
1343
|
+
}
|
|
1344
|
+
async getUserIdForVisitor(siteId, visitorId) {
|
|
1345
|
+
const rows = await this.queryRows(
|
|
1346
|
+
`SELECT user_id FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1347
|
+
WHERE site_id = {siteId:String} AND visitor_id = {visitorId:String}
|
|
1348
|
+
LIMIT 1`,
|
|
1349
|
+
{ siteId, visitorId }
|
|
1350
|
+
);
|
|
1351
|
+
return rows.length > 0 ? rows[0].user_id : null;
|
|
1352
|
+
}
|
|
1353
|
+
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
1354
|
+
const rows = await this.queryRows(
|
|
1355
|
+
`SELECT
|
|
1356
|
+
anyLast(visitor_id) AS last_visitor_id,
|
|
1357
|
+
anyLast(traits) AS traits,
|
|
1358
|
+
min(timestamp) AS firstSeen,
|
|
1359
|
+
max(timestamp) AS lastSeen,
|
|
1360
|
+
count() AS totalEvents,
|
|
1361
|
+
countIf(type = 'pageview') AS totalPageviews,
|
|
1362
|
+
uniq(session_id) AS totalSessions,
|
|
1363
|
+
anyLast(url) AS lastUrl,
|
|
1364
|
+
anyLast(referrer) AS referrer,
|
|
1365
|
+
anyLast(device_type) AS device_type,
|
|
1366
|
+
anyLast(browser) AS browser,
|
|
1367
|
+
anyLast(os) AS os,
|
|
1368
|
+
anyLast(country) AS country,
|
|
1369
|
+
anyLast(city) AS city,
|
|
1370
|
+
anyLast(region) AS region,
|
|
1371
|
+
anyLast(language) AS language,
|
|
1372
|
+
anyLast(timezone) AS timezone,
|
|
1373
|
+
anyLast(screen_width) AS screen_width,
|
|
1374
|
+
anyLast(screen_height) AS screen_height,
|
|
1375
|
+
anyLast(utm_source) AS utm_source,
|
|
1376
|
+
anyLast(utm_medium) AS utm_medium,
|
|
1377
|
+
anyLast(utm_campaign) AS utm_campaign,
|
|
1378
|
+
anyLast(utm_term) AS utm_term,
|
|
1379
|
+
anyLast(utm_content) AS utm_content
|
|
1380
|
+
FROM ${EVENTS_TABLE}
|
|
1381
|
+
WHERE site_id = {siteId:String}
|
|
1382
|
+
AND visitor_id IN {visitorIds:Array(String)}`,
|
|
1383
|
+
{ siteId, visitorIds }
|
|
1384
|
+
);
|
|
1385
|
+
if (rows.length === 0) return null;
|
|
1386
|
+
const u = rows[0];
|
|
1387
|
+
return {
|
|
1388
|
+
visitorId: String(u.last_visitor_id),
|
|
1389
|
+
visitorIds,
|
|
1390
|
+
userId,
|
|
1391
|
+
traits: this.parseJSON(u.traits),
|
|
1392
|
+
firstSeen: new Date(String(u.firstSeen)).toISOString(),
|
|
1393
|
+
lastSeen: new Date(String(u.lastSeen)).toISOString(),
|
|
1394
|
+
totalEvents: Number(u.totalEvents),
|
|
1395
|
+
totalPageviews: Number(u.totalPageviews),
|
|
1396
|
+
totalSessions: Number(u.totalSessions),
|
|
1397
|
+
lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
|
|
1398
|
+
referrer: u.referrer ? String(u.referrer) : void 0,
|
|
1399
|
+
device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
|
|
1400
|
+
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,
|
|
1401
|
+
language: u.language ? String(u.language) : void 0,
|
|
1402
|
+
timezone: u.timezone ? String(u.timezone) : void 0,
|
|
1403
|
+
screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
|
|
1404
|
+
utm: u.utm_source ? {
|
|
1405
|
+
source: String(u.utm_source),
|
|
1406
|
+
medium: u.utm_medium ? String(u.utm_medium) : void 0,
|
|
1407
|
+
campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
|
|
1408
|
+
term: u.utm_term ? String(u.utm_term) : void 0,
|
|
1409
|
+
content: u.utm_content ? String(u.utm_content) : void 0
|
|
1410
|
+
} : void 0
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
async listEventsForVisitorIds(siteId, visitorIds, params) {
|
|
1414
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
1415
|
+
const offset = params.offset ?? 0;
|
|
1416
|
+
const conditions = [`site_id = {siteId:String}`, `visitor_id IN {visitorIds:Array(String)}`];
|
|
1417
|
+
const queryParams = { siteId, visitorIds, limit, offset };
|
|
1418
|
+
if (params.type) {
|
|
1419
|
+
conditions.push(`type = {type:String}`);
|
|
1420
|
+
queryParams.type = params.type;
|
|
1421
|
+
}
|
|
1422
|
+
if (params.eventName) {
|
|
1423
|
+
conditions.push(`event_name = {eventName:String}`);
|
|
1424
|
+
queryParams.eventName = params.eventName;
|
|
1425
|
+
}
|
|
1426
|
+
if (params.eventNames && params.eventNames.length > 0) {
|
|
1427
|
+
conditions.push(`event_name IN {eventNames:Array(String)}`);
|
|
1428
|
+
queryParams.eventNames = params.eventNames;
|
|
1429
|
+
}
|
|
1430
|
+
if (params.period || params.dateFrom) {
|
|
1431
|
+
const { dateRange } = resolvePeriod({
|
|
1432
|
+
period: params.period,
|
|
1433
|
+
dateFrom: params.dateFrom,
|
|
1434
|
+
dateTo: params.dateTo
|
|
1435
|
+
});
|
|
1436
|
+
conditions.push(`timestamp >= {from:String} AND timestamp <= {to:String}`);
|
|
1437
|
+
queryParams.from = toCHDateTime(dateRange.from);
|
|
1438
|
+
queryParams.to = toCHDateTime(dateRange.to);
|
|
1439
|
+
}
|
|
1440
|
+
const where = conditions.join(" AND ");
|
|
1441
|
+
const [events, countRows] = await Promise.all([
|
|
1442
|
+
this.queryRows(
|
|
1443
|
+
`SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
|
|
1444
|
+
event_name, properties, event_source, event_subtype, page_path, target_url_path,
|
|
1445
|
+
element_selector, element_text, scroll_depth_pct,
|
|
1446
|
+
user_id, traits, country, city, region,
|
|
1447
|
+
device_type, browser, os, language,
|
|
1448
|
+
utm_source, utm_medium, utm_campaign, utm_term, utm_content
|
|
1449
|
+
FROM ${EVENTS_TABLE}
|
|
1450
|
+
WHERE ${where}
|
|
1451
|
+
ORDER BY timestamp DESC
|
|
1452
|
+
LIMIT {limit:UInt32}
|
|
1453
|
+
OFFSET {offset:UInt32}`,
|
|
1454
|
+
queryParams
|
|
1455
|
+
),
|
|
1456
|
+
this.queryRows(
|
|
1457
|
+
`SELECT count() AS total FROM ${EVENTS_TABLE} WHERE ${where}`,
|
|
1458
|
+
queryParams
|
|
1459
|
+
)
|
|
1460
|
+
]);
|
|
1461
|
+
return {
|
|
1462
|
+
events: events.map((e) => this.toEventListItem(e)),
|
|
1463
|
+
total: Number(countRows[0]?.total ?? 0),
|
|
1464
|
+
limit,
|
|
1465
|
+
offset
|
|
1466
|
+
};
|
|
1029
1467
|
}
|
|
1030
1468
|
// ─── Site Management ──────────────────────────────────────
|
|
1031
1469
|
async createSite(data) {
|
|
@@ -1036,6 +1474,7 @@ var ClickHouseAdapter = class {
|
|
|
1036
1474
|
siteId: generateSiteId(),
|
|
1037
1475
|
secretKey: generateSecretKey(),
|
|
1038
1476
|
name: data.name,
|
|
1477
|
+
type: data.type ?? "web",
|
|
1039
1478
|
domain: data.domain,
|
|
1040
1479
|
allowedOrigins: data.allowedOrigins,
|
|
1041
1480
|
conversionEvents: data.conversionEvents,
|
|
@@ -1048,6 +1487,7 @@ var ClickHouseAdapter = class {
|
|
|
1048
1487
|
site_id: site.siteId,
|
|
1049
1488
|
secret_key: site.secretKey,
|
|
1050
1489
|
name: site.name,
|
|
1490
|
+
type: site.type ?? "web",
|
|
1051
1491
|
domain: site.domain ?? null,
|
|
1052
1492
|
allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
|
|
1053
1493
|
conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
|
|
@@ -1062,7 +1502,7 @@ var ClickHouseAdapter = class {
|
|
|
1062
1502
|
}
|
|
1063
1503
|
async getSite(siteId) {
|
|
1064
1504
|
const rows = await this.queryRows(
|
|
1065
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1505
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1066
1506
|
FROM ${SITES_TABLE} FINAL
|
|
1067
1507
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1068
1508
|
{ siteId }
|
|
@@ -1071,7 +1511,7 @@ var ClickHouseAdapter = class {
|
|
|
1071
1511
|
}
|
|
1072
1512
|
async getSiteBySecret(secretKey) {
|
|
1073
1513
|
const rows = await this.queryRows(
|
|
1074
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1514
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1075
1515
|
FROM ${SITES_TABLE} FINAL
|
|
1076
1516
|
WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
|
|
1077
1517
|
{ secretKey }
|
|
@@ -1080,7 +1520,7 @@ var ClickHouseAdapter = class {
|
|
|
1080
1520
|
}
|
|
1081
1521
|
async listSites() {
|
|
1082
1522
|
const rows = await this.queryRows(
|
|
1083
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1523
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1084
1524
|
FROM ${SITES_TABLE} FINAL
|
|
1085
1525
|
WHERE is_deleted = 0
|
|
1086
1526
|
ORDER BY created_at DESC`,
|
|
@@ -1090,7 +1530,7 @@ var ClickHouseAdapter = class {
|
|
|
1090
1530
|
}
|
|
1091
1531
|
async updateSite(siteId, data) {
|
|
1092
1532
|
const currentRows = await this.queryRows(
|
|
1093
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
1533
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
1094
1534
|
FROM ${SITES_TABLE} FINAL
|
|
1095
1535
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1096
1536
|
{ siteId }
|
|
@@ -1102,6 +1542,7 @@ var ClickHouseAdapter = class {
|
|
|
1102
1542
|
const nowCH = toCHDateTime(now);
|
|
1103
1543
|
const newVersion = Number(current.version) + 1;
|
|
1104
1544
|
const newName = data.name !== void 0 ? data.name : String(current.name);
|
|
1545
|
+
const newType = data.type !== void 0 ? data.type : current.type ? String(current.type) : "web";
|
|
1105
1546
|
const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
|
|
1106
1547
|
const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
|
|
1107
1548
|
const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
|
|
@@ -1111,6 +1552,7 @@ var ClickHouseAdapter = class {
|
|
|
1111
1552
|
site_id: String(current.site_id),
|
|
1112
1553
|
secret_key: String(current.secret_key),
|
|
1113
1554
|
name: newName,
|
|
1555
|
+
type: newType,
|
|
1114
1556
|
domain: newDomain,
|
|
1115
1557
|
allowed_origins: newOrigins,
|
|
1116
1558
|
conversion_events: newConversions,
|
|
@@ -1125,6 +1567,7 @@ var ClickHouseAdapter = class {
|
|
|
1125
1567
|
siteId: String(current.site_id),
|
|
1126
1568
|
secretKey: String(current.secret_key),
|
|
1127
1569
|
name: newName,
|
|
1570
|
+
type: newType,
|
|
1128
1571
|
domain: newDomain ?? void 0,
|
|
1129
1572
|
allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
|
|
1130
1573
|
conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
|
|
@@ -1134,7 +1577,7 @@ var ClickHouseAdapter = class {
|
|
|
1134
1577
|
}
|
|
1135
1578
|
async deleteSite(siteId) {
|
|
1136
1579
|
const currentRows = await this.queryRows(
|
|
1137
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
1580
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, version
|
|
1138
1581
|
FROM ${SITES_TABLE} FINAL
|
|
1139
1582
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1140
1583
|
{ siteId }
|
|
@@ -1148,6 +1591,7 @@ var ClickHouseAdapter = class {
|
|
|
1148
1591
|
site_id: String(current.site_id),
|
|
1149
1592
|
secret_key: String(current.secret_key),
|
|
1150
1593
|
name: String(current.name),
|
|
1594
|
+
type: current.type ? String(current.type) : "web",
|
|
1151
1595
|
domain: current.domain ? String(current.domain) : null,
|
|
1152
1596
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1153
1597
|
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
@@ -1162,7 +1606,7 @@ var ClickHouseAdapter = class {
|
|
|
1162
1606
|
}
|
|
1163
1607
|
async regenerateSecret(siteId) {
|
|
1164
1608
|
const currentRows = await this.queryRows(
|
|
1165
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
1609
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, version
|
|
1166
1610
|
FROM ${SITES_TABLE} FINAL
|
|
1167
1611
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1168
1612
|
{ siteId }
|
|
@@ -1179,6 +1623,7 @@ var ClickHouseAdapter = class {
|
|
|
1179
1623
|
site_id: String(current.site_id),
|
|
1180
1624
|
secret_key: newSecret,
|
|
1181
1625
|
name: String(current.name),
|
|
1626
|
+
type: current.type ? String(current.type) : "web",
|
|
1182
1627
|
domain: current.domain ? String(current.domain) : null,
|
|
1183
1628
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1184
1629
|
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
@@ -1193,6 +1638,7 @@ var ClickHouseAdapter = class {
|
|
|
1193
1638
|
siteId: String(current.site_id),
|
|
1194
1639
|
secretKey: newSecret,
|
|
1195
1640
|
name: String(current.name),
|
|
1641
|
+
type: current.type ? String(current.type) : "web",
|
|
1196
1642
|
domain: current.domain ? String(current.domain) : void 0,
|
|
1197
1643
|
allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
|
|
1198
1644
|
conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
|
|
@@ -1214,6 +1660,7 @@ var ClickHouseAdapter = class {
|
|
|
1214
1660
|
siteId: String(row.site_id),
|
|
1215
1661
|
secretKey: String(row.secret_key),
|
|
1216
1662
|
name: String(row.name),
|
|
1663
|
+
type: row.type ? String(row.type) : "web",
|
|
1217
1664
|
domain: row.domain ? String(row.domain) : void 0,
|
|
1218
1665
|
allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
|
|
1219
1666
|
conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
|
|
@@ -1276,6 +1723,131 @@ var ClickHouseAdapter = class {
|
|
|
1276
1723
|
var import_mongodb = require("mongodb");
|
|
1277
1724
|
var EVENTS_COLLECTION = "litemetrics_events";
|
|
1278
1725
|
var SITES_COLLECTION = "litemetrics_sites";
|
|
1726
|
+
var IDENTITY_MAP_COLLECTION = "litemetrics_identity_map";
|
|
1727
|
+
function normalizedUtmSourceSwitch() {
|
|
1728
|
+
return {
|
|
1729
|
+
$switch: {
|
|
1730
|
+
branches: [
|
|
1731
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["ig", "instagram", "instagram.com"]] }, then: "Instagram" },
|
|
1732
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["fb", "facebook", "facebook.com", "fb.com"]] }, then: "Facebook" },
|
|
1733
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["tw", "twitter", "twitter.com", "x", "x.com", "t.co"]] }, then: "X (Twitter)" },
|
|
1734
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["li", "linkedin", "linkedin.com"]] }, then: "LinkedIn" },
|
|
1735
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["yt", "youtube", "youtube.com"]] }, then: "YouTube" },
|
|
1736
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["goog", "google", "google.com"]] }, then: "Google" },
|
|
1737
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["gh", "github", "github.com"]] }, then: "GitHub" },
|
|
1738
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["reddit", "reddit.com"]] }, then: "Reddit" },
|
|
1739
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["pinterest", "pinterest.com"]] }, then: "Pinterest" },
|
|
1740
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["tiktok", "tiktok.com"]] }, then: "TikTok" },
|
|
1741
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["openai", "chatgpt", "chat.openai.com"]] }, then: "OpenAI" },
|
|
1742
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["perplexity", "perplexity.ai"]] }, then: "Perplexity" }
|
|
1743
|
+
],
|
|
1744
|
+
default: "$utm_source"
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function normalizedUtmMediumSwitch() {
|
|
1749
|
+
return {
|
|
1750
|
+
$switch: {
|
|
1751
|
+
branches: [
|
|
1752
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["cpc", "ppc", "paidsearch", "paid-search", "paid_search", "paid"]] }, then: "Paid" },
|
|
1753
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["organic"]] }, then: "Organic" },
|
|
1754
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["social", "social-media", "social_media"]] }, then: "Social" },
|
|
1755
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["email", "e-mail", "e_mail"]] }, then: "Email" },
|
|
1756
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["display", "banner", "cpm"]] }, then: "Display" },
|
|
1757
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["affiliate"]] }, then: "Affiliate" },
|
|
1758
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["referral"]] }, then: "Referral" }
|
|
1759
|
+
],
|
|
1760
|
+
default: "$utm_medium"
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
var SEARCH_ENGINES = /google|bing|yahoo|duckduckgo|ecosia|baidu|yandex|search\.brave/i;
|
|
1765
|
+
var SOCIAL_NETWORKS = /instagram|facebook|twitter|x\.com|t\.co|linkedin|youtube|tiktok|pinterest|reddit|snapchat|mastodon|tumblr/i;
|
|
1766
|
+
var PAID_MEDIUMS = ["cpc", "ppc", "paidsearch", "paid-search", "paid_search", "paid"];
|
|
1767
|
+
var SOCIAL_SOURCES = ["instagram", "ig", "facebook", "fb", "twitter", "tw", "x", "linkedin", "li", "youtube", "yt", "tiktok", "pinterest", "reddit", "snapchat"];
|
|
1768
|
+
function channelClassificationSwitch() {
|
|
1769
|
+
const lMedium = { $toLower: { $ifNull: ["$utm_medium", ""] } };
|
|
1770
|
+
const lSource = { $toLower: { $ifNull: ["$utm_source", ""] } };
|
|
1771
|
+
const refStr = { $ifNull: ["$referrer", ""] };
|
|
1772
|
+
return {
|
|
1773
|
+
$switch: {
|
|
1774
|
+
branches: [
|
|
1775
|
+
// Paid Search
|
|
1776
|
+
{
|
|
1777
|
+
case: {
|
|
1778
|
+
$and: [
|
|
1779
|
+
{ $in: [lMedium, PAID_MEDIUMS] },
|
|
1780
|
+
{ $or: [
|
|
1781
|
+
{ $in: [lSource, ["google", "goog", "bing", "yahoo", "duckduckgo", "ecosia", "baidu", "yandex"]] },
|
|
1782
|
+
{ $regexMatch: { input: refStr, regex: SEARCH_ENGINES } }
|
|
1783
|
+
] }
|
|
1784
|
+
]
|
|
1785
|
+
},
|
|
1786
|
+
then: "Paid Search"
|
|
1787
|
+
},
|
|
1788
|
+
// Paid Social
|
|
1789
|
+
{
|
|
1790
|
+
case: {
|
|
1791
|
+
$and: [
|
|
1792
|
+
{ $in: [lMedium, PAID_MEDIUMS] },
|
|
1793
|
+
{ $or: [
|
|
1794
|
+
{ $in: [lSource, SOCIAL_SOURCES] },
|
|
1795
|
+
{ $regexMatch: { input: refStr, regex: SOCIAL_NETWORKS } }
|
|
1796
|
+
] }
|
|
1797
|
+
]
|
|
1798
|
+
},
|
|
1799
|
+
then: "Paid Social"
|
|
1800
|
+
},
|
|
1801
|
+
// Email
|
|
1802
|
+
{ case: { $in: [lMedium, ["email", "e-mail", "e_mail"]] }, then: "Email" },
|
|
1803
|
+
// Display
|
|
1804
|
+
{ case: { $in: [lMedium, ["display", "banner", "cpm"]] }, then: "Display" },
|
|
1805
|
+
// Affiliate
|
|
1806
|
+
{ case: { $in: [lMedium, ["affiliate"]] }, then: "Affiliate" },
|
|
1807
|
+
// Organic Search
|
|
1808
|
+
{
|
|
1809
|
+
case: {
|
|
1810
|
+
$and: [
|
|
1811
|
+
{ $regexMatch: { input: refStr, regex: SEARCH_ENGINES } },
|
|
1812
|
+
{ $not: [{ $in: [lMedium, PAID_MEDIUMS] }] }
|
|
1813
|
+
]
|
|
1814
|
+
},
|
|
1815
|
+
then: "Organic Search"
|
|
1816
|
+
},
|
|
1817
|
+
// Organic Social
|
|
1818
|
+
{
|
|
1819
|
+
case: {
|
|
1820
|
+
$and: [
|
|
1821
|
+
{ $or: [
|
|
1822
|
+
{ $regexMatch: { input: refStr, regex: SOCIAL_NETWORKS } },
|
|
1823
|
+
{ $in: [lSource, SOCIAL_SOURCES] }
|
|
1824
|
+
] },
|
|
1825
|
+
{ $not: [{ $in: [lMedium, PAID_MEDIUMS] }] }
|
|
1826
|
+
]
|
|
1827
|
+
},
|
|
1828
|
+
then: "Organic Social"
|
|
1829
|
+
},
|
|
1830
|
+
// Referral
|
|
1831
|
+
{
|
|
1832
|
+
case: { $and: [{ $ne: [refStr, ""] }, { $gt: [{ $strLenCP: refStr }, 0] }] },
|
|
1833
|
+
then: "Referral"
|
|
1834
|
+
},
|
|
1835
|
+
// Other (has UTM but no referrer)
|
|
1836
|
+
{
|
|
1837
|
+
case: {
|
|
1838
|
+
$or: [
|
|
1839
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_source", ""] }, ""] }] },
|
|
1840
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_medium", ""] }, ""] }] },
|
|
1841
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_campaign", ""] }, ""] }] }
|
|
1842
|
+
]
|
|
1843
|
+
},
|
|
1844
|
+
then: "Other"
|
|
1845
|
+
}
|
|
1846
|
+
],
|
|
1847
|
+
default: "Direct"
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1279
1851
|
function buildFilterMatch(filters) {
|
|
1280
1852
|
if (!filters) return {};
|
|
1281
1853
|
const map = {
|
|
@@ -1286,6 +1858,10 @@ function buildFilterMatch(filters) {
|
|
|
1286
1858
|
"device.type": "device_type",
|
|
1287
1859
|
"device.browser": "browser",
|
|
1288
1860
|
"device.os": "os",
|
|
1861
|
+
"device.osVersion": "os_version",
|
|
1862
|
+
"device.deviceModel": "device_model",
|
|
1863
|
+
"device.deviceBrand": "device_brand",
|
|
1864
|
+
"device.appVersion": "app_version",
|
|
1289
1865
|
"utm.source": "utm_source",
|
|
1290
1866
|
"utm.medium": "utm_medium",
|
|
1291
1867
|
"utm.campaign": "utm_campaign",
|
|
@@ -1301,7 +1877,9 @@ function buildFilterMatch(filters) {
|
|
|
1301
1877
|
};
|
|
1302
1878
|
const match = {};
|
|
1303
1879
|
for (const [key, value] of Object.entries(filters)) {
|
|
1304
|
-
if (!value
|
|
1880
|
+
if (!value) continue;
|
|
1881
|
+
if (key === "channel") continue;
|
|
1882
|
+
if (!map[key]) continue;
|
|
1305
1883
|
match[map[key]] = value;
|
|
1306
1884
|
}
|
|
1307
1885
|
return match;
|
|
@@ -1311,6 +1889,7 @@ var MongoDBAdapter = class {
|
|
|
1311
1889
|
db;
|
|
1312
1890
|
collection;
|
|
1313
1891
|
sites;
|
|
1892
|
+
identityMap;
|
|
1314
1893
|
constructor(url) {
|
|
1315
1894
|
this.client = new import_mongodb.MongoClient(url);
|
|
1316
1895
|
}
|
|
@@ -1319,13 +1898,16 @@ var MongoDBAdapter = class {
|
|
|
1319
1898
|
this.db = this.client.db();
|
|
1320
1899
|
this.collection = this.db.collection(EVENTS_COLLECTION);
|
|
1321
1900
|
this.sites = this.db.collection(SITES_COLLECTION);
|
|
1901
|
+
this.identityMap = this.db.collection(IDENTITY_MAP_COLLECTION);
|
|
1322
1902
|
await Promise.all([
|
|
1323
1903
|
this.collection.createIndex({ site_id: 1, timestamp: -1 }),
|
|
1324
1904
|
this.collection.createIndex({ site_id: 1, type: 1 }),
|
|
1325
1905
|
this.collection.createIndex({ site_id: 1, visitor_id: 1 }),
|
|
1326
1906
|
this.collection.createIndex({ site_id: 1, session_id: 1 }),
|
|
1327
1907
|
this.sites.createIndex({ site_id: 1 }, { unique: true }),
|
|
1328
|
-
this.sites.createIndex({ secret_key: 1 })
|
|
1908
|
+
this.sites.createIndex({ secret_key: 1 }),
|
|
1909
|
+
this.identityMap.createIndex({ site_id: 1, visitor_id: 1 }, { unique: true }),
|
|
1910
|
+
this.identityMap.createIndex({ site_id: 1, user_id: 1 })
|
|
1329
1911
|
]);
|
|
1330
1912
|
}
|
|
1331
1913
|
async insertEvents(events) {
|
|
@@ -1366,6 +1948,13 @@ var MongoDBAdapter = class {
|
|
|
1366
1948
|
utm_term: e.utm?.term ?? null,
|
|
1367
1949
|
utm_content: e.utm?.content ?? null,
|
|
1368
1950
|
ip: e.ip ?? null,
|
|
1951
|
+
os_version: e.device?.osVersion ?? null,
|
|
1952
|
+
device_model: e.device?.deviceModel ?? null,
|
|
1953
|
+
device_brand: e.device?.deviceBrand ?? null,
|
|
1954
|
+
app_version: e.device?.appVersion ?? null,
|
|
1955
|
+
app_build: e.device?.appBuild ?? null,
|
|
1956
|
+
sdk_name: e.device?.sdkName ?? null,
|
|
1957
|
+
sdk_version: e.device?.sdkVersion ?? null,
|
|
1369
1958
|
created_at: /* @__PURE__ */ new Date()
|
|
1370
1959
|
}));
|
|
1371
1960
|
await this.collection.insertMany(docs);
|
|
@@ -1379,12 +1968,22 @@ var MongoDBAdapter = class {
|
|
|
1379
1968
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1380
1969
|
};
|
|
1381
1970
|
const filterMatch = buildFilterMatch(q.filters);
|
|
1971
|
+
const matchStages = (extra) => {
|
|
1972
|
+
const stages = [
|
|
1973
|
+
{ $match: { ...baseMatch, ...filterMatch, ...extra } }
|
|
1974
|
+
];
|
|
1975
|
+
if (q.filters?.channel) {
|
|
1976
|
+
stages.push({ $addFields: { _channel: channelClassificationSwitch() } });
|
|
1977
|
+
stages.push({ $match: { _channel: q.filters.channel } });
|
|
1978
|
+
}
|
|
1979
|
+
return stages;
|
|
1980
|
+
};
|
|
1382
1981
|
let data = [];
|
|
1383
1982
|
let total = 0;
|
|
1384
1983
|
switch (q.metric) {
|
|
1385
1984
|
case "pageviews": {
|
|
1386
1985
|
const [result2] = await this.collection.aggregate([
|
|
1387
|
-
{
|
|
1986
|
+
...matchStages({ type: "pageview" }),
|
|
1388
1987
|
{ $count: "count" }
|
|
1389
1988
|
]).toArray();
|
|
1390
1989
|
total = result2?.count ?? 0;
|
|
@@ -1393,7 +1992,7 @@ var MongoDBAdapter = class {
|
|
|
1393
1992
|
}
|
|
1394
1993
|
case "visitors": {
|
|
1395
1994
|
const [result2] = await this.collection.aggregate([
|
|
1396
|
-
|
|
1995
|
+
...matchStages(),
|
|
1397
1996
|
{ $group: { _id: "$visitor_id" } },
|
|
1398
1997
|
{ $count: "count" }
|
|
1399
1998
|
]).toArray();
|
|
@@ -1403,7 +2002,7 @@ var MongoDBAdapter = class {
|
|
|
1403
2002
|
}
|
|
1404
2003
|
case "sessions": {
|
|
1405
2004
|
const [result2] = await this.collection.aggregate([
|
|
1406
|
-
|
|
2005
|
+
...matchStages(),
|
|
1407
2006
|
{ $group: { _id: "$session_id" } },
|
|
1408
2007
|
{ $count: "count" }
|
|
1409
2008
|
]).toArray();
|
|
@@ -1413,7 +2012,7 @@ var MongoDBAdapter = class {
|
|
|
1413
2012
|
}
|
|
1414
2013
|
case "events": {
|
|
1415
2014
|
const [result2] = await this.collection.aggregate([
|
|
1416
|
-
{
|
|
2015
|
+
...matchStages({ type: "event" }),
|
|
1417
2016
|
{ $count: "count" }
|
|
1418
2017
|
]).toArray();
|
|
1419
2018
|
total = result2?.count ?? 0;
|
|
@@ -1428,7 +2027,7 @@ var MongoDBAdapter = class {
|
|
|
1428
2027
|
break;
|
|
1429
2028
|
}
|
|
1430
2029
|
const [result2] = await this.collection.aggregate([
|
|
1431
|
-
{
|
|
2030
|
+
...matchStages({ type: "event", event_name: { $in: conversionEvents } }),
|
|
1432
2031
|
{ $count: "count" }
|
|
1433
2032
|
]).toArray();
|
|
1434
2033
|
total = result2?.count ?? 0;
|
|
@@ -1437,7 +2036,7 @@ var MongoDBAdapter = class {
|
|
|
1437
2036
|
}
|
|
1438
2037
|
case "top_pages": {
|
|
1439
2038
|
const rows = await this.collection.aggregate([
|
|
1440
|
-
{
|
|
2039
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1441
2040
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1442
2041
|
{ $sort: { value: -1 } },
|
|
1443
2042
|
{ $limit: limit }
|
|
@@ -1448,7 +2047,7 @@ var MongoDBAdapter = class {
|
|
|
1448
2047
|
}
|
|
1449
2048
|
case "top_referrers": {
|
|
1450
2049
|
const rows = await this.collection.aggregate([
|
|
1451
|
-
{
|
|
2050
|
+
...matchStages({ type: "pageview", referrer: { $nin: [null, ""] } }),
|
|
1452
2051
|
{ $group: { _id: "$referrer", value: { $sum: 1 } } },
|
|
1453
2052
|
{ $sort: { value: -1 } },
|
|
1454
2053
|
{ $limit: limit }
|
|
@@ -1459,7 +2058,7 @@ var MongoDBAdapter = class {
|
|
|
1459
2058
|
}
|
|
1460
2059
|
case "top_countries": {
|
|
1461
2060
|
const rows = await this.collection.aggregate([
|
|
1462
|
-
{
|
|
2061
|
+
...matchStages({ country: { $ne: null } }),
|
|
1463
2062
|
{ $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
|
|
1464
2063
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1465
2064
|
{ $sort: { value: -1 } },
|
|
@@ -1471,7 +2070,7 @@ var MongoDBAdapter = class {
|
|
|
1471
2070
|
}
|
|
1472
2071
|
case "top_cities": {
|
|
1473
2072
|
const rows = await this.collection.aggregate([
|
|
1474
|
-
{
|
|
2073
|
+
...matchStages({ city: { $ne: null } }),
|
|
1475
2074
|
{ $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
|
|
1476
2075
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1477
2076
|
{ $sort: { value: -1 } },
|
|
@@ -1483,7 +2082,7 @@ var MongoDBAdapter = class {
|
|
|
1483
2082
|
}
|
|
1484
2083
|
case "top_events": {
|
|
1485
2084
|
const rows = await this.collection.aggregate([
|
|
1486
|
-
{
|
|
2085
|
+
...matchStages({ type: "event", event_name: { $ne: null } }),
|
|
1487
2086
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1488
2087
|
{ $sort: { value: -1 } },
|
|
1489
2088
|
{ $limit: limit }
|
|
@@ -1500,7 +2099,7 @@ var MongoDBAdapter = class {
|
|
|
1500
2099
|
break;
|
|
1501
2100
|
}
|
|
1502
2101
|
const rows = await this.collection.aggregate([
|
|
1503
|
-
{
|
|
2102
|
+
...matchStages({ type: "event", event_name: { $in: conversionEvents } }),
|
|
1504
2103
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1505
2104
|
{ $sort: { value: -1 } },
|
|
1506
2105
|
{ $limit: limit }
|
|
@@ -1511,7 +2110,7 @@ var MongoDBAdapter = class {
|
|
|
1511
2110
|
}
|
|
1512
2111
|
case "top_exit_pages": {
|
|
1513
2112
|
const rows = await this.collection.aggregate([
|
|
1514
|
-
{
|
|
2113
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1515
2114
|
{ $sort: { timestamp: 1 } },
|
|
1516
2115
|
{ $group: { _id: "$session_id", url: { $last: "$url" } } },
|
|
1517
2116
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
@@ -1524,7 +2123,7 @@ var MongoDBAdapter = class {
|
|
|
1524
2123
|
}
|
|
1525
2124
|
case "top_transitions": {
|
|
1526
2125
|
const rows = await this.collection.aggregate([
|
|
1527
|
-
{
|
|
2126
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1528
2127
|
{
|
|
1529
2128
|
$setWindowFields: {
|
|
1530
2129
|
partitionBy: "$session_id",
|
|
@@ -1545,7 +2144,7 @@ var MongoDBAdapter = class {
|
|
|
1545
2144
|
}
|
|
1546
2145
|
case "top_scroll_pages": {
|
|
1547
2146
|
const rows = await this.collection.aggregate([
|
|
1548
|
-
{
|
|
2147
|
+
...matchStages({ type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } }),
|
|
1549
2148
|
{ $group: { _id: "$page_path", value: { $sum: 1 } } },
|
|
1550
2149
|
{ $sort: { value: -1 } },
|
|
1551
2150
|
{ $limit: limit }
|
|
@@ -1556,15 +2155,11 @@ var MongoDBAdapter = class {
|
|
|
1556
2155
|
}
|
|
1557
2156
|
case "top_button_clicks": {
|
|
1558
2157
|
const rows = await this.collection.aggregate([
|
|
1559
|
-
{
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
event_subtype: "button_click",
|
|
1565
|
-
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
1566
|
-
}
|
|
1567
|
-
},
|
|
2158
|
+
...matchStages({
|
|
2159
|
+
type: "event",
|
|
2160
|
+
event_subtype: "button_click",
|
|
2161
|
+
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
2162
|
+
}),
|
|
1568
2163
|
{ $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
|
|
1569
2164
|
{ $sort: { value: -1 } },
|
|
1570
2165
|
{ $limit: limit }
|
|
@@ -1575,15 +2170,11 @@ var MongoDBAdapter = class {
|
|
|
1575
2170
|
}
|
|
1576
2171
|
case "top_link_targets": {
|
|
1577
2172
|
const rows = await this.collection.aggregate([
|
|
1578
|
-
{
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
1584
|
-
target_url_path: { $ne: null }
|
|
1585
|
-
}
|
|
1586
|
-
},
|
|
2173
|
+
...matchStages({
|
|
2174
|
+
type: "event",
|
|
2175
|
+
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
2176
|
+
target_url_path: { $ne: null }
|
|
2177
|
+
}),
|
|
1587
2178
|
{ $group: { _id: "$target_url_path", value: { $sum: 1 } } },
|
|
1588
2179
|
{ $sort: { value: -1 } },
|
|
1589
2180
|
{ $limit: limit }
|
|
@@ -1594,7 +2185,7 @@ var MongoDBAdapter = class {
|
|
|
1594
2185
|
}
|
|
1595
2186
|
case "top_devices": {
|
|
1596
2187
|
const rows = await this.collection.aggregate([
|
|
1597
|
-
{
|
|
2188
|
+
...matchStages({ device_type: { $ne: null } }),
|
|
1598
2189
|
{ $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
|
|
1599
2190
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1600
2191
|
{ $sort: { value: -1 } },
|
|
@@ -1606,7 +2197,7 @@ var MongoDBAdapter = class {
|
|
|
1606
2197
|
}
|
|
1607
2198
|
case "top_browsers": {
|
|
1608
2199
|
const rows = await this.collection.aggregate([
|
|
1609
|
-
{
|
|
2200
|
+
...matchStages({ browser: { $ne: null } }),
|
|
1610
2201
|
{ $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
|
|
1611
2202
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1612
2203
|
{ $sort: { value: -1 } },
|
|
@@ -1618,7 +2209,7 @@ var MongoDBAdapter = class {
|
|
|
1618
2209
|
}
|
|
1619
2210
|
case "top_os": {
|
|
1620
2211
|
const rows = await this.collection.aggregate([
|
|
1621
|
-
{
|
|
2212
|
+
...matchStages({ os: { $ne: null } }),
|
|
1622
2213
|
{ $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
|
|
1623
2214
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1624
2215
|
{ $sort: { value: -1 } },
|
|
@@ -1628,6 +2219,117 @@ var MongoDBAdapter = class {
|
|
|
1628
2219
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1629
2220
|
break;
|
|
1630
2221
|
}
|
|
2222
|
+
case "top_os_versions": {
|
|
2223
|
+
const rows = await this.collection.aggregate([
|
|
2224
|
+
...matchStages({ os: { $ne: null }, os_version: { $ne: null } }),
|
|
2225
|
+
{ $group: { _id: { $concat: ["$os", " ", "$os_version"] }, value: { $addToSet: "$visitor_id" } } },
|
|
2226
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2227
|
+
{ $sort: { value: -1 } },
|
|
2228
|
+
{ $limit: limit }
|
|
2229
|
+
]).toArray();
|
|
2230
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2231
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2232
|
+
break;
|
|
2233
|
+
}
|
|
2234
|
+
case "top_device_models": {
|
|
2235
|
+
const rows = await this.collection.aggregate([
|
|
2236
|
+
...matchStages({ device_model: { $ne: null } }),
|
|
2237
|
+
{ $group: { _id: { $trim: { input: { $concat: [{ $ifNull: ["$device_brand", ""] }, " ", "$device_model"] } } }, value: { $addToSet: "$visitor_id" } } },
|
|
2238
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2239
|
+
{ $sort: { value: -1 } },
|
|
2240
|
+
{ $limit: limit }
|
|
2241
|
+
]).toArray();
|
|
2242
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2243
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2244
|
+
break;
|
|
2245
|
+
}
|
|
2246
|
+
case "top_app_versions": {
|
|
2247
|
+
const rows = await this.collection.aggregate([
|
|
2248
|
+
...matchStages({ app_version: { $ne: null } }),
|
|
2249
|
+
{ $group: { _id: "$app_version", value: { $addToSet: "$visitor_id" } } },
|
|
2250
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2251
|
+
{ $sort: { value: -1 } },
|
|
2252
|
+
{ $limit: limit }
|
|
2253
|
+
]).toArray();
|
|
2254
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2255
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2256
|
+
break;
|
|
2257
|
+
}
|
|
2258
|
+
case "top_utm_sources": {
|
|
2259
|
+
const rows = await this.collection.aggregate([
|
|
2260
|
+
...matchStages({ utm_source: { $nin: [null, ""] } }),
|
|
2261
|
+
{ $addFields: { _normalized_source: normalizedUtmSourceSwitch() } },
|
|
2262
|
+
{ $group: { _id: "$_normalized_source", value: { $addToSet: "$visitor_id" } } },
|
|
2263
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2264
|
+
{ $sort: { value: -1 } },
|
|
2265
|
+
{ $limit: limit }
|
|
2266
|
+
]).toArray();
|
|
2267
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2268
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2269
|
+
break;
|
|
2270
|
+
}
|
|
2271
|
+
case "top_utm_mediums": {
|
|
2272
|
+
const rows = await this.collection.aggregate([
|
|
2273
|
+
...matchStages({ utm_medium: { $nin: [null, ""] } }),
|
|
2274
|
+
{ $addFields: { _normalized_medium: normalizedUtmMediumSwitch() } },
|
|
2275
|
+
{ $group: { _id: "$_normalized_medium", value: { $addToSet: "$visitor_id" } } },
|
|
2276
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2277
|
+
{ $sort: { value: -1 } },
|
|
2278
|
+
{ $limit: limit }
|
|
2279
|
+
]).toArray();
|
|
2280
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2281
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2284
|
+
case "top_utm_campaigns": {
|
|
2285
|
+
const rows = await this.collection.aggregate([
|
|
2286
|
+
...matchStages({ utm_campaign: { $nin: [null, ""] } }),
|
|
2287
|
+
{ $group: { _id: "$utm_campaign", value: { $addToSet: "$visitor_id" } } },
|
|
2288
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2289
|
+
{ $sort: { value: -1 } },
|
|
2290
|
+
{ $limit: limit }
|
|
2291
|
+
]).toArray();
|
|
2292
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2293
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2296
|
+
case "top_utm_terms": {
|
|
2297
|
+
const rows = await this.collection.aggregate([
|
|
2298
|
+
...matchStages({ utm_term: { $nin: [null, ""] } }),
|
|
2299
|
+
{ $group: { _id: "$utm_term", value: { $addToSet: "$visitor_id" } } },
|
|
2300
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2301
|
+
{ $sort: { value: -1 } },
|
|
2302
|
+
{ $limit: limit }
|
|
2303
|
+
]).toArray();
|
|
2304
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2305
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2306
|
+
break;
|
|
2307
|
+
}
|
|
2308
|
+
case "top_utm_contents": {
|
|
2309
|
+
const rows = await this.collection.aggregate([
|
|
2310
|
+
...matchStages({ utm_content: { $nin: [null, ""] } }),
|
|
2311
|
+
{ $group: { _id: "$utm_content", value: { $addToSet: "$visitor_id" } } },
|
|
2312
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2313
|
+
{ $sort: { value: -1 } },
|
|
2314
|
+
{ $limit: limit }
|
|
2315
|
+
]).toArray();
|
|
2316
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2317
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2318
|
+
break;
|
|
2319
|
+
}
|
|
2320
|
+
case "top_channels": {
|
|
2321
|
+
const rows = await this.collection.aggregate([
|
|
2322
|
+
...matchStages(),
|
|
2323
|
+
{ $addFields: { _channel: channelClassificationSwitch() } },
|
|
2324
|
+
{ $group: { _id: "$_channel", value: { $addToSet: "$visitor_id" } } },
|
|
2325
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2326
|
+
{ $sort: { value: -1 } },
|
|
2327
|
+
{ $limit: limit }
|
|
2328
|
+
]).toArray();
|
|
2329
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2330
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2331
|
+
break;
|
|
2332
|
+
}
|
|
1631
2333
|
}
|
|
1632
2334
|
const result = { metric: q.metric, period, data, total };
|
|
1633
2335
|
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
@@ -1816,24 +2518,72 @@ var MongoDBAdapter = class {
|
|
|
1816
2518
|
offset
|
|
1817
2519
|
};
|
|
1818
2520
|
}
|
|
2521
|
+
// ─── Identity Mapping ──────────────────────────────────────
|
|
2522
|
+
async upsertIdentity(siteId, visitorId, userId) {
|
|
2523
|
+
await this.identityMap.updateOne(
|
|
2524
|
+
{ site_id: siteId, visitor_id: visitorId },
|
|
2525
|
+
{
|
|
2526
|
+
$set: { user_id: userId, identified_at: /* @__PURE__ */ new Date() },
|
|
2527
|
+
$setOnInsert: { site_id: siteId, visitor_id: visitorId, created_at: /* @__PURE__ */ new Date() }
|
|
2528
|
+
},
|
|
2529
|
+
{ upsert: true }
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
async getVisitorIdsForUser(siteId, userId) {
|
|
2533
|
+
const docs = await this.identityMap.find({ site_id: siteId, user_id: userId }).toArray();
|
|
2534
|
+
return docs.map((d) => d.visitor_id);
|
|
2535
|
+
}
|
|
2536
|
+
async getUserIdForVisitor(siteId, visitorId) {
|
|
2537
|
+
const doc = await this.identityMap.findOne({ site_id: siteId, visitor_id: visitorId });
|
|
2538
|
+
return doc?.user_id ?? null;
|
|
2539
|
+
}
|
|
1819
2540
|
// ─── User Listing ──────────────────────────────────────
|
|
1820
2541
|
async listUsers(params) {
|
|
1821
2542
|
const limit = Math.min(params.limit ?? 50, 200);
|
|
1822
2543
|
const offset = params.offset ?? 0;
|
|
1823
2544
|
const match = { site_id: params.siteId };
|
|
1824
|
-
if (params.search) {
|
|
1825
|
-
match.$or = [
|
|
1826
|
-
{ visitor_id: { $regex: params.search, $options: "i" } },
|
|
1827
|
-
{ user_id: { $regex: params.search, $options: "i" } }
|
|
1828
|
-
];
|
|
1829
|
-
}
|
|
1830
2545
|
const pipeline = [
|
|
1831
2546
|
{ $match: match },
|
|
2547
|
+
// Join with identity map to resolve visitor → user
|
|
2548
|
+
{
|
|
2549
|
+
$lookup: {
|
|
2550
|
+
from: IDENTITY_MAP_COLLECTION,
|
|
2551
|
+
let: { vid: "$visitor_id", sid: "$site_id" },
|
|
2552
|
+
pipeline: [
|
|
2553
|
+
{ $match: { $expr: { $and: [{ $eq: ["$visitor_id", "$$vid"] }, { $eq: ["$site_id", "$$sid"] }] } } }
|
|
2554
|
+
],
|
|
2555
|
+
as: "_identity"
|
|
2556
|
+
}
|
|
2557
|
+
},
|
|
2558
|
+
{
|
|
2559
|
+
$addFields: {
|
|
2560
|
+
_resolved_id: {
|
|
2561
|
+
$ifNull: [{ $arrayElemAt: ["$_identity.user_id", 0] }, "$visitor_id"]
|
|
2562
|
+
},
|
|
2563
|
+
_resolved_user_id: {
|
|
2564
|
+
$arrayElemAt: ["$_identity.user_id", 0]
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
];
|
|
2569
|
+
if (params.search) {
|
|
2570
|
+
pipeline.push({
|
|
2571
|
+
$match: {
|
|
2572
|
+
$or: [
|
|
2573
|
+
{ visitor_id: { $regex: params.search, $options: "i" } },
|
|
2574
|
+
{ user_id: { $regex: params.search, $options: "i" } },
|
|
2575
|
+
{ _resolved_user_id: { $regex: params.search, $options: "i" } }
|
|
2576
|
+
]
|
|
2577
|
+
}
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
pipeline.push(
|
|
1832
2581
|
{ $sort: { timestamp: 1 } },
|
|
1833
2582
|
{
|
|
1834
2583
|
$group: {
|
|
1835
|
-
_id: "$
|
|
1836
|
-
|
|
2584
|
+
_id: "$_resolved_id",
|
|
2585
|
+
visitorIds: { $addToSet: "$visitor_id" },
|
|
2586
|
+
userId: { $last: { $ifNull: ["$_resolved_user_id", "$user_id"] } },
|
|
1837
2587
|
traits: { $last: "$traits" },
|
|
1838
2588
|
firstSeen: { $min: "$timestamp" },
|
|
1839
2589
|
lastSeen: { $max: "$timestamp" },
|
|
@@ -1866,10 +2616,11 @@ var MongoDBAdapter = class {
|
|
|
1866
2616
|
count: [{ $count: "total" }]
|
|
1867
2617
|
}
|
|
1868
2618
|
}
|
|
1869
|
-
|
|
2619
|
+
);
|
|
1870
2620
|
const [result] = await this.collection.aggregate(pipeline).toArray();
|
|
1871
2621
|
const users = (result?.data ?? []).map((u) => ({
|
|
1872
|
-
visitorId: u._id,
|
|
2622
|
+
visitorId: u.visitorIds[0] ?? u._id,
|
|
2623
|
+
visitorIds: u.visitorIds.length > 1 ? u.visitorIds : void 0,
|
|
1873
2624
|
userId: u.userId ?? void 0,
|
|
1874
2625
|
traits: u.traits ?? void 0,
|
|
1875
2626
|
firstSeen: u.firstSeen.toISOString(),
|
|
@@ -1899,13 +2650,125 @@ var MongoDBAdapter = class {
|
|
|
1899
2650
|
offset
|
|
1900
2651
|
};
|
|
1901
2652
|
}
|
|
1902
|
-
async getUserDetail(siteId,
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
|
|
2653
|
+
async getUserDetail(siteId, identifier) {
|
|
2654
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
2655
|
+
if (visitorIds.length > 0) {
|
|
2656
|
+
return this.getMergedUserDetail(siteId, identifier, visitorIds);
|
|
2657
|
+
}
|
|
2658
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
2659
|
+
if (userId) {
|
|
2660
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
2661
|
+
return this.getMergedUserDetail(siteId, userId, allVisitorIds);
|
|
2662
|
+
}
|
|
2663
|
+
return this.getMergedUserDetail(siteId, void 0, [identifier]);
|
|
2664
|
+
}
|
|
2665
|
+
async getUserEvents(siteId, identifier, params) {
|
|
2666
|
+
let visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
2667
|
+
if (visitorIds.length === 0) {
|
|
2668
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
2669
|
+
if (userId) {
|
|
2670
|
+
visitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
if (visitorIds.length === 0) {
|
|
2674
|
+
visitorIds = [identifier];
|
|
2675
|
+
}
|
|
2676
|
+
return this.listEventsForVisitorIds(siteId, visitorIds, params);
|
|
2677
|
+
}
|
|
2678
|
+
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
2679
|
+
const pipeline = [
|
|
2680
|
+
{ $match: { site_id: siteId, visitor_id: { $in: visitorIds } } },
|
|
2681
|
+
{ $sort: { timestamp: 1 } },
|
|
2682
|
+
{
|
|
2683
|
+
$group: {
|
|
2684
|
+
_id: null,
|
|
2685
|
+
visitorIds: { $addToSet: "$visitor_id" },
|
|
2686
|
+
traits: { $last: "$traits" },
|
|
2687
|
+
firstSeen: { $min: "$timestamp" },
|
|
2688
|
+
lastSeen: { $max: "$timestamp" },
|
|
2689
|
+
totalEvents: { $sum: 1 },
|
|
2690
|
+
totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
|
|
2691
|
+
sessions: { $addToSet: "$session_id" },
|
|
2692
|
+
lastUrl: { $last: "$url" },
|
|
2693
|
+
referrer: { $last: "$referrer" },
|
|
2694
|
+
device_type: { $last: "$device_type" },
|
|
2695
|
+
browser: { $last: "$browser" },
|
|
2696
|
+
os: { $last: "$os" },
|
|
2697
|
+
country: { $last: "$country" },
|
|
2698
|
+
city: { $last: "$city" },
|
|
2699
|
+
region: { $last: "$region" },
|
|
2700
|
+
language: { $last: "$language" },
|
|
2701
|
+
timezone: { $last: "$timezone" },
|
|
2702
|
+
screen_width: { $last: "$screen_width" },
|
|
2703
|
+
screen_height: { $last: "$screen_height" },
|
|
2704
|
+
utm_source: { $last: "$utm_source" },
|
|
2705
|
+
utm_medium: { $last: "$utm_medium" },
|
|
2706
|
+
utm_campaign: { $last: "$utm_campaign" },
|
|
2707
|
+
utm_term: { $last: "$utm_term" },
|
|
2708
|
+
utm_content: { $last: "$utm_content" }
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
];
|
|
2712
|
+
const [row] = await this.collection.aggregate(pipeline).toArray();
|
|
2713
|
+
if (!row) return null;
|
|
2714
|
+
return {
|
|
2715
|
+
visitorId: visitorIds[0],
|
|
2716
|
+
visitorIds: row.visitorIds.length > 1 ? row.visitorIds : void 0,
|
|
2717
|
+
userId: userId ?? void 0,
|
|
2718
|
+
traits: row.traits ?? void 0,
|
|
2719
|
+
firstSeen: row.firstSeen.toISOString(),
|
|
2720
|
+
lastSeen: row.lastSeen.toISOString(),
|
|
2721
|
+
totalEvents: row.totalEvents,
|
|
2722
|
+
totalPageviews: row.totalPageviews,
|
|
2723
|
+
totalSessions: row.sessions.length,
|
|
2724
|
+
lastUrl: row.lastUrl ?? void 0,
|
|
2725
|
+
referrer: row.referrer ?? void 0,
|
|
2726
|
+
device: row.device_type ? { type: row.device_type, browser: row.browser ?? "", os: row.os ?? "" } : void 0,
|
|
2727
|
+
geo: row.country ? { country: row.country, city: row.city ?? void 0, region: row.region ?? void 0 } : void 0,
|
|
2728
|
+
language: row.language ?? void 0,
|
|
2729
|
+
timezone: row.timezone ?? void 0,
|
|
2730
|
+
screen: row.screen_width || row.screen_height ? { width: row.screen_width ?? 0, height: row.screen_height ?? 0 } : void 0,
|
|
2731
|
+
utm: row.utm_source ? {
|
|
2732
|
+
source: row.utm_source ?? void 0,
|
|
2733
|
+
medium: row.utm_medium ?? void 0,
|
|
2734
|
+
campaign: row.utm_campaign ?? void 0,
|
|
2735
|
+
term: row.utm_term ?? void 0,
|
|
2736
|
+
content: row.utm_content ?? void 0
|
|
2737
|
+
} : void 0
|
|
2738
|
+
};
|
|
1906
2739
|
}
|
|
1907
|
-
async
|
|
1908
|
-
|
|
2740
|
+
async listEventsForVisitorIds(siteId, visitorIds, params) {
|
|
2741
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
2742
|
+
const offset = params.offset ?? 0;
|
|
2743
|
+
const match = {
|
|
2744
|
+
site_id: siteId,
|
|
2745
|
+
visitor_id: { $in: visitorIds }
|
|
2746
|
+
};
|
|
2747
|
+
if (params.type) match.type = params.type;
|
|
2748
|
+
if (params.eventName) {
|
|
2749
|
+
match.event_name = params.eventName;
|
|
2750
|
+
} else if (params.eventNames && params.eventNames.length > 0) {
|
|
2751
|
+
match.event_name = { $in: params.eventNames };
|
|
2752
|
+
}
|
|
2753
|
+
if (params.eventSource) match.event_source = params.eventSource;
|
|
2754
|
+
if (params.period || params.dateFrom) {
|
|
2755
|
+
const { dateRange } = resolvePeriod({
|
|
2756
|
+
period: params.period,
|
|
2757
|
+
dateFrom: params.dateFrom,
|
|
2758
|
+
dateTo: params.dateTo
|
|
2759
|
+
});
|
|
2760
|
+
match.timestamp = { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) };
|
|
2761
|
+
}
|
|
2762
|
+
const [events, countResult] = await Promise.all([
|
|
2763
|
+
this.collection.find(match).sort({ timestamp: -1 }).skip(offset).limit(limit).toArray(),
|
|
2764
|
+
this.collection.countDocuments(match)
|
|
2765
|
+
]);
|
|
2766
|
+
return {
|
|
2767
|
+
events: events.map((e) => this.toEventListItem(e)),
|
|
2768
|
+
total: countResult,
|
|
2769
|
+
limit,
|
|
2770
|
+
offset
|
|
2771
|
+
};
|
|
1909
2772
|
}
|
|
1910
2773
|
toEventListItem(doc) {
|
|
1911
2774
|
return {
|
|
@@ -1929,7 +2792,18 @@ var MongoDBAdapter = class {
|
|
|
1929
2792
|
userId: doc.user_id ?? void 0,
|
|
1930
2793
|
traits: doc.traits ?? void 0,
|
|
1931
2794
|
geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
|
|
1932
|
-
device: doc.device_type ? {
|
|
2795
|
+
device: doc.device_type ? {
|
|
2796
|
+
type: doc.device_type,
|
|
2797
|
+
browser: doc.browser ?? "",
|
|
2798
|
+
os: doc.os ?? "",
|
|
2799
|
+
osVersion: doc.os_version ?? void 0,
|
|
2800
|
+
deviceModel: doc.device_model ?? void 0,
|
|
2801
|
+
deviceBrand: doc.device_brand ?? void 0,
|
|
2802
|
+
appVersion: doc.app_version ?? void 0,
|
|
2803
|
+
appBuild: doc.app_build ?? void 0,
|
|
2804
|
+
sdkName: doc.sdk_name ?? void 0,
|
|
2805
|
+
sdkVersion: doc.sdk_version ?? void 0
|
|
2806
|
+
} : void 0,
|
|
1933
2807
|
language: doc.language ?? void 0,
|
|
1934
2808
|
utm: doc.utm_source ? {
|
|
1935
2809
|
source: doc.utm_source ?? void 0,
|
|
@@ -1947,6 +2821,7 @@ var MongoDBAdapter = class {
|
|
|
1947
2821
|
site_id: generateSiteId(),
|
|
1948
2822
|
secret_key: generateSecretKey(),
|
|
1949
2823
|
name: data.name,
|
|
2824
|
+
type: data.type ?? "web",
|
|
1950
2825
|
domain: data.domain ?? null,
|
|
1951
2826
|
allowed_origins: data.allowedOrigins ?? null,
|
|
1952
2827
|
conversion_events: data.conversionEvents ?? null,
|
|
@@ -1971,6 +2846,7 @@ var MongoDBAdapter = class {
|
|
|
1971
2846
|
async updateSite(siteId, data) {
|
|
1972
2847
|
const updates = { updated_at: /* @__PURE__ */ new Date() };
|
|
1973
2848
|
if (data.name !== void 0) updates.name = data.name;
|
|
2849
|
+
if (data.type !== void 0) updates.type = data.type;
|
|
1974
2850
|
if (data.domain !== void 0) updates.domain = data.domain || null;
|
|
1975
2851
|
if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
|
|
1976
2852
|
if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
|
|
@@ -2002,6 +2878,7 @@ var MongoDBAdapter = class {
|
|
|
2002
2878
|
siteId: doc.site_id,
|
|
2003
2879
|
secretKey: doc.secret_key,
|
|
2004
2880
|
name: doc.name,
|
|
2881
|
+
type: doc.type ?? "web",
|
|
2005
2882
|
domain: doc.domain ?? void 0,
|
|
2006
2883
|
allowedOrigins: doc.allowed_origins ?? void 0,
|
|
2007
2884
|
conversionEvents: doc.conversion_events ?? void 0,
|
|
@@ -2270,12 +3147,74 @@ async function createCollector(config) {
|
|
|
2270
3147
|
return false;
|
|
2271
3148
|
}
|
|
2272
3149
|
function enrichEvents(events, ip, userAgent) {
|
|
2273
|
-
const
|
|
3150
|
+
const uaDevice = parseUserAgent(userAgent);
|
|
2274
3151
|
return events.map((event) => {
|
|
2275
3152
|
const geo = resolveGeo(ip, event.timezone);
|
|
3153
|
+
let device;
|
|
3154
|
+
if (event.mobile?.platform) {
|
|
3155
|
+
device = {
|
|
3156
|
+
type: "mobile",
|
|
3157
|
+
browser: "App",
|
|
3158
|
+
os: event.mobile.platform === "ios" ? "iOS" : "Android",
|
|
3159
|
+
osVersion: event.mobile.osVersion,
|
|
3160
|
+
deviceModel: event.mobile.deviceModel,
|
|
3161
|
+
deviceBrand: event.mobile.deviceBrand,
|
|
3162
|
+
appVersion: event.mobile.appVersion,
|
|
3163
|
+
appBuild: event.mobile.appBuild,
|
|
3164
|
+
sdkName: event.mobile.sdkName,
|
|
3165
|
+
sdkVersion: event.mobile.sdkVersion
|
|
3166
|
+
};
|
|
3167
|
+
} else {
|
|
3168
|
+
device = uaDevice;
|
|
3169
|
+
}
|
|
2276
3170
|
return { ...event, ip, geo, device };
|
|
2277
3171
|
});
|
|
2278
3172
|
}
|
|
3173
|
+
const identityCache = /* @__PURE__ */ new Map();
|
|
3174
|
+
const IDENTITY_CACHE_TTL = 5 * 60 * 1e3;
|
|
3175
|
+
function getCachedUserId(siteId, visitorId) {
|
|
3176
|
+
const key = `${siteId}:${visitorId}`;
|
|
3177
|
+
const entry = identityCache.get(key);
|
|
3178
|
+
if (!entry) return void 0;
|
|
3179
|
+
if (Date.now() > entry.expires) {
|
|
3180
|
+
identityCache.delete(key);
|
|
3181
|
+
return void 0;
|
|
3182
|
+
}
|
|
3183
|
+
return entry.userId;
|
|
3184
|
+
}
|
|
3185
|
+
function setCachedUserId(siteId, visitorId, userId) {
|
|
3186
|
+
const key = `${siteId}:${visitorId}`;
|
|
3187
|
+
identityCache.set(key, { userId, expires: Date.now() + IDENTITY_CACHE_TTL });
|
|
3188
|
+
if (identityCache.size > 1e4) {
|
|
3189
|
+
const now = Date.now();
|
|
3190
|
+
for (const [k, v] of identityCache) {
|
|
3191
|
+
if (now > v.expires) identityCache.delete(k);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
async function processIdentity(events) {
|
|
3196
|
+
for (const event of events) {
|
|
3197
|
+
if (!event.visitorId || event.visitorId === "server") continue;
|
|
3198
|
+
if (event.type === "identify" && event.userId) {
|
|
3199
|
+
await db.upsertIdentity(event.siteId, event.visitorId, event.userId);
|
|
3200
|
+
setCachedUserId(event.siteId, event.visitorId, event.userId);
|
|
3201
|
+
} else if (!event.userId) {
|
|
3202
|
+
const cached = getCachedUserId(event.siteId, event.visitorId);
|
|
3203
|
+
if (cached) {
|
|
3204
|
+
event.userId = cached;
|
|
3205
|
+
} else {
|
|
3206
|
+
const resolved = await db.getUserIdForVisitor(event.siteId, event.visitorId);
|
|
3207
|
+
if (resolved) {
|
|
3208
|
+
event.userId = resolved;
|
|
3209
|
+
setCachedUserId(event.siteId, event.visitorId, resolved);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
} else if (event.userId) {
|
|
3213
|
+
setCachedUserId(event.siteId, event.visitorId, event.userId);
|
|
3214
|
+
await db.upsertIdentity(event.siteId, event.visitorId, event.userId);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
2279
3218
|
function extractIp(req) {
|
|
2280
3219
|
if (config.trustProxy ?? true) {
|
|
2281
3220
|
const forwarded = req.headers?.["x-forwarded-for"];
|
|
@@ -2287,6 +3226,22 @@ async function createCollector(config) {
|
|
|
2287
3226
|
}
|
|
2288
3227
|
return req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || "";
|
|
2289
3228
|
}
|
|
3229
|
+
function extractRequestHostname(req) {
|
|
3230
|
+
const headerValue = (value) => {
|
|
3231
|
+
if (Array.isArray(value)) return value[0];
|
|
3232
|
+
if (typeof value === "string") return value;
|
|
3233
|
+
return void 0;
|
|
3234
|
+
};
|
|
3235
|
+
const origin = headerValue(req.headers?.origin);
|
|
3236
|
+
const referer = headerValue(req.headers?.referer) || headerValue(req.headers?.referrer);
|
|
3237
|
+
const raw = origin ?? referer;
|
|
3238
|
+
if (!raw || raw === "null") return void 0;
|
|
3239
|
+
try {
|
|
3240
|
+
return new URL(raw).hostname.toLowerCase();
|
|
3241
|
+
} catch {
|
|
3242
|
+
return void 0;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
2290
3245
|
function handler() {
|
|
2291
3246
|
return async (req, res) => {
|
|
2292
3247
|
res.setHeader?.("Access-Control-Allow-Origin", "*");
|
|
@@ -2312,6 +3267,12 @@ async function createCollector(config) {
|
|
|
2312
3267
|
sendJson(res, 400, { ok: false, error: "Too many events (max 100)" });
|
|
2313
3268
|
return;
|
|
2314
3269
|
}
|
|
3270
|
+
const siteIds = new Set(payload.events.map((event) => event.siteId).filter(Boolean));
|
|
3271
|
+
if (siteIds.size !== 1) {
|
|
3272
|
+
sendJson(res, 200, { ok: true });
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
const siteId = Array.from(siteIds)[0];
|
|
2315
3276
|
const userAgent = req.headers?.["user-agent"] || "";
|
|
2316
3277
|
if (isBot(userAgent)) {
|
|
2317
3278
|
sendJson(res, 200, { ok: true });
|
|
@@ -2319,29 +3280,20 @@ async function createCollector(config) {
|
|
|
2319
3280
|
}
|
|
2320
3281
|
const ip = extractIp(req);
|
|
2321
3282
|
const enriched = enrichEvents(payload.events, ip, userAgent);
|
|
2322
|
-
const
|
|
2323
|
-
if (
|
|
2324
|
-
const
|
|
2325
|
-
if (
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
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);
|
|
3283
|
+
const site = await db.getSite(siteId);
|
|
3284
|
+
if (site?.allowedOrigins && site.allowedOrigins.length > 0) {
|
|
3285
|
+
const requestHostname = extractRequestHostname(req);
|
|
3286
|
+
if (!requestHostname) {
|
|
3287
|
+
sendJson(res, 200, { ok: true });
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
const allowed = new Set(site.allowedOrigins.map((h) => h.toLowerCase()));
|
|
3291
|
+
if (!allowed.has(requestHostname)) {
|
|
2341
3292
|
sendJson(res, 200, { ok: true });
|
|
2342
3293
|
return;
|
|
2343
3294
|
}
|
|
2344
3295
|
}
|
|
3296
|
+
await processIdentity(enriched);
|
|
2345
3297
|
await db.insertEvents(enriched);
|
|
2346
3298
|
sendJson(res, 200, { ok: true });
|
|
2347
3299
|
} catch (err) {
|
|
@@ -2602,11 +3554,11 @@ async function createCollector(config) {
|
|
|
2602
3554
|
async listUsers(params) {
|
|
2603
3555
|
return db.listUsers(params);
|
|
2604
3556
|
},
|
|
2605
|
-
async getUserDetail(siteId,
|
|
2606
|
-
return db.getUserDetail(siteId,
|
|
3557
|
+
async getUserDetail(siteId, identifier) {
|
|
3558
|
+
return db.getUserDetail(siteId, identifier);
|
|
2607
3559
|
},
|
|
2608
|
-
async getUserEvents(siteId,
|
|
2609
|
-
return db.getUserEvents(siteId,
|
|
3560
|
+
async getUserEvents(siteId, identifier, params) {
|
|
3561
|
+
return db.getUserEvents(siteId, identifier, params);
|
|
2610
3562
|
},
|
|
2611
3563
|
async track(siteId, name, properties, options) {
|
|
2612
3564
|
const event = {
|