@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.js
CHANGED
|
@@ -140,6 +140,18 @@ function generateSecretKey() {
|
|
|
140
140
|
// src/adapters/clickhouse.ts
|
|
141
141
|
var EVENTS_TABLE = "litemetrics_events";
|
|
142
142
|
var SITES_TABLE = "litemetrics_sites";
|
|
143
|
+
var IDENTITY_MAP_TABLE = "litemetrics_identity_map";
|
|
144
|
+
var CREATE_IDENTITY_MAP_TABLE = `
|
|
145
|
+
CREATE TABLE IF NOT EXISTS ${IDENTITY_MAP_TABLE} (
|
|
146
|
+
site_id LowCardinality(String),
|
|
147
|
+
visitor_id String,
|
|
148
|
+
user_id String,
|
|
149
|
+
identified_at DateTime64(3),
|
|
150
|
+
created_at DateTime64(3) DEFAULT now64(3)
|
|
151
|
+
) ENGINE = ReplacingMergeTree(created_at)
|
|
152
|
+
ORDER BY (site_id, visitor_id)
|
|
153
|
+
SETTINGS index_granularity = 8192
|
|
154
|
+
`;
|
|
143
155
|
var CREATE_EVENTS_TABLE = `
|
|
144
156
|
CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
145
157
|
event_id UUID DEFAULT generateUUIDv4(),
|
|
@@ -178,6 +190,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
|
178
190
|
utm_term Nullable(String),
|
|
179
191
|
utm_content Nullable(String),
|
|
180
192
|
ip Nullable(String),
|
|
193
|
+
os_version LowCardinality(Nullable(String)),
|
|
194
|
+
device_model LowCardinality(Nullable(String)),
|
|
195
|
+
device_brand LowCardinality(Nullable(String)),
|
|
196
|
+
app_version LowCardinality(Nullable(String)),
|
|
197
|
+
app_build Nullable(String),
|
|
198
|
+
sdk_name LowCardinality(Nullable(String)),
|
|
199
|
+
sdk_version LowCardinality(Nullable(String)),
|
|
181
200
|
created_at DateTime64(3) DEFAULT now64(3)
|
|
182
201
|
) ENGINE = MergeTree()
|
|
183
202
|
PARTITION BY toYYYYMM(timestamp)
|
|
@@ -189,6 +208,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
189
208
|
site_id String,
|
|
190
209
|
secret_key String,
|
|
191
210
|
name String,
|
|
211
|
+
type LowCardinality(Nullable(String)) DEFAULT 'web',
|
|
192
212
|
domain Nullable(String),
|
|
193
213
|
allowed_origins Nullable(String),
|
|
194
214
|
conversion_events Nullable(String),
|
|
@@ -204,6 +224,65 @@ function toCHDateTime(d) {
|
|
|
204
224
|
const iso = typeof d === "string" ? d : d.toISOString();
|
|
205
225
|
return iso.replace("T", " ").replace("Z", "");
|
|
206
226
|
}
|
|
227
|
+
function normalizedUtmSourceExpr() {
|
|
228
|
+
return `multiIf(
|
|
229
|
+
lower(utm_source) IN ('ig','instagram','instagram.com'), 'Instagram',
|
|
230
|
+
lower(utm_source) IN ('fb','facebook','facebook.com','fb.com'), 'Facebook',
|
|
231
|
+
lower(utm_source) IN ('tw','twitter','twitter.com','x','x.com','t.co'), 'X (Twitter)',
|
|
232
|
+
lower(utm_source) IN ('li','linkedin','linkedin.com'), 'LinkedIn',
|
|
233
|
+
lower(utm_source) IN ('yt','youtube','youtube.com'), 'YouTube',
|
|
234
|
+
lower(utm_source) IN ('goog','google','google.com'), 'Google',
|
|
235
|
+
lower(utm_source) IN ('gh','github','github.com'), 'GitHub',
|
|
236
|
+
lower(utm_source) IN ('reddit','reddit.com'), 'Reddit',
|
|
237
|
+
lower(utm_source) IN ('pinterest','pinterest.com'), 'Pinterest',
|
|
238
|
+
lower(utm_source) IN ('tiktok','tiktok.com'), 'TikTok',
|
|
239
|
+
lower(utm_source) IN ('openai','chatgpt','chat.openai.com'), 'OpenAI',
|
|
240
|
+
lower(utm_source) IN ('perplexity','perplexity.ai'), 'Perplexity',
|
|
241
|
+
utm_source
|
|
242
|
+
)`;
|
|
243
|
+
}
|
|
244
|
+
function normalizedUtmMediumExpr() {
|
|
245
|
+
return `multiIf(
|
|
246
|
+
lower(utm_medium) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid'), 'Paid',
|
|
247
|
+
lower(utm_medium) IN ('organic'), 'Organic',
|
|
248
|
+
lower(utm_medium) IN ('social','social-media','social_media'), 'Social',
|
|
249
|
+
lower(utm_medium) IN ('email','e-mail','e_mail'), 'Email',
|
|
250
|
+
lower(utm_medium) IN ('display','banner','cpm'), 'Display',
|
|
251
|
+
lower(utm_medium) IN ('affiliate'), 'Affiliate',
|
|
252
|
+
lower(utm_medium) IN ('referral'), 'Referral',
|
|
253
|
+
utm_medium
|
|
254
|
+
)`;
|
|
255
|
+
}
|
|
256
|
+
function channelClassificationExpr() {
|
|
257
|
+
return `multiIf(
|
|
258
|
+
lower(ifNull(utm_medium,'')) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')
|
|
259
|
+
AND (lower(ifNull(utm_source,'')) IN ('google','goog','bing','yahoo','duckduckgo','ecosia','baidu','yandex')
|
|
260
|
+
OR multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['google','bing','yahoo','duckduckgo','ecosia','baidu','yandex','search.brave']) > 0),
|
|
261
|
+
'Paid Search',
|
|
262
|
+
lower(ifNull(utm_medium,'')) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')
|
|
263
|
+
AND (lower(ifNull(utm_source,'')) IN ('instagram','ig','facebook','fb','twitter','tw','x','linkedin','li','youtube','yt','tiktok','pinterest','reddit','snapchat')
|
|
264
|
+
OR multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['instagram','facebook','twitter','x.com','t.co','linkedin','youtube','tiktok','pinterest','reddit','snapchat']) > 0),
|
|
265
|
+
'Paid Social',
|
|
266
|
+
lower(ifNull(utm_medium,'')) IN ('email','e-mail','e_mail'),
|
|
267
|
+
'Email',
|
|
268
|
+
lower(ifNull(utm_medium,'')) IN ('display','banner','cpm'),
|
|
269
|
+
'Display',
|
|
270
|
+
lower(ifNull(utm_medium,'')) IN ('affiliate'),
|
|
271
|
+
'Affiliate',
|
|
272
|
+
multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['google','bing','yahoo','duckduckgo','ecosia','baidu','yandex','search.brave']) > 0
|
|
273
|
+
AND (ifNull(utm_medium,'') = '' OR lower(utm_medium) NOT IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')),
|
|
274
|
+
'Organic Search',
|
|
275
|
+
(multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['instagram','facebook','twitter','x.com','t.co','linkedin','youtube','tiktok','pinterest','reddit','snapchat','mastodon','tumblr']) > 0
|
|
276
|
+
OR lower(ifNull(utm_source,'')) IN ('instagram','ig','facebook','fb','twitter','tw','x','linkedin','li','youtube','yt','tiktok','pinterest','reddit','snapchat'))
|
|
277
|
+
AND (ifNull(utm_medium,'') = '' OR lower(utm_medium) NOT IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')),
|
|
278
|
+
'Organic Social',
|
|
279
|
+
ifNull(referrer,'') != '' AND length(ifNull(referrer,'')) > 0,
|
|
280
|
+
'Referral',
|
|
281
|
+
(ifNull(utm_source,'') != '' OR ifNull(utm_medium,'') != '' OR ifNull(utm_campaign,'') != ''),
|
|
282
|
+
'Other',
|
|
283
|
+
'Direct'
|
|
284
|
+
)`;
|
|
285
|
+
}
|
|
207
286
|
function buildFilterConditions(filters) {
|
|
208
287
|
if (!filters) return { conditions: [], params: {} };
|
|
209
288
|
const map = {
|
|
@@ -214,6 +293,10 @@ function buildFilterConditions(filters) {
|
|
|
214
293
|
"device.type": "device_type",
|
|
215
294
|
"device.browser": "browser",
|
|
216
295
|
"device.os": "os",
|
|
296
|
+
"device.osVersion": "os_version",
|
|
297
|
+
"device.deviceModel": "device_model",
|
|
298
|
+
"device.deviceBrand": "device_brand",
|
|
299
|
+
"device.appVersion": "app_version",
|
|
217
300
|
"utm.source": "utm_source",
|
|
218
301
|
"utm.medium": "utm_medium",
|
|
219
302
|
"utm.campaign": "utm_campaign",
|
|
@@ -230,7 +313,14 @@ function buildFilterConditions(filters) {
|
|
|
230
313
|
const conditions = [];
|
|
231
314
|
const params = {};
|
|
232
315
|
for (const [key, value] of Object.entries(filters)) {
|
|
233
|
-
if (!value
|
|
316
|
+
if (!value) continue;
|
|
317
|
+
if (key === "channel") {
|
|
318
|
+
const paramKey2 = "f_channel";
|
|
319
|
+
conditions.push(`${channelClassificationExpr()} = {${paramKey2}:String}`);
|
|
320
|
+
params[paramKey2] = value;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (!map[key]) continue;
|
|
234
324
|
const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
235
325
|
conditions.push(`${map[key]} = {${paramKey}:String}`);
|
|
236
326
|
params[paramKey] = value;
|
|
@@ -250,6 +340,7 @@ var ClickHouseAdapter = class {
|
|
|
250
340
|
async init() {
|
|
251
341
|
await this.client.command({ query: CREATE_EVENTS_TABLE });
|
|
252
342
|
await this.client.command({ query: CREATE_SITES_TABLE });
|
|
343
|
+
await this.client.command({ query: CREATE_IDENTITY_MAP_TABLE });
|
|
253
344
|
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_source LowCardinality(Nullable(String))` });
|
|
254
345
|
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS event_subtype LowCardinality(Nullable(String))` });
|
|
255
346
|
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS page_path Nullable(String)` });
|
|
@@ -260,6 +351,14 @@ var ClickHouseAdapter = class {
|
|
|
260
351
|
await this.client.command({
|
|
261
352
|
query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
|
|
262
353
|
});
|
|
354
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS os_version LowCardinality(Nullable(String))` });
|
|
355
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS device_model LowCardinality(Nullable(String))` });
|
|
356
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS device_brand LowCardinality(Nullable(String))` });
|
|
357
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS app_version LowCardinality(Nullable(String))` });
|
|
358
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS app_build Nullable(String)` });
|
|
359
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS sdk_name LowCardinality(Nullable(String))` });
|
|
360
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS sdk_version LowCardinality(Nullable(String))` });
|
|
361
|
+
await this.client.command({ query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS type LowCardinality(Nullable(String)) DEFAULT 'web'` });
|
|
263
362
|
}
|
|
264
363
|
async close() {
|
|
265
364
|
await this.client.close();
|
|
@@ -302,7 +401,14 @@ var ClickHouseAdapter = class {
|
|
|
302
401
|
utm_campaign: e.utm?.campaign ?? null,
|
|
303
402
|
utm_term: e.utm?.term ?? null,
|
|
304
403
|
utm_content: e.utm?.content ?? null,
|
|
305
|
-
ip: e.ip ?? null
|
|
404
|
+
ip: e.ip ?? null,
|
|
405
|
+
os_version: e.device?.osVersion ?? null,
|
|
406
|
+
device_model: e.device?.deviceModel ?? null,
|
|
407
|
+
device_brand: e.device?.deviceBrand ?? null,
|
|
408
|
+
app_version: e.device?.appVersion ?? null,
|
|
409
|
+
app_build: e.device?.appBuild ?? null,
|
|
410
|
+
sdk_name: e.device?.sdkName ?? null,
|
|
411
|
+
sdk_version: e.device?.sdkVersion ?? null
|
|
306
412
|
}));
|
|
307
413
|
await this.client.insert({
|
|
308
414
|
table: EVENTS_TABLE,
|
|
@@ -507,8 +613,8 @@ var ClickHouseAdapter = class {
|
|
|
507
613
|
}
|
|
508
614
|
case "top_exit_pages": {
|
|
509
615
|
const rows = await this.queryRows(
|
|
510
|
-
`SELECT
|
|
511
|
-
SELECT session_id, argMax(url, timestamp) AS
|
|
616
|
+
`SELECT exit_url AS key, count() AS value FROM (
|
|
617
|
+
SELECT session_id, argMax(url, timestamp) AS exit_url
|
|
512
618
|
FROM ${EVENTS_TABLE}
|
|
513
619
|
WHERE site_id = {siteId:String}
|
|
514
620
|
AND timestamp >= {from:String}
|
|
@@ -517,7 +623,7 @@ var ClickHouseAdapter = class {
|
|
|
517
623
|
AND url IS NOT NULL${filterSql}
|
|
518
624
|
GROUP BY session_id
|
|
519
625
|
)
|
|
520
|
-
GROUP BY
|
|
626
|
+
GROUP BY exit_url
|
|
521
627
|
ORDER BY value DESC
|
|
522
628
|
LIMIT {limit:UInt32}`,
|
|
523
629
|
{ ...params, ...filter.params }
|
|
@@ -528,9 +634,9 @@ var ClickHouseAdapter = class {
|
|
|
528
634
|
}
|
|
529
635
|
case "top_transitions": {
|
|
530
636
|
const rows = await this.queryRows(
|
|
531
|
-
`SELECT concat(prev_url, ' \u2192 ',
|
|
532
|
-
SELECT session_id, url,
|
|
533
|
-
|
|
637
|
+
`SELECT concat(prev_url, ' \u2192 ', curr_url) AS key, count() AS value FROM (
|
|
638
|
+
SELECT session_id, url AS curr_url,
|
|
639
|
+
lagInFrame(url, 1) OVER (PARTITION BY session_id ORDER BY timestamp ASC) AS prev_url
|
|
534
640
|
FROM ${EVENTS_TABLE}
|
|
535
641
|
WHERE site_id = {siteId:String}
|
|
536
642
|
AND timestamp >= {from:String}
|
|
@@ -538,7 +644,7 @@ var ClickHouseAdapter = class {
|
|
|
538
644
|
AND type = 'pageview'
|
|
539
645
|
AND url IS NOT NULL${filterSql}
|
|
540
646
|
)
|
|
541
|
-
WHERE prev_url IS NOT NULL
|
|
647
|
+
WHERE prev_url IS NOT NULL AND prev_url != ''
|
|
542
648
|
GROUP BY key
|
|
543
649
|
ORDER BY value DESC
|
|
544
650
|
LIMIT {limit:UInt32}`,
|
|
@@ -653,6 +759,159 @@ var ClickHouseAdapter = class {
|
|
|
653
759
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
654
760
|
break;
|
|
655
761
|
}
|
|
762
|
+
case "top_os_versions": {
|
|
763
|
+
const rows = await this.queryRows(
|
|
764
|
+
`SELECT concat(os, ' ', ifNull(os_version, '')) AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
765
|
+
WHERE site_id = {siteId:String}
|
|
766
|
+
AND timestamp >= {from:String}
|
|
767
|
+
AND timestamp <= {to:String}
|
|
768
|
+
AND os IS NOT NULL
|
|
769
|
+
AND os_version IS NOT NULL
|
|
770
|
+
${filterSql}
|
|
771
|
+
GROUP BY key
|
|
772
|
+
ORDER BY value DESC
|
|
773
|
+
LIMIT {limit:UInt32}`,
|
|
774
|
+
{ ...params, ...filter.params }
|
|
775
|
+
);
|
|
776
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
777
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
case "top_device_models": {
|
|
781
|
+
const rows = await this.queryRows(
|
|
782
|
+
`SELECT trim(concat(ifNull(device_brand, ''), ' ', device_model)) AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
783
|
+
WHERE site_id = {siteId:String}
|
|
784
|
+
AND timestamp >= {from:String}
|
|
785
|
+
AND timestamp <= {to:String}
|
|
786
|
+
AND device_model IS NOT NULL
|
|
787
|
+
${filterSql}
|
|
788
|
+
GROUP BY key
|
|
789
|
+
ORDER BY value DESC
|
|
790
|
+
LIMIT {limit:UInt32}`,
|
|
791
|
+
{ ...params, ...filter.params }
|
|
792
|
+
);
|
|
793
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
794
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
case "top_app_versions": {
|
|
798
|
+
const rows = await this.queryRows(
|
|
799
|
+
`SELECT app_version AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
800
|
+
WHERE site_id = {siteId:String}
|
|
801
|
+
AND timestamp >= {from:String}
|
|
802
|
+
AND timestamp <= {to:String}
|
|
803
|
+
AND app_version IS NOT NULL
|
|
804
|
+
${filterSql}
|
|
805
|
+
GROUP BY app_version
|
|
806
|
+
ORDER BY value DESC
|
|
807
|
+
LIMIT {limit:UInt32}`,
|
|
808
|
+
{ ...params, ...filter.params }
|
|
809
|
+
);
|
|
810
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
811
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case "top_utm_sources": {
|
|
815
|
+
const rows = await this.queryRows(
|
|
816
|
+
`SELECT ${normalizedUtmSourceExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
817
|
+
WHERE site_id = {siteId:String}
|
|
818
|
+
AND timestamp >= {from:String}
|
|
819
|
+
AND timestamp <= {to:String}
|
|
820
|
+
AND utm_source IS NOT NULL AND utm_source != ''
|
|
821
|
+
${filterSql}
|
|
822
|
+
GROUP BY key
|
|
823
|
+
ORDER BY value DESC
|
|
824
|
+
LIMIT {limit:UInt32}`,
|
|
825
|
+
{ ...params, ...filter.params }
|
|
826
|
+
);
|
|
827
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
828
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case "top_utm_mediums": {
|
|
832
|
+
const rows = await this.queryRows(
|
|
833
|
+
`SELECT ${normalizedUtmMediumExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
834
|
+
WHERE site_id = {siteId:String}
|
|
835
|
+
AND timestamp >= {from:String}
|
|
836
|
+
AND timestamp <= {to:String}
|
|
837
|
+
AND utm_medium IS NOT NULL AND utm_medium != ''
|
|
838
|
+
${filterSql}
|
|
839
|
+
GROUP BY key
|
|
840
|
+
ORDER BY value DESC
|
|
841
|
+
LIMIT {limit:UInt32}`,
|
|
842
|
+
{ ...params, ...filter.params }
|
|
843
|
+
);
|
|
844
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
845
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
case "top_utm_campaigns": {
|
|
849
|
+
const rows = await this.queryRows(
|
|
850
|
+
`SELECT utm_campaign AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
851
|
+
WHERE site_id = {siteId:String}
|
|
852
|
+
AND timestamp >= {from:String}
|
|
853
|
+
AND timestamp <= {to:String}
|
|
854
|
+
AND utm_campaign IS NOT NULL AND utm_campaign != ''
|
|
855
|
+
${filterSql}
|
|
856
|
+
GROUP BY utm_campaign
|
|
857
|
+
ORDER BY value DESC
|
|
858
|
+
LIMIT {limit:UInt32}`,
|
|
859
|
+
{ ...params, ...filter.params }
|
|
860
|
+
);
|
|
861
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
862
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
case "top_utm_terms": {
|
|
866
|
+
const rows = await this.queryRows(
|
|
867
|
+
`SELECT utm_term AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
868
|
+
WHERE site_id = {siteId:String}
|
|
869
|
+
AND timestamp >= {from:String}
|
|
870
|
+
AND timestamp <= {to:String}
|
|
871
|
+
AND utm_term IS NOT NULL AND utm_term != ''
|
|
872
|
+
${filterSql}
|
|
873
|
+
GROUP BY utm_term
|
|
874
|
+
ORDER BY value DESC
|
|
875
|
+
LIMIT {limit:UInt32}`,
|
|
876
|
+
{ ...params, ...filter.params }
|
|
877
|
+
);
|
|
878
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
879
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
case "top_utm_contents": {
|
|
883
|
+
const rows = await this.queryRows(
|
|
884
|
+
`SELECT utm_content AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
885
|
+
WHERE site_id = {siteId:String}
|
|
886
|
+
AND timestamp >= {from:String}
|
|
887
|
+
AND timestamp <= {to:String}
|
|
888
|
+
AND utm_content IS NOT NULL AND utm_content != ''
|
|
889
|
+
${filterSql}
|
|
890
|
+
GROUP BY utm_content
|
|
891
|
+
ORDER BY value DESC
|
|
892
|
+
LIMIT {limit:UInt32}`,
|
|
893
|
+
{ ...params, ...filter.params }
|
|
894
|
+
);
|
|
895
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
896
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
case "top_channels": {
|
|
900
|
+
const rows = await this.queryRows(
|
|
901
|
+
`SELECT ${channelClassificationExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
902
|
+
WHERE site_id = {siteId:String}
|
|
903
|
+
AND timestamp >= {from:String}
|
|
904
|
+
AND timestamp <= {to:String}
|
|
905
|
+
${filterSql}
|
|
906
|
+
GROUP BY key
|
|
907
|
+
ORDER BY value DESC
|
|
908
|
+
LIMIT {limit:UInt32}`,
|
|
909
|
+
{ ...params, ...filter.params }
|
|
910
|
+
);
|
|
911
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
912
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
656
915
|
}
|
|
657
916
|
const result = { metric: q.metric, period, data, total };
|
|
658
917
|
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
@@ -906,45 +1165,59 @@ var ClickHouseAdapter = class {
|
|
|
906
1165
|
const where = conditions.join(" AND ");
|
|
907
1166
|
const [userRows, countRows] = await Promise.all([
|
|
908
1167
|
this.queryRows(
|
|
909
|
-
`
|
|
910
|
-
visitor_id,
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1168
|
+
`WITH identity AS (
|
|
1169
|
+
SELECT visitor_id, user_id
|
|
1170
|
+
FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1171
|
+
WHERE site_id = {siteId:String}
|
|
1172
|
+
)
|
|
1173
|
+
SELECT
|
|
1174
|
+
if(i.user_id IS NOT NULL AND i.user_id != '', i.user_id, e.visitor_id) AS group_key,
|
|
1175
|
+
anyLast(e.visitor_id) AS visitor_id,
|
|
1176
|
+
anyLast(i.user_id) AS userId,
|
|
1177
|
+
anyLast(e.traits) AS traits,
|
|
1178
|
+
min(e.timestamp) AS firstSeen,
|
|
1179
|
+
max(e.timestamp) AS lastSeen,
|
|
915
1180
|
count() AS totalEvents,
|
|
916
|
-
countIf(type = 'pageview') AS totalPageviews,
|
|
917
|
-
uniq(session_id) AS totalSessions,
|
|
918
|
-
anyLast(url) AS lastUrl,
|
|
919
|
-
anyLast(referrer) AS referrer,
|
|
920
|
-
anyLast(device_type) AS device_type,
|
|
921
|
-
anyLast(browser) AS browser,
|
|
922
|
-
anyLast(os) AS os,
|
|
923
|
-
anyLast(country) AS country,
|
|
924
|
-
anyLast(city) AS city,
|
|
925
|
-
anyLast(region) AS region,
|
|
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
|
|
935
|
-
FROM ${EVENTS_TABLE}
|
|
936
|
-
|
|
937
|
-
|
|
1181
|
+
countIf(e.type = 'pageview') AS totalPageviews,
|
|
1182
|
+
uniq(e.session_id) AS totalSessions,
|
|
1183
|
+
anyLast(e.url) AS lastUrl,
|
|
1184
|
+
anyLast(e.referrer) AS referrer,
|
|
1185
|
+
anyLast(e.device_type) AS device_type,
|
|
1186
|
+
anyLast(e.browser) AS browser,
|
|
1187
|
+
anyLast(e.os) AS os,
|
|
1188
|
+
anyLast(e.country) AS country,
|
|
1189
|
+
anyLast(e.city) AS city,
|
|
1190
|
+
anyLast(e.region) AS region,
|
|
1191
|
+
anyLast(e.language) AS language,
|
|
1192
|
+
anyLast(e.timezone) AS timezone,
|
|
1193
|
+
anyLast(e.screen_width) AS screen_width,
|
|
1194
|
+
anyLast(e.screen_height) AS screen_height,
|
|
1195
|
+
anyLast(e.utm_source) AS utm_source,
|
|
1196
|
+
anyLast(e.utm_medium) AS utm_medium,
|
|
1197
|
+
anyLast(e.utm_campaign) AS utm_campaign,
|
|
1198
|
+
anyLast(e.utm_term) AS utm_term,
|
|
1199
|
+
anyLast(e.utm_content) AS utm_content
|
|
1200
|
+
FROM ${EVENTS_TABLE} e
|
|
1201
|
+
LEFT JOIN identity i ON e.visitor_id = i.visitor_id
|
|
1202
|
+
WHERE e.site_id = {siteId:String}${where.includes("ILIKE") ? ` AND (e.visitor_id ILIKE {search:String} OR i.user_id ILIKE {search:String})` : ""}
|
|
1203
|
+
GROUP BY group_key
|
|
938
1204
|
ORDER BY lastSeen DESC
|
|
939
1205
|
LIMIT {limit:UInt32}
|
|
940
1206
|
OFFSET {offset:UInt32}`,
|
|
941
1207
|
queryParams
|
|
942
1208
|
),
|
|
943
1209
|
this.queryRows(
|
|
944
|
-
`
|
|
945
|
-
SELECT visitor_id
|
|
946
|
-
|
|
947
|
-
|
|
1210
|
+
`WITH identity AS (
|
|
1211
|
+
SELECT visitor_id, user_id
|
|
1212
|
+
FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1213
|
+
WHERE site_id = {siteId:String}
|
|
1214
|
+
)
|
|
1215
|
+
SELECT count() AS total FROM (
|
|
1216
|
+
SELECT if(i.user_id IS NOT NULL AND i.user_id != '', i.user_id, e.visitor_id) AS group_key
|
|
1217
|
+
FROM ${EVENTS_TABLE} e
|
|
1218
|
+
LEFT JOIN identity i ON e.visitor_id = i.visitor_id
|
|
1219
|
+
WHERE e.site_id = {siteId:String}${where.includes("ILIKE") ? ` AND (e.visitor_id ILIKE {search:String} OR i.user_id ILIKE {search:String})` : ""}
|
|
1220
|
+
GROUP BY group_key
|
|
948
1221
|
)`,
|
|
949
1222
|
queryParams
|
|
950
1223
|
)
|
|
@@ -980,13 +1253,178 @@ var ClickHouseAdapter = class {
|
|
|
980
1253
|
offset
|
|
981
1254
|
};
|
|
982
1255
|
}
|
|
983
|
-
async getUserDetail(siteId,
|
|
984
|
-
const
|
|
985
|
-
|
|
1256
|
+
async getUserDetail(siteId, identifier) {
|
|
1257
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
1258
|
+
if (visitorIds.length > 0) {
|
|
1259
|
+
return this.getMergedUserDetail(siteId, identifier, visitorIds);
|
|
1260
|
+
}
|
|
1261
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
1262
|
+
if (userId) {
|
|
1263
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
1264
|
+
return this.getMergedUserDetail(siteId, userId, allVisitorIds.length > 0 ? allVisitorIds : [identifier]);
|
|
1265
|
+
}
|
|
1266
|
+
const result = await this.listUsers({ siteId, search: identifier, limit: 1 });
|
|
1267
|
+
const user = result.users.find((u) => u.visitorId === identifier);
|
|
986
1268
|
return user ?? null;
|
|
987
1269
|
}
|
|
988
|
-
async getUserEvents(siteId,
|
|
989
|
-
|
|
1270
|
+
async getUserEvents(siteId, identifier, params) {
|
|
1271
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
1272
|
+
if (visitorIds.length > 0) {
|
|
1273
|
+
return this.listEventsForVisitorIds(siteId, visitorIds, params);
|
|
1274
|
+
}
|
|
1275
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
1276
|
+
if (userId) {
|
|
1277
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
1278
|
+
if (allVisitorIds.length > 0) {
|
|
1279
|
+
return this.listEventsForVisitorIds(siteId, allVisitorIds, params);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return this.listEvents({ ...params, siteId, visitorId: identifier });
|
|
1283
|
+
}
|
|
1284
|
+
// ─── Identity Mapping ──────────────────────────────────────
|
|
1285
|
+
async upsertIdentity(siteId, visitorId, userId) {
|
|
1286
|
+
await this.client.insert({
|
|
1287
|
+
table: IDENTITY_MAP_TABLE,
|
|
1288
|
+
values: [{
|
|
1289
|
+
site_id: siteId,
|
|
1290
|
+
visitor_id: visitorId,
|
|
1291
|
+
user_id: userId,
|
|
1292
|
+
identified_at: toCHDateTime(/* @__PURE__ */ new Date())
|
|
1293
|
+
}],
|
|
1294
|
+
format: "JSONEachRow"
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
async getVisitorIdsForUser(siteId, userId) {
|
|
1298
|
+
const rows = await this.queryRows(
|
|
1299
|
+
`SELECT visitor_id FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1300
|
+
WHERE site_id = {siteId:String} AND user_id = {userId:String}`,
|
|
1301
|
+
{ siteId, userId }
|
|
1302
|
+
);
|
|
1303
|
+
return rows.map((r) => r.visitor_id);
|
|
1304
|
+
}
|
|
1305
|
+
async getUserIdForVisitor(siteId, visitorId) {
|
|
1306
|
+
const rows = await this.queryRows(
|
|
1307
|
+
`SELECT user_id FROM ${IDENTITY_MAP_TABLE} FINAL
|
|
1308
|
+
WHERE site_id = {siteId:String} AND visitor_id = {visitorId:String}
|
|
1309
|
+
LIMIT 1`,
|
|
1310
|
+
{ siteId, visitorId }
|
|
1311
|
+
);
|
|
1312
|
+
return rows.length > 0 ? rows[0].user_id : null;
|
|
1313
|
+
}
|
|
1314
|
+
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
1315
|
+
const rows = await this.queryRows(
|
|
1316
|
+
`SELECT
|
|
1317
|
+
anyLast(visitor_id) AS last_visitor_id,
|
|
1318
|
+
anyLast(traits) AS traits,
|
|
1319
|
+
min(timestamp) AS firstSeen,
|
|
1320
|
+
max(timestamp) AS lastSeen,
|
|
1321
|
+
count() AS totalEvents,
|
|
1322
|
+
countIf(type = 'pageview') AS totalPageviews,
|
|
1323
|
+
uniq(session_id) AS totalSessions,
|
|
1324
|
+
anyLast(url) AS lastUrl,
|
|
1325
|
+
anyLast(referrer) AS referrer,
|
|
1326
|
+
anyLast(device_type) AS device_type,
|
|
1327
|
+
anyLast(browser) AS browser,
|
|
1328
|
+
anyLast(os) AS os,
|
|
1329
|
+
anyLast(country) AS country,
|
|
1330
|
+
anyLast(city) AS city,
|
|
1331
|
+
anyLast(region) AS region,
|
|
1332
|
+
anyLast(language) AS language,
|
|
1333
|
+
anyLast(timezone) AS timezone,
|
|
1334
|
+
anyLast(screen_width) AS screen_width,
|
|
1335
|
+
anyLast(screen_height) AS screen_height,
|
|
1336
|
+
anyLast(utm_source) AS utm_source,
|
|
1337
|
+
anyLast(utm_medium) AS utm_medium,
|
|
1338
|
+
anyLast(utm_campaign) AS utm_campaign,
|
|
1339
|
+
anyLast(utm_term) AS utm_term,
|
|
1340
|
+
anyLast(utm_content) AS utm_content
|
|
1341
|
+
FROM ${EVENTS_TABLE}
|
|
1342
|
+
WHERE site_id = {siteId:String}
|
|
1343
|
+
AND visitor_id IN {visitorIds:Array(String)}`,
|
|
1344
|
+
{ siteId, visitorIds }
|
|
1345
|
+
);
|
|
1346
|
+
if (rows.length === 0) return null;
|
|
1347
|
+
const u = rows[0];
|
|
1348
|
+
return {
|
|
1349
|
+
visitorId: String(u.last_visitor_id),
|
|
1350
|
+
visitorIds,
|
|
1351
|
+
userId,
|
|
1352
|
+
traits: this.parseJSON(u.traits),
|
|
1353
|
+
firstSeen: new Date(String(u.firstSeen)).toISOString(),
|
|
1354
|
+
lastSeen: new Date(String(u.lastSeen)).toISOString(),
|
|
1355
|
+
totalEvents: Number(u.totalEvents),
|
|
1356
|
+
totalPageviews: Number(u.totalPageviews),
|
|
1357
|
+
totalSessions: Number(u.totalSessions),
|
|
1358
|
+
lastUrl: u.lastUrl ? String(u.lastUrl) : void 0,
|
|
1359
|
+
referrer: u.referrer ? String(u.referrer) : void 0,
|
|
1360
|
+
device: u.device_type ? { type: String(u.device_type), browser: String(u.browser ?? ""), os: String(u.os ?? "") } : void 0,
|
|
1361
|
+
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,
|
|
1362
|
+
language: u.language ? String(u.language) : void 0,
|
|
1363
|
+
timezone: u.timezone ? String(u.timezone) : void 0,
|
|
1364
|
+
screen: u.screen_width || u.screen_height ? { width: Number(u.screen_width ?? 0), height: Number(u.screen_height ?? 0) } : void 0,
|
|
1365
|
+
utm: u.utm_source ? {
|
|
1366
|
+
source: String(u.utm_source),
|
|
1367
|
+
medium: u.utm_medium ? String(u.utm_medium) : void 0,
|
|
1368
|
+
campaign: u.utm_campaign ? String(u.utm_campaign) : void 0,
|
|
1369
|
+
term: u.utm_term ? String(u.utm_term) : void 0,
|
|
1370
|
+
content: u.utm_content ? String(u.utm_content) : void 0
|
|
1371
|
+
} : void 0
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
async listEventsForVisitorIds(siteId, visitorIds, params) {
|
|
1375
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
1376
|
+
const offset = params.offset ?? 0;
|
|
1377
|
+
const conditions = [`site_id = {siteId:String}`, `visitor_id IN {visitorIds:Array(String)}`];
|
|
1378
|
+
const queryParams = { siteId, visitorIds, limit, offset };
|
|
1379
|
+
if (params.type) {
|
|
1380
|
+
conditions.push(`type = {type:String}`);
|
|
1381
|
+
queryParams.type = params.type;
|
|
1382
|
+
}
|
|
1383
|
+
if (params.eventName) {
|
|
1384
|
+
conditions.push(`event_name = {eventName:String}`);
|
|
1385
|
+
queryParams.eventName = params.eventName;
|
|
1386
|
+
}
|
|
1387
|
+
if (params.eventNames && params.eventNames.length > 0) {
|
|
1388
|
+
conditions.push(`event_name IN {eventNames:Array(String)}`);
|
|
1389
|
+
queryParams.eventNames = params.eventNames;
|
|
1390
|
+
}
|
|
1391
|
+
if (params.period || params.dateFrom) {
|
|
1392
|
+
const { dateRange } = resolvePeriod({
|
|
1393
|
+
period: params.period,
|
|
1394
|
+
dateFrom: params.dateFrom,
|
|
1395
|
+
dateTo: params.dateTo
|
|
1396
|
+
});
|
|
1397
|
+
conditions.push(`timestamp >= {from:String} AND timestamp <= {to:String}`);
|
|
1398
|
+
queryParams.from = toCHDateTime(dateRange.from);
|
|
1399
|
+
queryParams.to = toCHDateTime(dateRange.to);
|
|
1400
|
+
}
|
|
1401
|
+
const where = conditions.join(" AND ");
|
|
1402
|
+
const [events, countRows] = await Promise.all([
|
|
1403
|
+
this.queryRows(
|
|
1404
|
+
`SELECT event_id, type, timestamp, session_id, visitor_id, url, referrer, title,
|
|
1405
|
+
event_name, properties, event_source, event_subtype, page_path, target_url_path,
|
|
1406
|
+
element_selector, element_text, scroll_depth_pct,
|
|
1407
|
+
user_id, traits, country, city, region,
|
|
1408
|
+
device_type, browser, os, language,
|
|
1409
|
+
utm_source, utm_medium, utm_campaign, utm_term, utm_content
|
|
1410
|
+
FROM ${EVENTS_TABLE}
|
|
1411
|
+
WHERE ${where}
|
|
1412
|
+
ORDER BY timestamp DESC
|
|
1413
|
+
LIMIT {limit:UInt32}
|
|
1414
|
+
OFFSET {offset:UInt32}`,
|
|
1415
|
+
queryParams
|
|
1416
|
+
),
|
|
1417
|
+
this.queryRows(
|
|
1418
|
+
`SELECT count() AS total FROM ${EVENTS_TABLE} WHERE ${where}`,
|
|
1419
|
+
queryParams
|
|
1420
|
+
)
|
|
1421
|
+
]);
|
|
1422
|
+
return {
|
|
1423
|
+
events: events.map((e) => this.toEventListItem(e)),
|
|
1424
|
+
total: Number(countRows[0]?.total ?? 0),
|
|
1425
|
+
limit,
|
|
1426
|
+
offset
|
|
1427
|
+
};
|
|
990
1428
|
}
|
|
991
1429
|
// ─── Site Management ──────────────────────────────────────
|
|
992
1430
|
async createSite(data) {
|
|
@@ -997,6 +1435,7 @@ var ClickHouseAdapter = class {
|
|
|
997
1435
|
siteId: generateSiteId(),
|
|
998
1436
|
secretKey: generateSecretKey(),
|
|
999
1437
|
name: data.name,
|
|
1438
|
+
type: data.type ?? "web",
|
|
1000
1439
|
domain: data.domain,
|
|
1001
1440
|
allowedOrigins: data.allowedOrigins,
|
|
1002
1441
|
conversionEvents: data.conversionEvents,
|
|
@@ -1009,6 +1448,7 @@ var ClickHouseAdapter = class {
|
|
|
1009
1448
|
site_id: site.siteId,
|
|
1010
1449
|
secret_key: site.secretKey,
|
|
1011
1450
|
name: site.name,
|
|
1451
|
+
type: site.type ?? "web",
|
|
1012
1452
|
domain: site.domain ?? null,
|
|
1013
1453
|
allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
|
|
1014
1454
|
conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
|
|
@@ -1023,7 +1463,7 @@ var ClickHouseAdapter = class {
|
|
|
1023
1463
|
}
|
|
1024
1464
|
async getSite(siteId) {
|
|
1025
1465
|
const rows = await this.queryRows(
|
|
1026
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1466
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1027
1467
|
FROM ${SITES_TABLE} FINAL
|
|
1028
1468
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1029
1469
|
{ siteId }
|
|
@@ -1032,7 +1472,7 @@ var ClickHouseAdapter = class {
|
|
|
1032
1472
|
}
|
|
1033
1473
|
async getSiteBySecret(secretKey) {
|
|
1034
1474
|
const rows = await this.queryRows(
|
|
1035
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1475
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1036
1476
|
FROM ${SITES_TABLE} FINAL
|
|
1037
1477
|
WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
|
|
1038
1478
|
{ secretKey }
|
|
@@ -1041,7 +1481,7 @@ var ClickHouseAdapter = class {
|
|
|
1041
1481
|
}
|
|
1042
1482
|
async listSites() {
|
|
1043
1483
|
const rows = await this.queryRows(
|
|
1044
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1484
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1045
1485
|
FROM ${SITES_TABLE} FINAL
|
|
1046
1486
|
WHERE is_deleted = 0
|
|
1047
1487
|
ORDER BY created_at DESC`,
|
|
@@ -1051,7 +1491,7 @@ var ClickHouseAdapter = class {
|
|
|
1051
1491
|
}
|
|
1052
1492
|
async updateSite(siteId, data) {
|
|
1053
1493
|
const currentRows = await this.queryRows(
|
|
1054
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
1494
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
1055
1495
|
FROM ${SITES_TABLE} FINAL
|
|
1056
1496
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1057
1497
|
{ siteId }
|
|
@@ -1063,6 +1503,7 @@ var ClickHouseAdapter = class {
|
|
|
1063
1503
|
const nowCH = toCHDateTime(now);
|
|
1064
1504
|
const newVersion = Number(current.version) + 1;
|
|
1065
1505
|
const newName = data.name !== void 0 ? data.name : String(current.name);
|
|
1506
|
+
const newType = data.type !== void 0 ? data.type : current.type ? String(current.type) : "web";
|
|
1066
1507
|
const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
|
|
1067
1508
|
const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
|
|
1068
1509
|
const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
|
|
@@ -1072,6 +1513,7 @@ var ClickHouseAdapter = class {
|
|
|
1072
1513
|
site_id: String(current.site_id),
|
|
1073
1514
|
secret_key: String(current.secret_key),
|
|
1074
1515
|
name: newName,
|
|
1516
|
+
type: newType,
|
|
1075
1517
|
domain: newDomain,
|
|
1076
1518
|
allowed_origins: newOrigins,
|
|
1077
1519
|
conversion_events: newConversions,
|
|
@@ -1086,6 +1528,7 @@ var ClickHouseAdapter = class {
|
|
|
1086
1528
|
siteId: String(current.site_id),
|
|
1087
1529
|
secretKey: String(current.secret_key),
|
|
1088
1530
|
name: newName,
|
|
1531
|
+
type: newType,
|
|
1089
1532
|
domain: newDomain ?? void 0,
|
|
1090
1533
|
allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
|
|
1091
1534
|
conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
|
|
@@ -1095,7 +1538,7 @@ var ClickHouseAdapter = class {
|
|
|
1095
1538
|
}
|
|
1096
1539
|
async deleteSite(siteId) {
|
|
1097
1540
|
const currentRows = await this.queryRows(
|
|
1098
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
1541
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, version
|
|
1099
1542
|
FROM ${SITES_TABLE} FINAL
|
|
1100
1543
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1101
1544
|
{ siteId }
|
|
@@ -1109,6 +1552,7 @@ var ClickHouseAdapter = class {
|
|
|
1109
1552
|
site_id: String(current.site_id),
|
|
1110
1553
|
secret_key: String(current.secret_key),
|
|
1111
1554
|
name: String(current.name),
|
|
1555
|
+
type: current.type ? String(current.type) : "web",
|
|
1112
1556
|
domain: current.domain ? String(current.domain) : null,
|
|
1113
1557
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1114
1558
|
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
@@ -1123,7 +1567,7 @@ var ClickHouseAdapter = class {
|
|
|
1123
1567
|
}
|
|
1124
1568
|
async regenerateSecret(siteId) {
|
|
1125
1569
|
const currentRows = await this.queryRows(
|
|
1126
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
1570
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, version
|
|
1127
1571
|
FROM ${SITES_TABLE} FINAL
|
|
1128
1572
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1129
1573
|
{ siteId }
|
|
@@ -1140,6 +1584,7 @@ var ClickHouseAdapter = class {
|
|
|
1140
1584
|
site_id: String(current.site_id),
|
|
1141
1585
|
secret_key: newSecret,
|
|
1142
1586
|
name: String(current.name),
|
|
1587
|
+
type: current.type ? String(current.type) : "web",
|
|
1143
1588
|
domain: current.domain ? String(current.domain) : null,
|
|
1144
1589
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1145
1590
|
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
@@ -1154,6 +1599,7 @@ var ClickHouseAdapter = class {
|
|
|
1154
1599
|
siteId: String(current.site_id),
|
|
1155
1600
|
secretKey: newSecret,
|
|
1156
1601
|
name: String(current.name),
|
|
1602
|
+
type: current.type ? String(current.type) : "web",
|
|
1157
1603
|
domain: current.domain ? String(current.domain) : void 0,
|
|
1158
1604
|
allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
|
|
1159
1605
|
conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
|
|
@@ -1175,6 +1621,7 @@ var ClickHouseAdapter = class {
|
|
|
1175
1621
|
siteId: String(row.site_id),
|
|
1176
1622
|
secretKey: String(row.secret_key),
|
|
1177
1623
|
name: String(row.name),
|
|
1624
|
+
type: row.type ? String(row.type) : "web",
|
|
1178
1625
|
domain: row.domain ? String(row.domain) : void 0,
|
|
1179
1626
|
allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
|
|
1180
1627
|
conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
|
|
@@ -1237,6 +1684,131 @@ var ClickHouseAdapter = class {
|
|
|
1237
1684
|
import { MongoClient } from "mongodb";
|
|
1238
1685
|
var EVENTS_COLLECTION = "litemetrics_events";
|
|
1239
1686
|
var SITES_COLLECTION = "litemetrics_sites";
|
|
1687
|
+
var IDENTITY_MAP_COLLECTION = "litemetrics_identity_map";
|
|
1688
|
+
function normalizedUtmSourceSwitch() {
|
|
1689
|
+
return {
|
|
1690
|
+
$switch: {
|
|
1691
|
+
branches: [
|
|
1692
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["ig", "instagram", "instagram.com"]] }, then: "Instagram" },
|
|
1693
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["fb", "facebook", "facebook.com", "fb.com"]] }, then: "Facebook" },
|
|
1694
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["tw", "twitter", "twitter.com", "x", "x.com", "t.co"]] }, then: "X (Twitter)" },
|
|
1695
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["li", "linkedin", "linkedin.com"]] }, then: "LinkedIn" },
|
|
1696
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["yt", "youtube", "youtube.com"]] }, then: "YouTube" },
|
|
1697
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["goog", "google", "google.com"]] }, then: "Google" },
|
|
1698
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["gh", "github", "github.com"]] }, then: "GitHub" },
|
|
1699
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["reddit", "reddit.com"]] }, then: "Reddit" },
|
|
1700
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["pinterest", "pinterest.com"]] }, then: "Pinterest" },
|
|
1701
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["tiktok", "tiktok.com"]] }, then: "TikTok" },
|
|
1702
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["openai", "chatgpt", "chat.openai.com"]] }, then: "OpenAI" },
|
|
1703
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["perplexity", "perplexity.ai"]] }, then: "Perplexity" }
|
|
1704
|
+
],
|
|
1705
|
+
default: "$utm_source"
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function normalizedUtmMediumSwitch() {
|
|
1710
|
+
return {
|
|
1711
|
+
$switch: {
|
|
1712
|
+
branches: [
|
|
1713
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["cpc", "ppc", "paidsearch", "paid-search", "paid_search", "paid"]] }, then: "Paid" },
|
|
1714
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["organic"]] }, then: "Organic" },
|
|
1715
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["social", "social-media", "social_media"]] }, then: "Social" },
|
|
1716
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["email", "e-mail", "e_mail"]] }, then: "Email" },
|
|
1717
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["display", "banner", "cpm"]] }, then: "Display" },
|
|
1718
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["affiliate"]] }, then: "Affiliate" },
|
|
1719
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["referral"]] }, then: "Referral" }
|
|
1720
|
+
],
|
|
1721
|
+
default: "$utm_medium"
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
var SEARCH_ENGINES = /google|bing|yahoo|duckduckgo|ecosia|baidu|yandex|search\.brave/i;
|
|
1726
|
+
var SOCIAL_NETWORKS = /instagram|facebook|twitter|x\.com|t\.co|linkedin|youtube|tiktok|pinterest|reddit|snapchat|mastodon|tumblr/i;
|
|
1727
|
+
var PAID_MEDIUMS = ["cpc", "ppc", "paidsearch", "paid-search", "paid_search", "paid"];
|
|
1728
|
+
var SOCIAL_SOURCES = ["instagram", "ig", "facebook", "fb", "twitter", "tw", "x", "linkedin", "li", "youtube", "yt", "tiktok", "pinterest", "reddit", "snapchat"];
|
|
1729
|
+
function channelClassificationSwitch() {
|
|
1730
|
+
const lMedium = { $toLower: { $ifNull: ["$utm_medium", ""] } };
|
|
1731
|
+
const lSource = { $toLower: { $ifNull: ["$utm_source", ""] } };
|
|
1732
|
+
const refStr = { $ifNull: ["$referrer", ""] };
|
|
1733
|
+
return {
|
|
1734
|
+
$switch: {
|
|
1735
|
+
branches: [
|
|
1736
|
+
// Paid Search
|
|
1737
|
+
{
|
|
1738
|
+
case: {
|
|
1739
|
+
$and: [
|
|
1740
|
+
{ $in: [lMedium, PAID_MEDIUMS] },
|
|
1741
|
+
{ $or: [
|
|
1742
|
+
{ $in: [lSource, ["google", "goog", "bing", "yahoo", "duckduckgo", "ecosia", "baidu", "yandex"]] },
|
|
1743
|
+
{ $regexMatch: { input: refStr, regex: SEARCH_ENGINES } }
|
|
1744
|
+
] }
|
|
1745
|
+
]
|
|
1746
|
+
},
|
|
1747
|
+
then: "Paid Search"
|
|
1748
|
+
},
|
|
1749
|
+
// Paid Social
|
|
1750
|
+
{
|
|
1751
|
+
case: {
|
|
1752
|
+
$and: [
|
|
1753
|
+
{ $in: [lMedium, PAID_MEDIUMS] },
|
|
1754
|
+
{ $or: [
|
|
1755
|
+
{ $in: [lSource, SOCIAL_SOURCES] },
|
|
1756
|
+
{ $regexMatch: { input: refStr, regex: SOCIAL_NETWORKS } }
|
|
1757
|
+
] }
|
|
1758
|
+
]
|
|
1759
|
+
},
|
|
1760
|
+
then: "Paid Social"
|
|
1761
|
+
},
|
|
1762
|
+
// Email
|
|
1763
|
+
{ case: { $in: [lMedium, ["email", "e-mail", "e_mail"]] }, then: "Email" },
|
|
1764
|
+
// Display
|
|
1765
|
+
{ case: { $in: [lMedium, ["display", "banner", "cpm"]] }, then: "Display" },
|
|
1766
|
+
// Affiliate
|
|
1767
|
+
{ case: { $in: [lMedium, ["affiliate"]] }, then: "Affiliate" },
|
|
1768
|
+
// Organic Search
|
|
1769
|
+
{
|
|
1770
|
+
case: {
|
|
1771
|
+
$and: [
|
|
1772
|
+
{ $regexMatch: { input: refStr, regex: SEARCH_ENGINES } },
|
|
1773
|
+
{ $not: [{ $in: [lMedium, PAID_MEDIUMS] }] }
|
|
1774
|
+
]
|
|
1775
|
+
},
|
|
1776
|
+
then: "Organic Search"
|
|
1777
|
+
},
|
|
1778
|
+
// Organic Social
|
|
1779
|
+
{
|
|
1780
|
+
case: {
|
|
1781
|
+
$and: [
|
|
1782
|
+
{ $or: [
|
|
1783
|
+
{ $regexMatch: { input: refStr, regex: SOCIAL_NETWORKS } },
|
|
1784
|
+
{ $in: [lSource, SOCIAL_SOURCES] }
|
|
1785
|
+
] },
|
|
1786
|
+
{ $not: [{ $in: [lMedium, PAID_MEDIUMS] }] }
|
|
1787
|
+
]
|
|
1788
|
+
},
|
|
1789
|
+
then: "Organic Social"
|
|
1790
|
+
},
|
|
1791
|
+
// Referral
|
|
1792
|
+
{
|
|
1793
|
+
case: { $and: [{ $ne: [refStr, ""] }, { $gt: [{ $strLenCP: refStr }, 0] }] },
|
|
1794
|
+
then: "Referral"
|
|
1795
|
+
},
|
|
1796
|
+
// Other (has UTM but no referrer)
|
|
1797
|
+
{
|
|
1798
|
+
case: {
|
|
1799
|
+
$or: [
|
|
1800
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_source", ""] }, ""] }] },
|
|
1801
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_medium", ""] }, ""] }] },
|
|
1802
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_campaign", ""] }, ""] }] }
|
|
1803
|
+
]
|
|
1804
|
+
},
|
|
1805
|
+
then: "Other"
|
|
1806
|
+
}
|
|
1807
|
+
],
|
|
1808
|
+
default: "Direct"
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1240
1812
|
function buildFilterMatch(filters) {
|
|
1241
1813
|
if (!filters) return {};
|
|
1242
1814
|
const map = {
|
|
@@ -1247,6 +1819,10 @@ function buildFilterMatch(filters) {
|
|
|
1247
1819
|
"device.type": "device_type",
|
|
1248
1820
|
"device.browser": "browser",
|
|
1249
1821
|
"device.os": "os",
|
|
1822
|
+
"device.osVersion": "os_version",
|
|
1823
|
+
"device.deviceModel": "device_model",
|
|
1824
|
+
"device.deviceBrand": "device_brand",
|
|
1825
|
+
"device.appVersion": "app_version",
|
|
1250
1826
|
"utm.source": "utm_source",
|
|
1251
1827
|
"utm.medium": "utm_medium",
|
|
1252
1828
|
"utm.campaign": "utm_campaign",
|
|
@@ -1262,7 +1838,9 @@ function buildFilterMatch(filters) {
|
|
|
1262
1838
|
};
|
|
1263
1839
|
const match = {};
|
|
1264
1840
|
for (const [key, value] of Object.entries(filters)) {
|
|
1265
|
-
if (!value
|
|
1841
|
+
if (!value) continue;
|
|
1842
|
+
if (key === "channel") continue;
|
|
1843
|
+
if (!map[key]) continue;
|
|
1266
1844
|
match[map[key]] = value;
|
|
1267
1845
|
}
|
|
1268
1846
|
return match;
|
|
@@ -1272,6 +1850,7 @@ var MongoDBAdapter = class {
|
|
|
1272
1850
|
db;
|
|
1273
1851
|
collection;
|
|
1274
1852
|
sites;
|
|
1853
|
+
identityMap;
|
|
1275
1854
|
constructor(url) {
|
|
1276
1855
|
this.client = new MongoClient(url);
|
|
1277
1856
|
}
|
|
@@ -1280,13 +1859,16 @@ var MongoDBAdapter = class {
|
|
|
1280
1859
|
this.db = this.client.db();
|
|
1281
1860
|
this.collection = this.db.collection(EVENTS_COLLECTION);
|
|
1282
1861
|
this.sites = this.db.collection(SITES_COLLECTION);
|
|
1862
|
+
this.identityMap = this.db.collection(IDENTITY_MAP_COLLECTION);
|
|
1283
1863
|
await Promise.all([
|
|
1284
1864
|
this.collection.createIndex({ site_id: 1, timestamp: -1 }),
|
|
1285
1865
|
this.collection.createIndex({ site_id: 1, type: 1 }),
|
|
1286
1866
|
this.collection.createIndex({ site_id: 1, visitor_id: 1 }),
|
|
1287
1867
|
this.collection.createIndex({ site_id: 1, session_id: 1 }),
|
|
1288
1868
|
this.sites.createIndex({ site_id: 1 }, { unique: true }),
|
|
1289
|
-
this.sites.createIndex({ secret_key: 1 })
|
|
1869
|
+
this.sites.createIndex({ secret_key: 1 }),
|
|
1870
|
+
this.identityMap.createIndex({ site_id: 1, visitor_id: 1 }, { unique: true }),
|
|
1871
|
+
this.identityMap.createIndex({ site_id: 1, user_id: 1 })
|
|
1290
1872
|
]);
|
|
1291
1873
|
}
|
|
1292
1874
|
async insertEvents(events) {
|
|
@@ -1327,6 +1909,13 @@ var MongoDBAdapter = class {
|
|
|
1327
1909
|
utm_term: e.utm?.term ?? null,
|
|
1328
1910
|
utm_content: e.utm?.content ?? null,
|
|
1329
1911
|
ip: e.ip ?? null,
|
|
1912
|
+
os_version: e.device?.osVersion ?? null,
|
|
1913
|
+
device_model: e.device?.deviceModel ?? null,
|
|
1914
|
+
device_brand: e.device?.deviceBrand ?? null,
|
|
1915
|
+
app_version: e.device?.appVersion ?? null,
|
|
1916
|
+
app_build: e.device?.appBuild ?? null,
|
|
1917
|
+
sdk_name: e.device?.sdkName ?? null,
|
|
1918
|
+
sdk_version: e.device?.sdkVersion ?? null,
|
|
1330
1919
|
created_at: /* @__PURE__ */ new Date()
|
|
1331
1920
|
}));
|
|
1332
1921
|
await this.collection.insertMany(docs);
|
|
@@ -1340,12 +1929,22 @@ var MongoDBAdapter = class {
|
|
|
1340
1929
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1341
1930
|
};
|
|
1342
1931
|
const filterMatch = buildFilterMatch(q.filters);
|
|
1932
|
+
const matchStages = (extra) => {
|
|
1933
|
+
const stages = [
|
|
1934
|
+
{ $match: { ...baseMatch, ...filterMatch, ...extra } }
|
|
1935
|
+
];
|
|
1936
|
+
if (q.filters?.channel) {
|
|
1937
|
+
stages.push({ $addFields: { _channel: channelClassificationSwitch() } });
|
|
1938
|
+
stages.push({ $match: { _channel: q.filters.channel } });
|
|
1939
|
+
}
|
|
1940
|
+
return stages;
|
|
1941
|
+
};
|
|
1343
1942
|
let data = [];
|
|
1344
1943
|
let total = 0;
|
|
1345
1944
|
switch (q.metric) {
|
|
1346
1945
|
case "pageviews": {
|
|
1347
1946
|
const [result2] = await this.collection.aggregate([
|
|
1348
|
-
{
|
|
1947
|
+
...matchStages({ type: "pageview" }),
|
|
1349
1948
|
{ $count: "count" }
|
|
1350
1949
|
]).toArray();
|
|
1351
1950
|
total = result2?.count ?? 0;
|
|
@@ -1354,7 +1953,7 @@ var MongoDBAdapter = class {
|
|
|
1354
1953
|
}
|
|
1355
1954
|
case "visitors": {
|
|
1356
1955
|
const [result2] = await this.collection.aggregate([
|
|
1357
|
-
|
|
1956
|
+
...matchStages(),
|
|
1358
1957
|
{ $group: { _id: "$visitor_id" } },
|
|
1359
1958
|
{ $count: "count" }
|
|
1360
1959
|
]).toArray();
|
|
@@ -1364,7 +1963,7 @@ var MongoDBAdapter = class {
|
|
|
1364
1963
|
}
|
|
1365
1964
|
case "sessions": {
|
|
1366
1965
|
const [result2] = await this.collection.aggregate([
|
|
1367
|
-
|
|
1966
|
+
...matchStages(),
|
|
1368
1967
|
{ $group: { _id: "$session_id" } },
|
|
1369
1968
|
{ $count: "count" }
|
|
1370
1969
|
]).toArray();
|
|
@@ -1374,7 +1973,7 @@ var MongoDBAdapter = class {
|
|
|
1374
1973
|
}
|
|
1375
1974
|
case "events": {
|
|
1376
1975
|
const [result2] = await this.collection.aggregate([
|
|
1377
|
-
{
|
|
1976
|
+
...matchStages({ type: "event" }),
|
|
1378
1977
|
{ $count: "count" }
|
|
1379
1978
|
]).toArray();
|
|
1380
1979
|
total = result2?.count ?? 0;
|
|
@@ -1389,7 +1988,7 @@ var MongoDBAdapter = class {
|
|
|
1389
1988
|
break;
|
|
1390
1989
|
}
|
|
1391
1990
|
const [result2] = await this.collection.aggregate([
|
|
1392
|
-
{
|
|
1991
|
+
...matchStages({ type: "event", event_name: { $in: conversionEvents } }),
|
|
1393
1992
|
{ $count: "count" }
|
|
1394
1993
|
]).toArray();
|
|
1395
1994
|
total = result2?.count ?? 0;
|
|
@@ -1398,7 +1997,7 @@ var MongoDBAdapter = class {
|
|
|
1398
1997
|
}
|
|
1399
1998
|
case "top_pages": {
|
|
1400
1999
|
const rows = await this.collection.aggregate([
|
|
1401
|
-
{
|
|
2000
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1402
2001
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1403
2002
|
{ $sort: { value: -1 } },
|
|
1404
2003
|
{ $limit: limit }
|
|
@@ -1409,7 +2008,7 @@ var MongoDBAdapter = class {
|
|
|
1409
2008
|
}
|
|
1410
2009
|
case "top_referrers": {
|
|
1411
2010
|
const rows = await this.collection.aggregate([
|
|
1412
|
-
{
|
|
2011
|
+
...matchStages({ type: "pageview", referrer: { $nin: [null, ""] } }),
|
|
1413
2012
|
{ $group: { _id: "$referrer", value: { $sum: 1 } } },
|
|
1414
2013
|
{ $sort: { value: -1 } },
|
|
1415
2014
|
{ $limit: limit }
|
|
@@ -1420,7 +2019,7 @@ var MongoDBAdapter = class {
|
|
|
1420
2019
|
}
|
|
1421
2020
|
case "top_countries": {
|
|
1422
2021
|
const rows = await this.collection.aggregate([
|
|
1423
|
-
{
|
|
2022
|
+
...matchStages({ country: { $ne: null } }),
|
|
1424
2023
|
{ $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
|
|
1425
2024
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1426
2025
|
{ $sort: { value: -1 } },
|
|
@@ -1432,7 +2031,7 @@ var MongoDBAdapter = class {
|
|
|
1432
2031
|
}
|
|
1433
2032
|
case "top_cities": {
|
|
1434
2033
|
const rows = await this.collection.aggregate([
|
|
1435
|
-
{
|
|
2034
|
+
...matchStages({ city: { $ne: null } }),
|
|
1436
2035
|
{ $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
|
|
1437
2036
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1438
2037
|
{ $sort: { value: -1 } },
|
|
@@ -1444,7 +2043,7 @@ var MongoDBAdapter = class {
|
|
|
1444
2043
|
}
|
|
1445
2044
|
case "top_events": {
|
|
1446
2045
|
const rows = await this.collection.aggregate([
|
|
1447
|
-
{
|
|
2046
|
+
...matchStages({ type: "event", event_name: { $ne: null } }),
|
|
1448
2047
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1449
2048
|
{ $sort: { value: -1 } },
|
|
1450
2049
|
{ $limit: limit }
|
|
@@ -1461,7 +2060,7 @@ var MongoDBAdapter = class {
|
|
|
1461
2060
|
break;
|
|
1462
2061
|
}
|
|
1463
2062
|
const rows = await this.collection.aggregate([
|
|
1464
|
-
{
|
|
2063
|
+
...matchStages({ type: "event", event_name: { $in: conversionEvents } }),
|
|
1465
2064
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1466
2065
|
{ $sort: { value: -1 } },
|
|
1467
2066
|
{ $limit: limit }
|
|
@@ -1472,7 +2071,7 @@ var MongoDBAdapter = class {
|
|
|
1472
2071
|
}
|
|
1473
2072
|
case "top_exit_pages": {
|
|
1474
2073
|
const rows = await this.collection.aggregate([
|
|
1475
|
-
{
|
|
2074
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1476
2075
|
{ $sort: { timestamp: 1 } },
|
|
1477
2076
|
{ $group: { _id: "$session_id", url: { $last: "$url" } } },
|
|
1478
2077
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
@@ -1485,7 +2084,7 @@ var MongoDBAdapter = class {
|
|
|
1485
2084
|
}
|
|
1486
2085
|
case "top_transitions": {
|
|
1487
2086
|
const rows = await this.collection.aggregate([
|
|
1488
|
-
{
|
|
2087
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1489
2088
|
{
|
|
1490
2089
|
$setWindowFields: {
|
|
1491
2090
|
partitionBy: "$session_id",
|
|
@@ -1506,7 +2105,7 @@ var MongoDBAdapter = class {
|
|
|
1506
2105
|
}
|
|
1507
2106
|
case "top_scroll_pages": {
|
|
1508
2107
|
const rows = await this.collection.aggregate([
|
|
1509
|
-
{
|
|
2108
|
+
...matchStages({ type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } }),
|
|
1510
2109
|
{ $group: { _id: "$page_path", value: { $sum: 1 } } },
|
|
1511
2110
|
{ $sort: { value: -1 } },
|
|
1512
2111
|
{ $limit: limit }
|
|
@@ -1517,15 +2116,11 @@ var MongoDBAdapter = class {
|
|
|
1517
2116
|
}
|
|
1518
2117
|
case "top_button_clicks": {
|
|
1519
2118
|
const rows = await this.collection.aggregate([
|
|
1520
|
-
{
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
event_subtype: "button_click",
|
|
1526
|
-
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
1527
|
-
}
|
|
1528
|
-
},
|
|
2119
|
+
...matchStages({
|
|
2120
|
+
type: "event",
|
|
2121
|
+
event_subtype: "button_click",
|
|
2122
|
+
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
2123
|
+
}),
|
|
1529
2124
|
{ $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
|
|
1530
2125
|
{ $sort: { value: -1 } },
|
|
1531
2126
|
{ $limit: limit }
|
|
@@ -1536,15 +2131,11 @@ var MongoDBAdapter = class {
|
|
|
1536
2131
|
}
|
|
1537
2132
|
case "top_link_targets": {
|
|
1538
2133
|
const rows = await this.collection.aggregate([
|
|
1539
|
-
{
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
1545
|
-
target_url_path: { $ne: null }
|
|
1546
|
-
}
|
|
1547
|
-
},
|
|
2134
|
+
...matchStages({
|
|
2135
|
+
type: "event",
|
|
2136
|
+
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
2137
|
+
target_url_path: { $ne: null }
|
|
2138
|
+
}),
|
|
1548
2139
|
{ $group: { _id: "$target_url_path", value: { $sum: 1 } } },
|
|
1549
2140
|
{ $sort: { value: -1 } },
|
|
1550
2141
|
{ $limit: limit }
|
|
@@ -1555,7 +2146,7 @@ var MongoDBAdapter = class {
|
|
|
1555
2146
|
}
|
|
1556
2147
|
case "top_devices": {
|
|
1557
2148
|
const rows = await this.collection.aggregate([
|
|
1558
|
-
{
|
|
2149
|
+
...matchStages({ device_type: { $ne: null } }),
|
|
1559
2150
|
{ $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
|
|
1560
2151
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1561
2152
|
{ $sort: { value: -1 } },
|
|
@@ -1567,7 +2158,7 @@ var MongoDBAdapter = class {
|
|
|
1567
2158
|
}
|
|
1568
2159
|
case "top_browsers": {
|
|
1569
2160
|
const rows = await this.collection.aggregate([
|
|
1570
|
-
{
|
|
2161
|
+
...matchStages({ browser: { $ne: null } }),
|
|
1571
2162
|
{ $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
|
|
1572
2163
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1573
2164
|
{ $sort: { value: -1 } },
|
|
@@ -1579,7 +2170,7 @@ var MongoDBAdapter = class {
|
|
|
1579
2170
|
}
|
|
1580
2171
|
case "top_os": {
|
|
1581
2172
|
const rows = await this.collection.aggregate([
|
|
1582
|
-
{
|
|
2173
|
+
...matchStages({ os: { $ne: null } }),
|
|
1583
2174
|
{ $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
|
|
1584
2175
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1585
2176
|
{ $sort: { value: -1 } },
|
|
@@ -1589,6 +2180,117 @@ var MongoDBAdapter = class {
|
|
|
1589
2180
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1590
2181
|
break;
|
|
1591
2182
|
}
|
|
2183
|
+
case "top_os_versions": {
|
|
2184
|
+
const rows = await this.collection.aggregate([
|
|
2185
|
+
...matchStages({ os: { $ne: null }, os_version: { $ne: null } }),
|
|
2186
|
+
{ $group: { _id: { $concat: ["$os", " ", "$os_version"] }, value: { $addToSet: "$visitor_id" } } },
|
|
2187
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2188
|
+
{ $sort: { value: -1 } },
|
|
2189
|
+
{ $limit: limit }
|
|
2190
|
+
]).toArray();
|
|
2191
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2192
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2193
|
+
break;
|
|
2194
|
+
}
|
|
2195
|
+
case "top_device_models": {
|
|
2196
|
+
const rows = await this.collection.aggregate([
|
|
2197
|
+
...matchStages({ device_model: { $ne: null } }),
|
|
2198
|
+
{ $group: { _id: { $trim: { input: { $concat: [{ $ifNull: ["$device_brand", ""] }, " ", "$device_model"] } } }, value: { $addToSet: "$visitor_id" } } },
|
|
2199
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2200
|
+
{ $sort: { value: -1 } },
|
|
2201
|
+
{ $limit: limit }
|
|
2202
|
+
]).toArray();
|
|
2203
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2204
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2205
|
+
break;
|
|
2206
|
+
}
|
|
2207
|
+
case "top_app_versions": {
|
|
2208
|
+
const rows = await this.collection.aggregate([
|
|
2209
|
+
...matchStages({ app_version: { $ne: null } }),
|
|
2210
|
+
{ $group: { _id: "$app_version", value: { $addToSet: "$visitor_id" } } },
|
|
2211
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2212
|
+
{ $sort: { value: -1 } },
|
|
2213
|
+
{ $limit: limit }
|
|
2214
|
+
]).toArray();
|
|
2215
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2216
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2217
|
+
break;
|
|
2218
|
+
}
|
|
2219
|
+
case "top_utm_sources": {
|
|
2220
|
+
const rows = await this.collection.aggregate([
|
|
2221
|
+
...matchStages({ utm_source: { $nin: [null, ""] } }),
|
|
2222
|
+
{ $addFields: { _normalized_source: normalizedUtmSourceSwitch() } },
|
|
2223
|
+
{ $group: { _id: "$_normalized_source", value: { $addToSet: "$visitor_id" } } },
|
|
2224
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2225
|
+
{ $sort: { value: -1 } },
|
|
2226
|
+
{ $limit: limit }
|
|
2227
|
+
]).toArray();
|
|
2228
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2229
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
case "top_utm_mediums": {
|
|
2233
|
+
const rows = await this.collection.aggregate([
|
|
2234
|
+
...matchStages({ utm_medium: { $nin: [null, ""] } }),
|
|
2235
|
+
{ $addFields: { _normalized_medium: normalizedUtmMediumSwitch() } },
|
|
2236
|
+
{ $group: { _id: "$_normalized_medium", value: { $addToSet: "$visitor_id" } } },
|
|
2237
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2238
|
+
{ $sort: { value: -1 } },
|
|
2239
|
+
{ $limit: limit }
|
|
2240
|
+
]).toArray();
|
|
2241
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2242
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2243
|
+
break;
|
|
2244
|
+
}
|
|
2245
|
+
case "top_utm_campaigns": {
|
|
2246
|
+
const rows = await this.collection.aggregate([
|
|
2247
|
+
...matchStages({ utm_campaign: { $nin: [null, ""] } }),
|
|
2248
|
+
{ $group: { _id: "$utm_campaign", value: { $addToSet: "$visitor_id" } } },
|
|
2249
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2250
|
+
{ $sort: { value: -1 } },
|
|
2251
|
+
{ $limit: limit }
|
|
2252
|
+
]).toArray();
|
|
2253
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2254
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2255
|
+
break;
|
|
2256
|
+
}
|
|
2257
|
+
case "top_utm_terms": {
|
|
2258
|
+
const rows = await this.collection.aggregate([
|
|
2259
|
+
...matchStages({ utm_term: { $nin: [null, ""] } }),
|
|
2260
|
+
{ $group: { _id: "$utm_term", value: { $addToSet: "$visitor_id" } } },
|
|
2261
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2262
|
+
{ $sort: { value: -1 } },
|
|
2263
|
+
{ $limit: limit }
|
|
2264
|
+
]).toArray();
|
|
2265
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2266
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
case "top_utm_contents": {
|
|
2270
|
+
const rows = await this.collection.aggregate([
|
|
2271
|
+
...matchStages({ utm_content: { $nin: [null, ""] } }),
|
|
2272
|
+
{ $group: { _id: "$utm_content", value: { $addToSet: "$visitor_id" } } },
|
|
2273
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2274
|
+
{ $sort: { value: -1 } },
|
|
2275
|
+
{ $limit: limit }
|
|
2276
|
+
]).toArray();
|
|
2277
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2278
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2279
|
+
break;
|
|
2280
|
+
}
|
|
2281
|
+
case "top_channels": {
|
|
2282
|
+
const rows = await this.collection.aggregate([
|
|
2283
|
+
...matchStages(),
|
|
2284
|
+
{ $addFields: { _channel: channelClassificationSwitch() } },
|
|
2285
|
+
{ $group: { _id: "$_channel", value: { $addToSet: "$visitor_id" } } },
|
|
2286
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2287
|
+
{ $sort: { value: -1 } },
|
|
2288
|
+
{ $limit: limit }
|
|
2289
|
+
]).toArray();
|
|
2290
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2291
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
1592
2294
|
}
|
|
1593
2295
|
const result = { metric: q.metric, period, data, total };
|
|
1594
2296
|
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
@@ -1777,24 +2479,72 @@ var MongoDBAdapter = class {
|
|
|
1777
2479
|
offset
|
|
1778
2480
|
};
|
|
1779
2481
|
}
|
|
2482
|
+
// ─── Identity Mapping ──────────────────────────────────────
|
|
2483
|
+
async upsertIdentity(siteId, visitorId, userId) {
|
|
2484
|
+
await this.identityMap.updateOne(
|
|
2485
|
+
{ site_id: siteId, visitor_id: visitorId },
|
|
2486
|
+
{
|
|
2487
|
+
$set: { user_id: userId, identified_at: /* @__PURE__ */ new Date() },
|
|
2488
|
+
$setOnInsert: { site_id: siteId, visitor_id: visitorId, created_at: /* @__PURE__ */ new Date() }
|
|
2489
|
+
},
|
|
2490
|
+
{ upsert: true }
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
async getVisitorIdsForUser(siteId, userId) {
|
|
2494
|
+
const docs = await this.identityMap.find({ site_id: siteId, user_id: userId }).toArray();
|
|
2495
|
+
return docs.map((d) => d.visitor_id);
|
|
2496
|
+
}
|
|
2497
|
+
async getUserIdForVisitor(siteId, visitorId) {
|
|
2498
|
+
const doc = await this.identityMap.findOne({ site_id: siteId, visitor_id: visitorId });
|
|
2499
|
+
return doc?.user_id ?? null;
|
|
2500
|
+
}
|
|
1780
2501
|
// ─── User Listing ──────────────────────────────────────
|
|
1781
2502
|
async listUsers(params) {
|
|
1782
2503
|
const limit = Math.min(params.limit ?? 50, 200);
|
|
1783
2504
|
const offset = params.offset ?? 0;
|
|
1784
2505
|
const match = { site_id: params.siteId };
|
|
1785
|
-
if (params.search) {
|
|
1786
|
-
match.$or = [
|
|
1787
|
-
{ visitor_id: { $regex: params.search, $options: "i" } },
|
|
1788
|
-
{ user_id: { $regex: params.search, $options: "i" } }
|
|
1789
|
-
];
|
|
1790
|
-
}
|
|
1791
2506
|
const pipeline = [
|
|
1792
2507
|
{ $match: match },
|
|
2508
|
+
// Join with identity map to resolve visitor → user
|
|
2509
|
+
{
|
|
2510
|
+
$lookup: {
|
|
2511
|
+
from: IDENTITY_MAP_COLLECTION,
|
|
2512
|
+
let: { vid: "$visitor_id", sid: "$site_id" },
|
|
2513
|
+
pipeline: [
|
|
2514
|
+
{ $match: { $expr: { $and: [{ $eq: ["$visitor_id", "$$vid"] }, { $eq: ["$site_id", "$$sid"] }] } } }
|
|
2515
|
+
],
|
|
2516
|
+
as: "_identity"
|
|
2517
|
+
}
|
|
2518
|
+
},
|
|
2519
|
+
{
|
|
2520
|
+
$addFields: {
|
|
2521
|
+
_resolved_id: {
|
|
2522
|
+
$ifNull: [{ $arrayElemAt: ["$_identity.user_id", 0] }, "$visitor_id"]
|
|
2523
|
+
},
|
|
2524
|
+
_resolved_user_id: {
|
|
2525
|
+
$arrayElemAt: ["$_identity.user_id", 0]
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
];
|
|
2530
|
+
if (params.search) {
|
|
2531
|
+
pipeline.push({
|
|
2532
|
+
$match: {
|
|
2533
|
+
$or: [
|
|
2534
|
+
{ visitor_id: { $regex: params.search, $options: "i" } },
|
|
2535
|
+
{ user_id: { $regex: params.search, $options: "i" } },
|
|
2536
|
+
{ _resolved_user_id: { $regex: params.search, $options: "i" } }
|
|
2537
|
+
]
|
|
2538
|
+
}
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
pipeline.push(
|
|
1793
2542
|
{ $sort: { timestamp: 1 } },
|
|
1794
2543
|
{
|
|
1795
2544
|
$group: {
|
|
1796
|
-
_id: "$
|
|
1797
|
-
|
|
2545
|
+
_id: "$_resolved_id",
|
|
2546
|
+
visitorIds: { $addToSet: "$visitor_id" },
|
|
2547
|
+
userId: { $last: { $ifNull: ["$_resolved_user_id", "$user_id"] } },
|
|
1798
2548
|
traits: { $last: "$traits" },
|
|
1799
2549
|
firstSeen: { $min: "$timestamp" },
|
|
1800
2550
|
lastSeen: { $max: "$timestamp" },
|
|
@@ -1827,10 +2577,11 @@ var MongoDBAdapter = class {
|
|
|
1827
2577
|
count: [{ $count: "total" }]
|
|
1828
2578
|
}
|
|
1829
2579
|
}
|
|
1830
|
-
|
|
2580
|
+
);
|
|
1831
2581
|
const [result] = await this.collection.aggregate(pipeline).toArray();
|
|
1832
2582
|
const users = (result?.data ?? []).map((u) => ({
|
|
1833
|
-
visitorId: u._id,
|
|
2583
|
+
visitorId: u.visitorIds[0] ?? u._id,
|
|
2584
|
+
visitorIds: u.visitorIds.length > 1 ? u.visitorIds : void 0,
|
|
1834
2585
|
userId: u.userId ?? void 0,
|
|
1835
2586
|
traits: u.traits ?? void 0,
|
|
1836
2587
|
firstSeen: u.firstSeen.toISOString(),
|
|
@@ -1860,13 +2611,125 @@ var MongoDBAdapter = class {
|
|
|
1860
2611
|
offset
|
|
1861
2612
|
};
|
|
1862
2613
|
}
|
|
1863
|
-
async getUserDetail(siteId,
|
|
1864
|
-
const
|
|
1865
|
-
|
|
1866
|
-
|
|
2614
|
+
async getUserDetail(siteId, identifier) {
|
|
2615
|
+
const visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
2616
|
+
if (visitorIds.length > 0) {
|
|
2617
|
+
return this.getMergedUserDetail(siteId, identifier, visitorIds);
|
|
2618
|
+
}
|
|
2619
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
2620
|
+
if (userId) {
|
|
2621
|
+
const allVisitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
2622
|
+
return this.getMergedUserDetail(siteId, userId, allVisitorIds);
|
|
2623
|
+
}
|
|
2624
|
+
return this.getMergedUserDetail(siteId, void 0, [identifier]);
|
|
2625
|
+
}
|
|
2626
|
+
async getUserEvents(siteId, identifier, params) {
|
|
2627
|
+
let visitorIds = await this.getVisitorIdsForUser(siteId, identifier);
|
|
2628
|
+
if (visitorIds.length === 0) {
|
|
2629
|
+
const userId = await this.getUserIdForVisitor(siteId, identifier);
|
|
2630
|
+
if (userId) {
|
|
2631
|
+
visitorIds = await this.getVisitorIdsForUser(siteId, userId);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (visitorIds.length === 0) {
|
|
2635
|
+
visitorIds = [identifier];
|
|
2636
|
+
}
|
|
2637
|
+
return this.listEventsForVisitorIds(siteId, visitorIds, params);
|
|
2638
|
+
}
|
|
2639
|
+
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
2640
|
+
const pipeline = [
|
|
2641
|
+
{ $match: { site_id: siteId, visitor_id: { $in: visitorIds } } },
|
|
2642
|
+
{ $sort: { timestamp: 1 } },
|
|
2643
|
+
{
|
|
2644
|
+
$group: {
|
|
2645
|
+
_id: null,
|
|
2646
|
+
visitorIds: { $addToSet: "$visitor_id" },
|
|
2647
|
+
traits: { $last: "$traits" },
|
|
2648
|
+
firstSeen: { $min: "$timestamp" },
|
|
2649
|
+
lastSeen: { $max: "$timestamp" },
|
|
2650
|
+
totalEvents: { $sum: 1 },
|
|
2651
|
+
totalPageviews: { $sum: { $cond: [{ $eq: ["$type", "pageview"] }, 1, 0] } },
|
|
2652
|
+
sessions: { $addToSet: "$session_id" },
|
|
2653
|
+
lastUrl: { $last: "$url" },
|
|
2654
|
+
referrer: { $last: "$referrer" },
|
|
2655
|
+
device_type: { $last: "$device_type" },
|
|
2656
|
+
browser: { $last: "$browser" },
|
|
2657
|
+
os: { $last: "$os" },
|
|
2658
|
+
country: { $last: "$country" },
|
|
2659
|
+
city: { $last: "$city" },
|
|
2660
|
+
region: { $last: "$region" },
|
|
2661
|
+
language: { $last: "$language" },
|
|
2662
|
+
timezone: { $last: "$timezone" },
|
|
2663
|
+
screen_width: { $last: "$screen_width" },
|
|
2664
|
+
screen_height: { $last: "$screen_height" },
|
|
2665
|
+
utm_source: { $last: "$utm_source" },
|
|
2666
|
+
utm_medium: { $last: "$utm_medium" },
|
|
2667
|
+
utm_campaign: { $last: "$utm_campaign" },
|
|
2668
|
+
utm_term: { $last: "$utm_term" },
|
|
2669
|
+
utm_content: { $last: "$utm_content" }
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
];
|
|
2673
|
+
const [row] = await this.collection.aggregate(pipeline).toArray();
|
|
2674
|
+
if (!row) return null;
|
|
2675
|
+
return {
|
|
2676
|
+
visitorId: visitorIds[0],
|
|
2677
|
+
visitorIds: row.visitorIds.length > 1 ? row.visitorIds : void 0,
|
|
2678
|
+
userId: userId ?? void 0,
|
|
2679
|
+
traits: row.traits ?? void 0,
|
|
2680
|
+
firstSeen: row.firstSeen.toISOString(),
|
|
2681
|
+
lastSeen: row.lastSeen.toISOString(),
|
|
2682
|
+
totalEvents: row.totalEvents,
|
|
2683
|
+
totalPageviews: row.totalPageviews,
|
|
2684
|
+
totalSessions: row.sessions.length,
|
|
2685
|
+
lastUrl: row.lastUrl ?? void 0,
|
|
2686
|
+
referrer: row.referrer ?? void 0,
|
|
2687
|
+
device: row.device_type ? { type: row.device_type, browser: row.browser ?? "", os: row.os ?? "" } : void 0,
|
|
2688
|
+
geo: row.country ? { country: row.country, city: row.city ?? void 0, region: row.region ?? void 0 } : void 0,
|
|
2689
|
+
language: row.language ?? void 0,
|
|
2690
|
+
timezone: row.timezone ?? void 0,
|
|
2691
|
+
screen: row.screen_width || row.screen_height ? { width: row.screen_width ?? 0, height: row.screen_height ?? 0 } : void 0,
|
|
2692
|
+
utm: row.utm_source ? {
|
|
2693
|
+
source: row.utm_source ?? void 0,
|
|
2694
|
+
medium: row.utm_medium ?? void 0,
|
|
2695
|
+
campaign: row.utm_campaign ?? void 0,
|
|
2696
|
+
term: row.utm_term ?? void 0,
|
|
2697
|
+
content: row.utm_content ?? void 0
|
|
2698
|
+
} : void 0
|
|
2699
|
+
};
|
|
1867
2700
|
}
|
|
1868
|
-
async
|
|
1869
|
-
|
|
2701
|
+
async listEventsForVisitorIds(siteId, visitorIds, params) {
|
|
2702
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
2703
|
+
const offset = params.offset ?? 0;
|
|
2704
|
+
const match = {
|
|
2705
|
+
site_id: siteId,
|
|
2706
|
+
visitor_id: { $in: visitorIds }
|
|
2707
|
+
};
|
|
2708
|
+
if (params.type) match.type = params.type;
|
|
2709
|
+
if (params.eventName) {
|
|
2710
|
+
match.event_name = params.eventName;
|
|
2711
|
+
} else if (params.eventNames && params.eventNames.length > 0) {
|
|
2712
|
+
match.event_name = { $in: params.eventNames };
|
|
2713
|
+
}
|
|
2714
|
+
if (params.eventSource) match.event_source = params.eventSource;
|
|
2715
|
+
if (params.period || params.dateFrom) {
|
|
2716
|
+
const { dateRange } = resolvePeriod({
|
|
2717
|
+
period: params.period,
|
|
2718
|
+
dateFrom: params.dateFrom,
|
|
2719
|
+
dateTo: params.dateTo
|
|
2720
|
+
});
|
|
2721
|
+
match.timestamp = { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) };
|
|
2722
|
+
}
|
|
2723
|
+
const [events, countResult] = await Promise.all([
|
|
2724
|
+
this.collection.find(match).sort({ timestamp: -1 }).skip(offset).limit(limit).toArray(),
|
|
2725
|
+
this.collection.countDocuments(match)
|
|
2726
|
+
]);
|
|
2727
|
+
return {
|
|
2728
|
+
events: events.map((e) => this.toEventListItem(e)),
|
|
2729
|
+
total: countResult,
|
|
2730
|
+
limit,
|
|
2731
|
+
offset
|
|
2732
|
+
};
|
|
1870
2733
|
}
|
|
1871
2734
|
toEventListItem(doc) {
|
|
1872
2735
|
return {
|
|
@@ -1890,7 +2753,18 @@ var MongoDBAdapter = class {
|
|
|
1890
2753
|
userId: doc.user_id ?? void 0,
|
|
1891
2754
|
traits: doc.traits ?? void 0,
|
|
1892
2755
|
geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
|
|
1893
|
-
device: doc.device_type ? {
|
|
2756
|
+
device: doc.device_type ? {
|
|
2757
|
+
type: doc.device_type,
|
|
2758
|
+
browser: doc.browser ?? "",
|
|
2759
|
+
os: doc.os ?? "",
|
|
2760
|
+
osVersion: doc.os_version ?? void 0,
|
|
2761
|
+
deviceModel: doc.device_model ?? void 0,
|
|
2762
|
+
deviceBrand: doc.device_brand ?? void 0,
|
|
2763
|
+
appVersion: doc.app_version ?? void 0,
|
|
2764
|
+
appBuild: doc.app_build ?? void 0,
|
|
2765
|
+
sdkName: doc.sdk_name ?? void 0,
|
|
2766
|
+
sdkVersion: doc.sdk_version ?? void 0
|
|
2767
|
+
} : void 0,
|
|
1894
2768
|
language: doc.language ?? void 0,
|
|
1895
2769
|
utm: doc.utm_source ? {
|
|
1896
2770
|
source: doc.utm_source ?? void 0,
|
|
@@ -1908,6 +2782,7 @@ var MongoDBAdapter = class {
|
|
|
1908
2782
|
site_id: generateSiteId(),
|
|
1909
2783
|
secret_key: generateSecretKey(),
|
|
1910
2784
|
name: data.name,
|
|
2785
|
+
type: data.type ?? "web",
|
|
1911
2786
|
domain: data.domain ?? null,
|
|
1912
2787
|
allowed_origins: data.allowedOrigins ?? null,
|
|
1913
2788
|
conversion_events: data.conversionEvents ?? null,
|
|
@@ -1932,6 +2807,7 @@ var MongoDBAdapter = class {
|
|
|
1932
2807
|
async updateSite(siteId, data) {
|
|
1933
2808
|
const updates = { updated_at: /* @__PURE__ */ new Date() };
|
|
1934
2809
|
if (data.name !== void 0) updates.name = data.name;
|
|
2810
|
+
if (data.type !== void 0) updates.type = data.type;
|
|
1935
2811
|
if (data.domain !== void 0) updates.domain = data.domain || null;
|
|
1936
2812
|
if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
|
|
1937
2813
|
if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
|
|
@@ -1963,6 +2839,7 @@ var MongoDBAdapter = class {
|
|
|
1963
2839
|
siteId: doc.site_id,
|
|
1964
2840
|
secretKey: doc.secret_key,
|
|
1965
2841
|
name: doc.name,
|
|
2842
|
+
type: doc.type ?? "web",
|
|
1966
2843
|
domain: doc.domain ?? void 0,
|
|
1967
2844
|
allowedOrigins: doc.allowed_origins ?? void 0,
|
|
1968
2845
|
conversionEvents: doc.conversion_events ?? void 0,
|
|
@@ -2231,12 +3108,74 @@ async function createCollector(config) {
|
|
|
2231
3108
|
return false;
|
|
2232
3109
|
}
|
|
2233
3110
|
function enrichEvents(events, ip, userAgent) {
|
|
2234
|
-
const
|
|
3111
|
+
const uaDevice = parseUserAgent(userAgent);
|
|
2235
3112
|
return events.map((event) => {
|
|
2236
3113
|
const geo = resolveGeo(ip, event.timezone);
|
|
3114
|
+
let device;
|
|
3115
|
+
if (event.mobile?.platform) {
|
|
3116
|
+
device = {
|
|
3117
|
+
type: "mobile",
|
|
3118
|
+
browser: "App",
|
|
3119
|
+
os: event.mobile.platform === "ios" ? "iOS" : "Android",
|
|
3120
|
+
osVersion: event.mobile.osVersion,
|
|
3121
|
+
deviceModel: event.mobile.deviceModel,
|
|
3122
|
+
deviceBrand: event.mobile.deviceBrand,
|
|
3123
|
+
appVersion: event.mobile.appVersion,
|
|
3124
|
+
appBuild: event.mobile.appBuild,
|
|
3125
|
+
sdkName: event.mobile.sdkName,
|
|
3126
|
+
sdkVersion: event.mobile.sdkVersion
|
|
3127
|
+
};
|
|
3128
|
+
} else {
|
|
3129
|
+
device = uaDevice;
|
|
3130
|
+
}
|
|
2237
3131
|
return { ...event, ip, geo, device };
|
|
2238
3132
|
});
|
|
2239
3133
|
}
|
|
3134
|
+
const identityCache = /* @__PURE__ */ new Map();
|
|
3135
|
+
const IDENTITY_CACHE_TTL = 5 * 60 * 1e3;
|
|
3136
|
+
function getCachedUserId(siteId, visitorId) {
|
|
3137
|
+
const key = `${siteId}:${visitorId}`;
|
|
3138
|
+
const entry = identityCache.get(key);
|
|
3139
|
+
if (!entry) return void 0;
|
|
3140
|
+
if (Date.now() > entry.expires) {
|
|
3141
|
+
identityCache.delete(key);
|
|
3142
|
+
return void 0;
|
|
3143
|
+
}
|
|
3144
|
+
return entry.userId;
|
|
3145
|
+
}
|
|
3146
|
+
function setCachedUserId(siteId, visitorId, userId) {
|
|
3147
|
+
const key = `${siteId}:${visitorId}`;
|
|
3148
|
+
identityCache.set(key, { userId, expires: Date.now() + IDENTITY_CACHE_TTL });
|
|
3149
|
+
if (identityCache.size > 1e4) {
|
|
3150
|
+
const now = Date.now();
|
|
3151
|
+
for (const [k, v] of identityCache) {
|
|
3152
|
+
if (now > v.expires) identityCache.delete(k);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
async function processIdentity(events) {
|
|
3157
|
+
for (const event of events) {
|
|
3158
|
+
if (!event.visitorId || event.visitorId === "server") continue;
|
|
3159
|
+
if (event.type === "identify" && event.userId) {
|
|
3160
|
+
await db.upsertIdentity(event.siteId, event.visitorId, event.userId);
|
|
3161
|
+
setCachedUserId(event.siteId, event.visitorId, event.userId);
|
|
3162
|
+
} else if (!event.userId) {
|
|
3163
|
+
const cached = getCachedUserId(event.siteId, event.visitorId);
|
|
3164
|
+
if (cached) {
|
|
3165
|
+
event.userId = cached;
|
|
3166
|
+
} else {
|
|
3167
|
+
const resolved = await db.getUserIdForVisitor(event.siteId, event.visitorId);
|
|
3168
|
+
if (resolved) {
|
|
3169
|
+
event.userId = resolved;
|
|
3170
|
+
setCachedUserId(event.siteId, event.visitorId, resolved);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
} else if (event.userId) {
|
|
3174
|
+
setCachedUserId(event.siteId, event.visitorId, event.userId);
|
|
3175
|
+
await db.upsertIdentity(event.siteId, event.visitorId, event.userId);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
2240
3179
|
function extractIp(req) {
|
|
2241
3180
|
if (config.trustProxy ?? true) {
|
|
2242
3181
|
const forwarded = req.headers?.["x-forwarded-for"];
|
|
@@ -2248,6 +3187,22 @@ async function createCollector(config) {
|
|
|
2248
3187
|
}
|
|
2249
3188
|
return req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || "";
|
|
2250
3189
|
}
|
|
3190
|
+
function extractRequestHostname(req) {
|
|
3191
|
+
const headerValue = (value) => {
|
|
3192
|
+
if (Array.isArray(value)) return value[0];
|
|
3193
|
+
if (typeof value === "string") return value;
|
|
3194
|
+
return void 0;
|
|
3195
|
+
};
|
|
3196
|
+
const origin = headerValue(req.headers?.origin);
|
|
3197
|
+
const referer = headerValue(req.headers?.referer) || headerValue(req.headers?.referrer);
|
|
3198
|
+
const raw = origin ?? referer;
|
|
3199
|
+
if (!raw || raw === "null") return void 0;
|
|
3200
|
+
try {
|
|
3201
|
+
return new URL(raw).hostname.toLowerCase();
|
|
3202
|
+
} catch {
|
|
3203
|
+
return void 0;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
2251
3206
|
function handler() {
|
|
2252
3207
|
return async (req, res) => {
|
|
2253
3208
|
res.setHeader?.("Access-Control-Allow-Origin", "*");
|
|
@@ -2273,6 +3228,12 @@ async function createCollector(config) {
|
|
|
2273
3228
|
sendJson(res, 400, { ok: false, error: "Too many events (max 100)" });
|
|
2274
3229
|
return;
|
|
2275
3230
|
}
|
|
3231
|
+
const siteIds = new Set(payload.events.map((event) => event.siteId).filter(Boolean));
|
|
3232
|
+
if (siteIds.size !== 1) {
|
|
3233
|
+
sendJson(res, 200, { ok: true });
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
const siteId = Array.from(siteIds)[0];
|
|
2276
3237
|
const userAgent = req.headers?.["user-agent"] || "";
|
|
2277
3238
|
if (isBot(userAgent)) {
|
|
2278
3239
|
sendJson(res, 200, { ok: true });
|
|
@@ -2280,29 +3241,20 @@ async function createCollector(config) {
|
|
|
2280
3241
|
}
|
|
2281
3242
|
const ip = extractIp(req);
|
|
2282
3243
|
const enriched = enrichEvents(payload.events, ip, userAgent);
|
|
2283
|
-
const
|
|
2284
|
-
if (
|
|
2285
|
-
const
|
|
2286
|
-
if (
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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);
|
|
3244
|
+
const site = await db.getSite(siteId);
|
|
3245
|
+
if (site?.allowedOrigins && site.allowedOrigins.length > 0) {
|
|
3246
|
+
const requestHostname = extractRequestHostname(req);
|
|
3247
|
+
if (!requestHostname) {
|
|
3248
|
+
sendJson(res, 200, { ok: true });
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
const allowed = new Set(site.allowedOrigins.map((h) => h.toLowerCase()));
|
|
3252
|
+
if (!allowed.has(requestHostname)) {
|
|
2302
3253
|
sendJson(res, 200, { ok: true });
|
|
2303
3254
|
return;
|
|
2304
3255
|
}
|
|
2305
3256
|
}
|
|
3257
|
+
await processIdentity(enriched);
|
|
2306
3258
|
await db.insertEvents(enriched);
|
|
2307
3259
|
sendJson(res, 200, { ok: true });
|
|
2308
3260
|
} catch (err) {
|
|
@@ -2563,11 +3515,11 @@ async function createCollector(config) {
|
|
|
2563
3515
|
async listUsers(params) {
|
|
2564
3516
|
return db.listUsers(params);
|
|
2565
3517
|
},
|
|
2566
|
-
async getUserDetail(siteId,
|
|
2567
|
-
return db.getUserDetail(siteId,
|
|
3518
|
+
async getUserDetail(siteId, identifier) {
|
|
3519
|
+
return db.getUserDetail(siteId, identifier);
|
|
2568
3520
|
},
|
|
2569
|
-
async getUserEvents(siteId,
|
|
2570
|
-
return db.getUserEvents(siteId,
|
|
3521
|
+
async getUserEvents(siteId, identifier, params) {
|
|
3522
|
+
return db.getUserEvents(siteId, identifier, params);
|
|
2571
3523
|
},
|
|
2572
3524
|
async track(siteId, name, properties, options) {
|
|
2573
3525
|
const event = {
|