@litemetrics/node 0.1.3 → 0.3.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 +1 -1
- package/dist/index.cjs +669 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +669 -143
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,36 @@ var import_client = require("@clickhouse/client");
|
|
|
42
42
|
|
|
43
43
|
// src/adapters/utils.ts
|
|
44
44
|
var import_crypto = require("crypto");
|
|
45
|
+
function getTimezoneOffsetMs(date, timezone) {
|
|
46
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
47
|
+
timeZone: timezone,
|
|
48
|
+
year: "numeric",
|
|
49
|
+
month: "2-digit",
|
|
50
|
+
day: "2-digit",
|
|
51
|
+
hour: "2-digit",
|
|
52
|
+
minute: "2-digit",
|
|
53
|
+
second: "2-digit",
|
|
54
|
+
hour12: false
|
|
55
|
+
});
|
|
56
|
+
const parts = formatter.formatToParts(date);
|
|
57
|
+
const get = (type) => parts.find((p) => p.type === type).value;
|
|
58
|
+
const y = parseInt(get("year"));
|
|
59
|
+
const m = parseInt(get("month")) - 1;
|
|
60
|
+
const d = parseInt(get("day"));
|
|
61
|
+
const h = parseInt(get("hour") === "24" ? "0" : get("hour"));
|
|
62
|
+
const mi = parseInt(get("minute"));
|
|
63
|
+
const s = parseInt(get("second"));
|
|
64
|
+
return Date.UTC(y, m, d, h, mi, s) - date.getTime();
|
|
65
|
+
}
|
|
66
|
+
function toUTCDate(value) {
|
|
67
|
+
if (value instanceof Date) return value;
|
|
68
|
+
if (typeof value === "number") return new Date(value);
|
|
69
|
+
const s = String(value).trim();
|
|
70
|
+
if (s.length >= 10 && !s.endsWith("Z") && !/[+-]\d{2}:?\d{2}$/.test(s)) {
|
|
71
|
+
return /* @__PURE__ */ new Date(s.replace(" ", "T") + "Z");
|
|
72
|
+
}
|
|
73
|
+
return new Date(s);
|
|
74
|
+
}
|
|
45
75
|
function resolvePeriod(q) {
|
|
46
76
|
const now = /* @__PURE__ */ new Date();
|
|
47
77
|
const period = q.period ?? "7d";
|
|
@@ -108,60 +138,66 @@ function granularityToDateFormat(g) {
|
|
|
108
138
|
return "%Y-%m";
|
|
109
139
|
}
|
|
110
140
|
}
|
|
111
|
-
function fillBuckets(from, to, granularity, dateFormat, rows) {
|
|
141
|
+
function fillBuckets(from, to, granularity, dateFormat, rows, timezone) {
|
|
112
142
|
const map = new Map(rows.map((r) => [r._id, r.value]));
|
|
113
143
|
const points = [];
|
|
114
|
-
const
|
|
144
|
+
const fromOffset = timezone ? getTimezoneOffsetMs(from, timezone) : 0;
|
|
145
|
+
const toOffset = timezone ? getTimezoneOffsetMs(to, timezone) : 0;
|
|
146
|
+
const current = new Date(from.getTime() + fromOffset);
|
|
147
|
+
const toWall = new Date(to.getTime() + toOffset);
|
|
115
148
|
if (granularity === "hour") {
|
|
116
|
-
current.
|
|
149
|
+
current.setUTCMinutes(0, 0, 0);
|
|
117
150
|
} else if (granularity === "day") {
|
|
118
|
-
current.
|
|
151
|
+
current.setUTCHours(0, 0, 0, 0);
|
|
119
152
|
} else if (granularity === "week") {
|
|
120
|
-
const day = current.
|
|
153
|
+
const day = current.getUTCDay();
|
|
121
154
|
const diff = day === 0 ? -6 : 1 - day;
|
|
122
|
-
current.
|
|
123
|
-
current.
|
|
155
|
+
current.setUTCDate(current.getUTCDate() + diff);
|
|
156
|
+
current.setUTCHours(0, 0, 0, 0);
|
|
124
157
|
} else if (granularity === "month") {
|
|
125
|
-
current.
|
|
126
|
-
current.
|
|
158
|
+
current.setUTCDate(1);
|
|
159
|
+
current.setUTCHours(0, 0, 0, 0);
|
|
127
160
|
}
|
|
128
|
-
while (current <=
|
|
161
|
+
while (current <= toWall) {
|
|
129
162
|
const key = formatDateBucket(current, dateFormat);
|
|
130
|
-
|
|
163
|
+
const approxUtc = new Date(current.getTime() - fromOffset);
|
|
164
|
+
const exactOffset = timezone ? getTimezoneOffsetMs(approxUtc, timezone) : 0;
|
|
165
|
+
const realUtc = new Date(current.getTime() - exactOffset);
|
|
166
|
+
points.push({ date: realUtc.toISOString(), value: map.get(key) ?? 0 });
|
|
131
167
|
if (granularity === "hour") {
|
|
132
|
-
current.
|
|
168
|
+
current.setUTCHours(current.getUTCHours() + 1);
|
|
133
169
|
} else if (granularity === "day") {
|
|
134
|
-
current.
|
|
170
|
+
current.setUTCDate(current.getUTCDate() + 1);
|
|
135
171
|
} else if (granularity === "week") {
|
|
136
|
-
current.
|
|
172
|
+
current.setUTCDate(current.getUTCDate() + 7);
|
|
137
173
|
} else if (granularity === "month") {
|
|
138
|
-
current.
|
|
174
|
+
current.setUTCMonth(current.getUTCMonth() + 1);
|
|
139
175
|
}
|
|
140
176
|
}
|
|
141
177
|
return points;
|
|
142
178
|
}
|
|
143
179
|
function formatDateBucket(date, format) {
|
|
144
|
-
const y = date.
|
|
145
|
-
const m = String(date.
|
|
146
|
-
const d = String(date.
|
|
147
|
-
const h = String(date.
|
|
180
|
+
const y = date.getUTCFullYear();
|
|
181
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
182
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
183
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
148
184
|
if (format === "%Y-%m-%dT%H:00") return `${y}-${m}-${d}T${h}:00`;
|
|
149
185
|
if (format === "%Y-%m-%d") return `${y}-${m}-${d}`;
|
|
150
186
|
if (format === "%Y-%m") return `${y}-${m}`;
|
|
151
187
|
if (format === "%G-W%V") {
|
|
152
|
-
const jan4 = new Date(y, 0, 4);
|
|
153
|
-
const dayOfYear = Math.ceil((date.getTime() -
|
|
154
|
-
const jan4Day = jan4.
|
|
188
|
+
const jan4 = new Date(Date.UTC(y, 0, 4));
|
|
189
|
+
const dayOfYear = Math.ceil((date.getTime() - Date.UTC(y, 0, 1)) / 864e5) + 1;
|
|
190
|
+
const jan4Day = jan4.getUTCDay() || 7;
|
|
155
191
|
const weekNum = Math.ceil((dayOfYear + jan4Day - 1) / 7);
|
|
156
192
|
return `${y}-W${String(weekNum).padStart(2, "0")}`;
|
|
157
193
|
}
|
|
158
194
|
return date.toISOString();
|
|
159
195
|
}
|
|
160
196
|
function getISOWeek(date) {
|
|
161
|
-
const y = date.
|
|
162
|
-
const jan4 = new Date(y, 0, 4);
|
|
163
|
-
const dayOfYear = Math.ceil((date.getTime() -
|
|
164
|
-
const jan4Day = jan4.
|
|
197
|
+
const y = date.getUTCFullYear();
|
|
198
|
+
const jan4 = new Date(Date.UTC(y, 0, 4));
|
|
199
|
+
const dayOfYear = Math.ceil((date.getTime() - Date.UTC(y, 0, 1)) / 864e5) + 1;
|
|
200
|
+
const jan4Day = jan4.getUTCDay() || 7;
|
|
165
201
|
const weekNum = Math.ceil((dayOfYear + jan4Day - 1) / 7);
|
|
166
202
|
return `${y}-W${String(weekNum).padStart(2, "0")}`;
|
|
167
203
|
}
|
|
@@ -176,6 +212,38 @@ function generateSecretKey() {
|
|
|
176
212
|
return `sk_${(0, import_crypto.randomBytes)(32).toString("hex")}`;
|
|
177
213
|
}
|
|
178
214
|
|
|
215
|
+
// src/query-helpers.ts
|
|
216
|
+
function isValidTimezone(tz) {
|
|
217
|
+
try {
|
|
218
|
+
Intl.DateTimeFormat(void 0, { timeZone: tz });
|
|
219
|
+
return true;
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function extractQueryParams(req) {
|
|
225
|
+
const q = req.query ?? Object.fromEntries(new URL(req.url, "http://localhost").searchParams);
|
|
226
|
+
let timezone;
|
|
227
|
+
if (typeof q.timezone === "string" && q.timezone) {
|
|
228
|
+
if (isValidTimezone(q.timezone)) {
|
|
229
|
+
timezone = q.timezone;
|
|
230
|
+
} else {
|
|
231
|
+
console.warn(`[litemetrics] Invalid timezone "${q.timezone}", falling back to UTC`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
siteId: q.siteId,
|
|
236
|
+
metric: q.metric,
|
|
237
|
+
period: q.period,
|
|
238
|
+
dateFrom: q.dateFrom,
|
|
239
|
+
dateTo: q.dateTo,
|
|
240
|
+
limit: q.limit ? parseInt(q.limit, 10) : void 0,
|
|
241
|
+
filters: q.filters ? JSON.parse(q.filters) : void 0,
|
|
242
|
+
compare: q.compare === "true" || q.compare === "1",
|
|
243
|
+
timezone
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
179
247
|
// src/adapters/clickhouse.ts
|
|
180
248
|
var EVENTS_TABLE = "litemetrics_events";
|
|
181
249
|
var SITES_TABLE = "litemetrics_sites";
|
|
@@ -229,6 +297,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
|
229
297
|
utm_term Nullable(String),
|
|
230
298
|
utm_content Nullable(String),
|
|
231
299
|
ip Nullable(String),
|
|
300
|
+
os_version LowCardinality(Nullable(String)),
|
|
301
|
+
device_model LowCardinality(Nullable(String)),
|
|
302
|
+
device_brand LowCardinality(Nullable(String)),
|
|
303
|
+
app_version LowCardinality(Nullable(String)),
|
|
304
|
+
app_build Nullable(String),
|
|
305
|
+
sdk_name LowCardinality(Nullable(String)),
|
|
306
|
+
sdk_version LowCardinality(Nullable(String)),
|
|
232
307
|
created_at DateTime64(3) DEFAULT now64(3)
|
|
233
308
|
) ENGINE = MergeTree()
|
|
234
309
|
PARTITION BY toYYYYMM(timestamp)
|
|
@@ -240,6 +315,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
240
315
|
site_id String,
|
|
241
316
|
secret_key String,
|
|
242
317
|
name String,
|
|
318
|
+
type LowCardinality(Nullable(String)) DEFAULT 'web',
|
|
243
319
|
domain Nullable(String),
|
|
244
320
|
allowed_origins Nullable(String),
|
|
245
321
|
conversion_events Nullable(String),
|
|
@@ -252,8 +328,74 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
252
328
|
SETTINGS index_granularity = 8192
|
|
253
329
|
`;
|
|
254
330
|
function toCHDateTime(d) {
|
|
255
|
-
const
|
|
256
|
-
|
|
331
|
+
const date = d instanceof Date ? d : toUTCDate(d);
|
|
332
|
+
const y = date.getUTCFullYear();
|
|
333
|
+
const mo = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
334
|
+
const da = String(date.getUTCDate()).padStart(2, "0");
|
|
335
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
336
|
+
const mi = String(date.getUTCMinutes()).padStart(2, "0");
|
|
337
|
+
const s = String(date.getUTCSeconds()).padStart(2, "0");
|
|
338
|
+
const ms = String(date.getUTCMilliseconds()).padStart(3, "0");
|
|
339
|
+
return `${y}-${mo}-${da} ${h}:${mi}:${s}.${ms}`;
|
|
340
|
+
}
|
|
341
|
+
function normalizedUtmSourceExpr() {
|
|
342
|
+
return `multiIf(
|
|
343
|
+
lower(utm_source) IN ('ig','instagram','instagram.com'), 'Instagram',
|
|
344
|
+
lower(utm_source) IN ('fb','facebook','facebook.com','fb.com'), 'Facebook',
|
|
345
|
+
lower(utm_source) IN ('tw','twitter','twitter.com','x','x.com','t.co'), 'X (Twitter)',
|
|
346
|
+
lower(utm_source) IN ('li','linkedin','linkedin.com'), 'LinkedIn',
|
|
347
|
+
lower(utm_source) IN ('yt','youtube','youtube.com'), 'YouTube',
|
|
348
|
+
lower(utm_source) IN ('goog','google','google.com'), 'Google',
|
|
349
|
+
lower(utm_source) IN ('gh','github','github.com'), 'GitHub',
|
|
350
|
+
lower(utm_source) IN ('reddit','reddit.com'), 'Reddit',
|
|
351
|
+
lower(utm_source) IN ('pinterest','pinterest.com'), 'Pinterest',
|
|
352
|
+
lower(utm_source) IN ('tiktok','tiktok.com'), 'TikTok',
|
|
353
|
+
lower(utm_source) IN ('openai','chatgpt','chat.openai.com'), 'OpenAI',
|
|
354
|
+
lower(utm_source) IN ('perplexity','perplexity.ai'), 'Perplexity',
|
|
355
|
+
utm_source
|
|
356
|
+
)`;
|
|
357
|
+
}
|
|
358
|
+
function normalizedUtmMediumExpr() {
|
|
359
|
+
return `multiIf(
|
|
360
|
+
lower(utm_medium) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid'), 'Paid',
|
|
361
|
+
lower(utm_medium) IN ('organic'), 'Organic',
|
|
362
|
+
lower(utm_medium) IN ('social','social-media','social_media'), 'Social',
|
|
363
|
+
lower(utm_medium) IN ('email','e-mail','e_mail'), 'Email',
|
|
364
|
+
lower(utm_medium) IN ('display','banner','cpm'), 'Display',
|
|
365
|
+
lower(utm_medium) IN ('affiliate'), 'Affiliate',
|
|
366
|
+
lower(utm_medium) IN ('referral'), 'Referral',
|
|
367
|
+
utm_medium
|
|
368
|
+
)`;
|
|
369
|
+
}
|
|
370
|
+
function channelClassificationExpr() {
|
|
371
|
+
return `multiIf(
|
|
372
|
+
lower(ifNull(utm_medium,'')) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')
|
|
373
|
+
AND (lower(ifNull(utm_source,'')) IN ('google','goog','bing','yahoo','duckduckgo','ecosia','baidu','yandex')
|
|
374
|
+
OR multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['google','bing','yahoo','duckduckgo','ecosia','baidu','yandex','search.brave']) > 0),
|
|
375
|
+
'Paid Search',
|
|
376
|
+
lower(ifNull(utm_medium,'')) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')
|
|
377
|
+
AND (lower(ifNull(utm_source,'')) IN ('instagram','ig','facebook','fb','twitter','tw','x','linkedin','li','youtube','yt','tiktok','pinterest','reddit','snapchat')
|
|
378
|
+
OR multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['instagram','facebook','twitter','x.com','t.co','linkedin','youtube','tiktok','pinterest','reddit','snapchat']) > 0),
|
|
379
|
+
'Paid Social',
|
|
380
|
+
lower(ifNull(utm_medium,'')) IN ('email','e-mail','e_mail'),
|
|
381
|
+
'Email',
|
|
382
|
+
lower(ifNull(utm_medium,'')) IN ('display','banner','cpm'),
|
|
383
|
+
'Display',
|
|
384
|
+
lower(ifNull(utm_medium,'')) IN ('affiliate'),
|
|
385
|
+
'Affiliate',
|
|
386
|
+
multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['google','bing','yahoo','duckduckgo','ecosia','baidu','yandex','search.brave']) > 0
|
|
387
|
+
AND (ifNull(utm_medium,'') = '' OR lower(utm_medium) NOT IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')),
|
|
388
|
+
'Organic Search',
|
|
389
|
+
(multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['instagram','facebook','twitter','x.com','t.co','linkedin','youtube','tiktok','pinterest','reddit','snapchat','mastodon','tumblr']) > 0
|
|
390
|
+
OR lower(ifNull(utm_source,'')) IN ('instagram','ig','facebook','fb','twitter','tw','x','linkedin','li','youtube','yt','tiktok','pinterest','reddit','snapchat'))
|
|
391
|
+
AND (ifNull(utm_medium,'') = '' OR lower(utm_medium) NOT IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')),
|
|
392
|
+
'Organic Social',
|
|
393
|
+
ifNull(referrer,'') != '' AND length(ifNull(referrer,'')) > 0,
|
|
394
|
+
'Referral',
|
|
395
|
+
(ifNull(utm_source,'') != '' OR ifNull(utm_medium,'') != '' OR ifNull(utm_campaign,'') != ''),
|
|
396
|
+
'Other',
|
|
397
|
+
'Direct'
|
|
398
|
+
)`;
|
|
257
399
|
}
|
|
258
400
|
function buildFilterConditions(filters) {
|
|
259
401
|
if (!filters) return { conditions: [], params: {} };
|
|
@@ -265,6 +407,10 @@ function buildFilterConditions(filters) {
|
|
|
265
407
|
"device.type": "device_type",
|
|
266
408
|
"device.browser": "browser",
|
|
267
409
|
"device.os": "os",
|
|
410
|
+
"device.osVersion": "os_version",
|
|
411
|
+
"device.deviceModel": "device_model",
|
|
412
|
+
"device.deviceBrand": "device_brand",
|
|
413
|
+
"device.appVersion": "app_version",
|
|
268
414
|
"utm.source": "utm_source",
|
|
269
415
|
"utm.medium": "utm_medium",
|
|
270
416
|
"utm.campaign": "utm_campaign",
|
|
@@ -281,7 +427,14 @@ function buildFilterConditions(filters) {
|
|
|
281
427
|
const conditions = [];
|
|
282
428
|
const params = {};
|
|
283
429
|
for (const [key, value] of Object.entries(filters)) {
|
|
284
|
-
if (!value
|
|
430
|
+
if (!value) continue;
|
|
431
|
+
if (key === "channel") {
|
|
432
|
+
const paramKey2 = "f_channel";
|
|
433
|
+
conditions.push(`${channelClassificationExpr()} = {${paramKey2}:String}`);
|
|
434
|
+
params[paramKey2] = value;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (!map[key]) continue;
|
|
285
438
|
const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
286
439
|
conditions.push(`${map[key]} = {${paramKey}:String}`);
|
|
287
440
|
params[paramKey] = value;
|
|
@@ -312,6 +465,14 @@ var ClickHouseAdapter = class {
|
|
|
312
465
|
await this.client.command({
|
|
313
466
|
query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
|
|
314
467
|
});
|
|
468
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS os_version LowCardinality(Nullable(String))` });
|
|
469
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS device_model LowCardinality(Nullable(String))` });
|
|
470
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS device_brand LowCardinality(Nullable(String))` });
|
|
471
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS app_version LowCardinality(Nullable(String))` });
|
|
472
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS app_build Nullable(String)` });
|
|
473
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS sdk_name LowCardinality(Nullable(String))` });
|
|
474
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS sdk_version LowCardinality(Nullable(String))` });
|
|
475
|
+
await this.client.command({ query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS type LowCardinality(Nullable(String)) DEFAULT 'web'` });
|
|
315
476
|
}
|
|
316
477
|
async close() {
|
|
317
478
|
await this.client.close();
|
|
@@ -354,7 +515,14 @@ var ClickHouseAdapter = class {
|
|
|
354
515
|
utm_campaign: e.utm?.campaign ?? null,
|
|
355
516
|
utm_term: e.utm?.term ?? null,
|
|
356
517
|
utm_content: e.utm?.content ?? null,
|
|
357
|
-
ip: e.ip ?? null
|
|
518
|
+
ip: e.ip ?? null,
|
|
519
|
+
os_version: e.device?.osVersion ?? null,
|
|
520
|
+
device_model: e.device?.deviceModel ?? null,
|
|
521
|
+
device_brand: e.device?.deviceBrand ?? null,
|
|
522
|
+
app_version: e.device?.appVersion ?? null,
|
|
523
|
+
app_build: e.device?.appBuild ?? null,
|
|
524
|
+
sdk_name: e.device?.sdkName ?? null,
|
|
525
|
+
sdk_version: e.device?.sdkVersion ?? null
|
|
358
526
|
}));
|
|
359
527
|
await this.client.insert({
|
|
360
528
|
table: EVENTS_TABLE,
|
|
@@ -705,15 +873,67 @@ var ClickHouseAdapter = class {
|
|
|
705
873
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
706
874
|
break;
|
|
707
875
|
}
|
|
876
|
+
case "top_os_versions": {
|
|
877
|
+
const rows = await this.queryRows(
|
|
878
|
+
`SELECT concat(os, ' ', ifNull(os_version, '')) AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
879
|
+
WHERE site_id = {siteId:String}
|
|
880
|
+
AND timestamp >= {from:String}
|
|
881
|
+
AND timestamp <= {to:String}
|
|
882
|
+
AND os IS NOT NULL
|
|
883
|
+
AND os_version IS NOT NULL
|
|
884
|
+
${filterSql}
|
|
885
|
+
GROUP BY key
|
|
886
|
+
ORDER BY value DESC
|
|
887
|
+
LIMIT {limit:UInt32}`,
|
|
888
|
+
{ ...params, ...filter.params }
|
|
889
|
+
);
|
|
890
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
891
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
case "top_device_models": {
|
|
895
|
+
const rows = await this.queryRows(
|
|
896
|
+
`SELECT trim(concat(ifNull(device_brand, ''), ' ', device_model)) AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
897
|
+
WHERE site_id = {siteId:String}
|
|
898
|
+
AND timestamp >= {from:String}
|
|
899
|
+
AND timestamp <= {to:String}
|
|
900
|
+
AND device_model IS NOT NULL
|
|
901
|
+
${filterSql}
|
|
902
|
+
GROUP BY key
|
|
903
|
+
ORDER BY value DESC
|
|
904
|
+
LIMIT {limit:UInt32}`,
|
|
905
|
+
{ ...params, ...filter.params }
|
|
906
|
+
);
|
|
907
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
908
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
case "top_app_versions": {
|
|
912
|
+
const rows = await this.queryRows(
|
|
913
|
+
`SELECT app_version AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
914
|
+
WHERE site_id = {siteId:String}
|
|
915
|
+
AND timestamp >= {from:String}
|
|
916
|
+
AND timestamp <= {to:String}
|
|
917
|
+
AND app_version IS NOT NULL
|
|
918
|
+
${filterSql}
|
|
919
|
+
GROUP BY app_version
|
|
920
|
+
ORDER BY value DESC
|
|
921
|
+
LIMIT {limit:UInt32}`,
|
|
922
|
+
{ ...params, ...filter.params }
|
|
923
|
+
);
|
|
924
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
925
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
708
928
|
case "top_utm_sources": {
|
|
709
929
|
const rows = await this.queryRows(
|
|
710
|
-
`SELECT
|
|
930
|
+
`SELECT ${normalizedUtmSourceExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
711
931
|
WHERE site_id = {siteId:String}
|
|
712
932
|
AND timestamp >= {from:String}
|
|
713
933
|
AND timestamp <= {to:String}
|
|
714
934
|
AND utm_source IS NOT NULL AND utm_source != ''
|
|
715
935
|
${filterSql}
|
|
716
|
-
GROUP BY
|
|
936
|
+
GROUP BY key
|
|
717
937
|
ORDER BY value DESC
|
|
718
938
|
LIMIT {limit:UInt32}`,
|
|
719
939
|
{ ...params, ...filter.params }
|
|
@@ -724,13 +944,13 @@ var ClickHouseAdapter = class {
|
|
|
724
944
|
}
|
|
725
945
|
case "top_utm_mediums": {
|
|
726
946
|
const rows = await this.queryRows(
|
|
727
|
-
`SELECT
|
|
947
|
+
`SELECT ${normalizedUtmMediumExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
728
948
|
WHERE site_id = {siteId:String}
|
|
729
949
|
AND timestamp >= {from:String}
|
|
730
950
|
AND timestamp <= {to:String}
|
|
731
951
|
AND utm_medium IS NOT NULL AND utm_medium != ''
|
|
732
952
|
${filterSql}
|
|
733
|
-
GROUP BY
|
|
953
|
+
GROUP BY key
|
|
734
954
|
ORDER BY value DESC
|
|
735
955
|
LIMIT {limit:UInt32}`,
|
|
736
956
|
{ ...params, ...filter.params }
|
|
@@ -756,6 +976,56 @@ var ClickHouseAdapter = class {
|
|
|
756
976
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
757
977
|
break;
|
|
758
978
|
}
|
|
979
|
+
case "top_utm_terms": {
|
|
980
|
+
const rows = await this.queryRows(
|
|
981
|
+
`SELECT utm_term AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
982
|
+
WHERE site_id = {siteId:String}
|
|
983
|
+
AND timestamp >= {from:String}
|
|
984
|
+
AND timestamp <= {to:String}
|
|
985
|
+
AND utm_term IS NOT NULL AND utm_term != ''
|
|
986
|
+
${filterSql}
|
|
987
|
+
GROUP BY utm_term
|
|
988
|
+
ORDER BY value DESC
|
|
989
|
+
LIMIT {limit:UInt32}`,
|
|
990
|
+
{ ...params, ...filter.params }
|
|
991
|
+
);
|
|
992
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
993
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
case "top_utm_contents": {
|
|
997
|
+
const rows = await this.queryRows(
|
|
998
|
+
`SELECT utm_content AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
999
|
+
WHERE site_id = {siteId:String}
|
|
1000
|
+
AND timestamp >= {from:String}
|
|
1001
|
+
AND timestamp <= {to:String}
|
|
1002
|
+
AND utm_content IS NOT NULL AND utm_content != ''
|
|
1003
|
+
${filterSql}
|
|
1004
|
+
GROUP BY utm_content
|
|
1005
|
+
ORDER BY value DESC
|
|
1006
|
+
LIMIT {limit:UInt32}`,
|
|
1007
|
+
{ ...params, ...filter.params }
|
|
1008
|
+
);
|
|
1009
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
1010
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
case "top_channels": {
|
|
1014
|
+
const rows = await this.queryRows(
|
|
1015
|
+
`SELECT ${channelClassificationExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
1016
|
+
WHERE site_id = {siteId:String}
|
|
1017
|
+
AND timestamp >= {from:String}
|
|
1018
|
+
AND timestamp <= {to:String}
|
|
1019
|
+
${filterSql}
|
|
1020
|
+
GROUP BY key
|
|
1021
|
+
ORDER BY value DESC
|
|
1022
|
+
LIMIT {limit:UInt32}`,
|
|
1023
|
+
{ ...params, ...filter.params }
|
|
1024
|
+
);
|
|
1025
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
1026
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
759
1029
|
}
|
|
760
1030
|
const result = { metric: q.metric, period, data, total };
|
|
761
1031
|
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
@@ -786,7 +1056,7 @@ var ClickHouseAdapter = class {
|
|
|
786
1056
|
dateTo: params.dateTo
|
|
787
1057
|
});
|
|
788
1058
|
const granularity = params.granularity ?? autoGranularity(period);
|
|
789
|
-
const bucketFn = this.granularityToClickHouseFunc(granularity);
|
|
1059
|
+
const bucketFn = this.granularityToClickHouseFunc(granularity, params.timezone);
|
|
790
1060
|
const dateFormat = granularityToDateFormat(granularity);
|
|
791
1061
|
const filter = buildFilterConditions(params.filters);
|
|
792
1062
|
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
@@ -835,38 +1105,41 @@ var ClickHouseAdapter = class {
|
|
|
835
1105
|
new Date(dateRange.to),
|
|
836
1106
|
granularity,
|
|
837
1107
|
dateFormat,
|
|
838
|
-
mappedRows
|
|
1108
|
+
mappedRows,
|
|
1109
|
+
params.timezone
|
|
839
1110
|
);
|
|
840
1111
|
return { metric: params.metric, granularity, data };
|
|
841
1112
|
}
|
|
842
|
-
granularityToClickHouseFunc(g) {
|
|
1113
|
+
granularityToClickHouseFunc(g, timezone) {
|
|
1114
|
+
const safeTz = timezone && isValidTimezone(timezone) ? timezone : void 0;
|
|
1115
|
+
const tz = safeTz ? `, '${safeTz}'` : "";
|
|
843
1116
|
switch (g) {
|
|
844
1117
|
case "hour":
|
|
845
|
-
return
|
|
1118
|
+
return `toStartOfHour(timestamp${tz})`;
|
|
846
1119
|
case "day":
|
|
847
|
-
return
|
|
1120
|
+
return `toStartOfDay(timestamp${tz})`;
|
|
848
1121
|
case "week":
|
|
849
|
-
return
|
|
1122
|
+
return `toStartOfWeek(timestamp, 1${tz})`;
|
|
850
1123
|
// 1 = Monday
|
|
851
1124
|
case "month":
|
|
852
|
-
return
|
|
1125
|
+
return `toStartOfMonth(timestamp${tz})`;
|
|
853
1126
|
}
|
|
854
1127
|
}
|
|
855
1128
|
convertClickHouseBucket(bucket, granularity) {
|
|
856
|
-
const date =
|
|
857
|
-
const y = date.
|
|
858
|
-
const m = String(date.
|
|
859
|
-
const d = String(date.
|
|
860
|
-
const h = String(date.
|
|
1129
|
+
const date = toUTCDate(bucket);
|
|
1130
|
+
const y = date.getUTCFullYear();
|
|
1131
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
1132
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
1133
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
861
1134
|
switch (granularity) {
|
|
862
1135
|
case "hour":
|
|
863
1136
|
return `${y}-${m}-${d}T${h}:00`;
|
|
864
1137
|
case "day":
|
|
865
1138
|
return `${y}-${m}-${d}`;
|
|
866
1139
|
case "week": {
|
|
867
|
-
const jan4 = new Date(y, 0, 4);
|
|
868
|
-
const dayOfYear = Math.ceil((date.getTime() -
|
|
869
|
-
const jan4Day = jan4.
|
|
1140
|
+
const jan4 = new Date(Date.UTC(y, 0, 4));
|
|
1141
|
+
const dayOfYear = Math.ceil((date.getTime() - Date.UTC(y, 0, 1)) / 864e5) + 1;
|
|
1142
|
+
const jan4Day = jan4.getUTCDay() || 7;
|
|
870
1143
|
const weekNum = Math.ceil((dayOfYear + jan4Day - 1) / 7);
|
|
871
1144
|
return `${y}-W${String(weekNum).padStart(2, "0")}`;
|
|
872
1145
|
}
|
|
@@ -895,7 +1168,7 @@ var ClickHouseAdapter = class {
|
|
|
895
1168
|
);
|
|
896
1169
|
const cohortMap = /* @__PURE__ */ new Map();
|
|
897
1170
|
for (const v of rows) {
|
|
898
|
-
const firstDate =
|
|
1171
|
+
const firstDate = toUTCDate(v.first_event);
|
|
899
1172
|
const cohortWeek = getISOWeek(firstDate);
|
|
900
1173
|
if (!cohortMap.has(cohortWeek)) {
|
|
901
1174
|
cohortMap.set(cohortWeek, { visitors: /* @__PURE__ */ new Set(), weekSets: /* @__PURE__ */ new Map() });
|
|
@@ -903,7 +1176,7 @@ var ClickHouseAdapter = class {
|
|
|
903
1176
|
const cohort = cohortMap.get(cohortWeek);
|
|
904
1177
|
cohort.visitors.add(v.visitor_id);
|
|
905
1178
|
const eventWeeks = (Array.isArray(v.active_weeks) ? v.active_weeks : []).map((w) => {
|
|
906
|
-
const d =
|
|
1179
|
+
const d = toUTCDate(w);
|
|
907
1180
|
return getISOWeek(d);
|
|
908
1181
|
});
|
|
909
1182
|
for (const w of eventWeeks) {
|
|
@@ -1070,8 +1343,8 @@ var ClickHouseAdapter = class {
|
|
|
1070
1343
|
visitorId: String(u.visitor_id),
|
|
1071
1344
|
userId: u.userId ? String(u.userId) : void 0,
|
|
1072
1345
|
traits: this.parseJSON(u.traits),
|
|
1073
|
-
firstSeen:
|
|
1074
|
-
lastSeen:
|
|
1346
|
+
firstSeen: toUTCDate(String(u.firstSeen)).toISOString(),
|
|
1347
|
+
lastSeen: toUTCDate(String(u.lastSeen)).toISOString(),
|
|
1075
1348
|
totalEvents: Number(u.totalEvents),
|
|
1076
1349
|
totalPageviews: Number(u.totalPageviews),
|
|
1077
1350
|
totalSessions: Number(u.totalSessions),
|
|
@@ -1158,7 +1431,7 @@ var ClickHouseAdapter = class {
|
|
|
1158
1431
|
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
1159
1432
|
const rows = await this.queryRows(
|
|
1160
1433
|
`SELECT
|
|
1161
|
-
anyLast(visitor_id) AS
|
|
1434
|
+
anyLast(visitor_id) AS last_visitor_id,
|
|
1162
1435
|
anyLast(traits) AS traits,
|
|
1163
1436
|
min(timestamp) AS firstSeen,
|
|
1164
1437
|
max(timestamp) AS lastSeen,
|
|
@@ -1190,12 +1463,12 @@ var ClickHouseAdapter = class {
|
|
|
1190
1463
|
if (rows.length === 0) return null;
|
|
1191
1464
|
const u = rows[0];
|
|
1192
1465
|
return {
|
|
1193
|
-
visitorId: String(u.
|
|
1466
|
+
visitorId: String(u.last_visitor_id),
|
|
1194
1467
|
visitorIds,
|
|
1195
1468
|
userId,
|
|
1196
1469
|
traits: this.parseJSON(u.traits),
|
|
1197
|
-
firstSeen:
|
|
1198
|
-
lastSeen:
|
|
1470
|
+
firstSeen: toUTCDate(String(u.firstSeen)).toISOString(),
|
|
1471
|
+
lastSeen: toUTCDate(String(u.lastSeen)).toISOString(),
|
|
1199
1472
|
totalEvents: Number(u.totalEvents),
|
|
1200
1473
|
totalPageviews: Number(u.totalPageviews),
|
|
1201
1474
|
totalSessions: Number(u.totalSessions),
|
|
@@ -1279,6 +1552,7 @@ var ClickHouseAdapter = class {
|
|
|
1279
1552
|
siteId: generateSiteId(),
|
|
1280
1553
|
secretKey: generateSecretKey(),
|
|
1281
1554
|
name: data.name,
|
|
1555
|
+
type: data.type ?? "web",
|
|
1282
1556
|
domain: data.domain,
|
|
1283
1557
|
allowedOrigins: data.allowedOrigins,
|
|
1284
1558
|
conversionEvents: data.conversionEvents,
|
|
@@ -1291,6 +1565,7 @@ var ClickHouseAdapter = class {
|
|
|
1291
1565
|
site_id: site.siteId,
|
|
1292
1566
|
secret_key: site.secretKey,
|
|
1293
1567
|
name: site.name,
|
|
1568
|
+
type: site.type ?? "web",
|
|
1294
1569
|
domain: site.domain ?? null,
|
|
1295
1570
|
allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
|
|
1296
1571
|
conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
|
|
@@ -1305,7 +1580,7 @@ var ClickHouseAdapter = class {
|
|
|
1305
1580
|
}
|
|
1306
1581
|
async getSite(siteId) {
|
|
1307
1582
|
const rows = await this.queryRows(
|
|
1308
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1583
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1309
1584
|
FROM ${SITES_TABLE} FINAL
|
|
1310
1585
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1311
1586
|
{ siteId }
|
|
@@ -1314,7 +1589,7 @@ var ClickHouseAdapter = class {
|
|
|
1314
1589
|
}
|
|
1315
1590
|
async getSiteBySecret(secretKey) {
|
|
1316
1591
|
const rows = await this.queryRows(
|
|
1317
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1592
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1318
1593
|
FROM ${SITES_TABLE} FINAL
|
|
1319
1594
|
WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
|
|
1320
1595
|
{ secretKey }
|
|
@@ -1323,7 +1598,7 @@ var ClickHouseAdapter = class {
|
|
|
1323
1598
|
}
|
|
1324
1599
|
async listSites() {
|
|
1325
1600
|
const rows = await this.queryRows(
|
|
1326
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1601
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1327
1602
|
FROM ${SITES_TABLE} FINAL
|
|
1328
1603
|
WHERE is_deleted = 0
|
|
1329
1604
|
ORDER BY created_at DESC`,
|
|
@@ -1333,7 +1608,7 @@ var ClickHouseAdapter = class {
|
|
|
1333
1608
|
}
|
|
1334
1609
|
async updateSite(siteId, data) {
|
|
1335
1610
|
const currentRows = await this.queryRows(
|
|
1336
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
1611
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
1337
1612
|
FROM ${SITES_TABLE} FINAL
|
|
1338
1613
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1339
1614
|
{ siteId }
|
|
@@ -1345,6 +1620,7 @@ var ClickHouseAdapter = class {
|
|
|
1345
1620
|
const nowCH = toCHDateTime(now);
|
|
1346
1621
|
const newVersion = Number(current.version) + 1;
|
|
1347
1622
|
const newName = data.name !== void 0 ? data.name : String(current.name);
|
|
1623
|
+
const newType = data.type !== void 0 ? data.type : current.type ? String(current.type) : "web";
|
|
1348
1624
|
const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
|
|
1349
1625
|
const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
|
|
1350
1626
|
const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
|
|
@@ -1354,6 +1630,7 @@ var ClickHouseAdapter = class {
|
|
|
1354
1630
|
site_id: String(current.site_id),
|
|
1355
1631
|
secret_key: String(current.secret_key),
|
|
1356
1632
|
name: newName,
|
|
1633
|
+
type: newType,
|
|
1357
1634
|
domain: newDomain,
|
|
1358
1635
|
allowed_origins: newOrigins,
|
|
1359
1636
|
conversion_events: newConversions,
|
|
@@ -1368,6 +1645,7 @@ var ClickHouseAdapter = class {
|
|
|
1368
1645
|
siteId: String(current.site_id),
|
|
1369
1646
|
secretKey: String(current.secret_key),
|
|
1370
1647
|
name: newName,
|
|
1648
|
+
type: newType,
|
|
1371
1649
|
domain: newDomain ?? void 0,
|
|
1372
1650
|
allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
|
|
1373
1651
|
conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
|
|
@@ -1377,7 +1655,7 @@ var ClickHouseAdapter = class {
|
|
|
1377
1655
|
}
|
|
1378
1656
|
async deleteSite(siteId) {
|
|
1379
1657
|
const currentRows = await this.queryRows(
|
|
1380
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
1658
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, version
|
|
1381
1659
|
FROM ${SITES_TABLE} FINAL
|
|
1382
1660
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1383
1661
|
{ siteId }
|
|
@@ -1391,6 +1669,7 @@ var ClickHouseAdapter = class {
|
|
|
1391
1669
|
site_id: String(current.site_id),
|
|
1392
1670
|
secret_key: String(current.secret_key),
|
|
1393
1671
|
name: String(current.name),
|
|
1672
|
+
type: current.type ? String(current.type) : "web",
|
|
1394
1673
|
domain: current.domain ? String(current.domain) : null,
|
|
1395
1674
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1396
1675
|
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
@@ -1405,7 +1684,7 @@ var ClickHouseAdapter = class {
|
|
|
1405
1684
|
}
|
|
1406
1685
|
async regenerateSecret(siteId) {
|
|
1407
1686
|
const currentRows = await this.queryRows(
|
|
1408
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
1687
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, version
|
|
1409
1688
|
FROM ${SITES_TABLE} FINAL
|
|
1410
1689
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1411
1690
|
{ siteId }
|
|
@@ -1422,6 +1701,7 @@ var ClickHouseAdapter = class {
|
|
|
1422
1701
|
site_id: String(current.site_id),
|
|
1423
1702
|
secret_key: newSecret,
|
|
1424
1703
|
name: String(current.name),
|
|
1704
|
+
type: current.type ? String(current.type) : "web",
|
|
1425
1705
|
domain: current.domain ? String(current.domain) : null,
|
|
1426
1706
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1427
1707
|
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
@@ -1436,6 +1716,7 @@ var ClickHouseAdapter = class {
|
|
|
1436
1716
|
siteId: String(current.site_id),
|
|
1437
1717
|
secretKey: newSecret,
|
|
1438
1718
|
name: String(current.name),
|
|
1719
|
+
type: current.type ? String(current.type) : "web",
|
|
1439
1720
|
domain: current.domain ? String(current.domain) : void 0,
|
|
1440
1721
|
allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
|
|
1441
1722
|
conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
|
|
@@ -1457,18 +1738,19 @@ var ClickHouseAdapter = class {
|
|
|
1457
1738
|
siteId: String(row.site_id),
|
|
1458
1739
|
secretKey: String(row.secret_key),
|
|
1459
1740
|
name: String(row.name),
|
|
1741
|
+
type: row.type ? String(row.type) : "web",
|
|
1460
1742
|
domain: row.domain ? String(row.domain) : void 0,
|
|
1461
1743
|
allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
|
|
1462
1744
|
conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
|
|
1463
|
-
createdAt:
|
|
1464
|
-
updatedAt:
|
|
1745
|
+
createdAt: toUTCDate(String(row.created_at)).toISOString(),
|
|
1746
|
+
updatedAt: toUTCDate(String(row.updated_at)).toISOString()
|
|
1465
1747
|
};
|
|
1466
1748
|
}
|
|
1467
1749
|
toEventListItem(row) {
|
|
1468
1750
|
return {
|
|
1469
1751
|
id: String(row.event_id ?? ""),
|
|
1470
1752
|
type: String(row.type),
|
|
1471
|
-
timestamp:
|
|
1753
|
+
timestamp: toUTCDate(String(row.timestamp)).toISOString(),
|
|
1472
1754
|
visitorId: String(row.visitor_id),
|
|
1473
1755
|
sessionId: String(row.session_id),
|
|
1474
1756
|
url: row.url ? String(row.url) : void 0,
|
|
@@ -1520,6 +1802,130 @@ var import_mongodb = require("mongodb");
|
|
|
1520
1802
|
var EVENTS_COLLECTION = "litemetrics_events";
|
|
1521
1803
|
var SITES_COLLECTION = "litemetrics_sites";
|
|
1522
1804
|
var IDENTITY_MAP_COLLECTION = "litemetrics_identity_map";
|
|
1805
|
+
function normalizedUtmSourceSwitch() {
|
|
1806
|
+
return {
|
|
1807
|
+
$switch: {
|
|
1808
|
+
branches: [
|
|
1809
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["ig", "instagram", "instagram.com"]] }, then: "Instagram" },
|
|
1810
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["fb", "facebook", "facebook.com", "fb.com"]] }, then: "Facebook" },
|
|
1811
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["tw", "twitter", "twitter.com", "x", "x.com", "t.co"]] }, then: "X (Twitter)" },
|
|
1812
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["li", "linkedin", "linkedin.com"]] }, then: "LinkedIn" },
|
|
1813
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["yt", "youtube", "youtube.com"]] }, then: "YouTube" },
|
|
1814
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["goog", "google", "google.com"]] }, then: "Google" },
|
|
1815
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["gh", "github", "github.com"]] }, then: "GitHub" },
|
|
1816
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["reddit", "reddit.com"]] }, then: "Reddit" },
|
|
1817
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["pinterest", "pinterest.com"]] }, then: "Pinterest" },
|
|
1818
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["tiktok", "tiktok.com"]] }, then: "TikTok" },
|
|
1819
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["openai", "chatgpt", "chat.openai.com"]] }, then: "OpenAI" },
|
|
1820
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["perplexity", "perplexity.ai"]] }, then: "Perplexity" }
|
|
1821
|
+
],
|
|
1822
|
+
default: "$utm_source"
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
function normalizedUtmMediumSwitch() {
|
|
1827
|
+
return {
|
|
1828
|
+
$switch: {
|
|
1829
|
+
branches: [
|
|
1830
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["cpc", "ppc", "paidsearch", "paid-search", "paid_search", "paid"]] }, then: "Paid" },
|
|
1831
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["organic"]] }, then: "Organic" },
|
|
1832
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["social", "social-media", "social_media"]] }, then: "Social" },
|
|
1833
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["email", "e-mail", "e_mail"]] }, then: "Email" },
|
|
1834
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["display", "banner", "cpm"]] }, then: "Display" },
|
|
1835
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["affiliate"]] }, then: "Affiliate" },
|
|
1836
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["referral"]] }, then: "Referral" }
|
|
1837
|
+
],
|
|
1838
|
+
default: "$utm_medium"
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
var SEARCH_ENGINES = /google|bing|yahoo|duckduckgo|ecosia|baidu|yandex|search\.brave/i;
|
|
1843
|
+
var SOCIAL_NETWORKS = /instagram|facebook|twitter|x\.com|t\.co|linkedin|youtube|tiktok|pinterest|reddit|snapchat|mastodon|tumblr/i;
|
|
1844
|
+
var PAID_MEDIUMS = ["cpc", "ppc", "paidsearch", "paid-search", "paid_search", "paid"];
|
|
1845
|
+
var SOCIAL_SOURCES = ["instagram", "ig", "facebook", "fb", "twitter", "tw", "x", "linkedin", "li", "youtube", "yt", "tiktok", "pinterest", "reddit", "snapchat"];
|
|
1846
|
+
function channelClassificationSwitch() {
|
|
1847
|
+
const lMedium = { $toLower: { $ifNull: ["$utm_medium", ""] } };
|
|
1848
|
+
const lSource = { $toLower: { $ifNull: ["$utm_source", ""] } };
|
|
1849
|
+
const refStr = { $ifNull: ["$referrer", ""] };
|
|
1850
|
+
return {
|
|
1851
|
+
$switch: {
|
|
1852
|
+
branches: [
|
|
1853
|
+
// Paid Search
|
|
1854
|
+
{
|
|
1855
|
+
case: {
|
|
1856
|
+
$and: [
|
|
1857
|
+
{ $in: [lMedium, PAID_MEDIUMS] },
|
|
1858
|
+
{ $or: [
|
|
1859
|
+
{ $in: [lSource, ["google", "goog", "bing", "yahoo", "duckduckgo", "ecosia", "baidu", "yandex"]] },
|
|
1860
|
+
{ $regexMatch: { input: refStr, regex: SEARCH_ENGINES } }
|
|
1861
|
+
] }
|
|
1862
|
+
]
|
|
1863
|
+
},
|
|
1864
|
+
then: "Paid Search"
|
|
1865
|
+
},
|
|
1866
|
+
// Paid Social
|
|
1867
|
+
{
|
|
1868
|
+
case: {
|
|
1869
|
+
$and: [
|
|
1870
|
+
{ $in: [lMedium, PAID_MEDIUMS] },
|
|
1871
|
+
{ $or: [
|
|
1872
|
+
{ $in: [lSource, SOCIAL_SOURCES] },
|
|
1873
|
+
{ $regexMatch: { input: refStr, regex: SOCIAL_NETWORKS } }
|
|
1874
|
+
] }
|
|
1875
|
+
]
|
|
1876
|
+
},
|
|
1877
|
+
then: "Paid Social"
|
|
1878
|
+
},
|
|
1879
|
+
// Email
|
|
1880
|
+
{ case: { $in: [lMedium, ["email", "e-mail", "e_mail"]] }, then: "Email" },
|
|
1881
|
+
// Display
|
|
1882
|
+
{ case: { $in: [lMedium, ["display", "banner", "cpm"]] }, then: "Display" },
|
|
1883
|
+
// Affiliate
|
|
1884
|
+
{ case: { $in: [lMedium, ["affiliate"]] }, then: "Affiliate" },
|
|
1885
|
+
// Organic Search
|
|
1886
|
+
{
|
|
1887
|
+
case: {
|
|
1888
|
+
$and: [
|
|
1889
|
+
{ $regexMatch: { input: refStr, regex: SEARCH_ENGINES } },
|
|
1890
|
+
{ $not: [{ $in: [lMedium, PAID_MEDIUMS] }] }
|
|
1891
|
+
]
|
|
1892
|
+
},
|
|
1893
|
+
then: "Organic Search"
|
|
1894
|
+
},
|
|
1895
|
+
// Organic Social
|
|
1896
|
+
{
|
|
1897
|
+
case: {
|
|
1898
|
+
$and: [
|
|
1899
|
+
{ $or: [
|
|
1900
|
+
{ $regexMatch: { input: refStr, regex: SOCIAL_NETWORKS } },
|
|
1901
|
+
{ $in: [lSource, SOCIAL_SOURCES] }
|
|
1902
|
+
] },
|
|
1903
|
+
{ $not: [{ $in: [lMedium, PAID_MEDIUMS] }] }
|
|
1904
|
+
]
|
|
1905
|
+
},
|
|
1906
|
+
then: "Organic Social"
|
|
1907
|
+
},
|
|
1908
|
+
// Referral
|
|
1909
|
+
{
|
|
1910
|
+
case: { $and: [{ $ne: [refStr, ""] }, { $gt: [{ $strLenCP: refStr }, 0] }] },
|
|
1911
|
+
then: "Referral"
|
|
1912
|
+
},
|
|
1913
|
+
// Other (has UTM but no referrer)
|
|
1914
|
+
{
|
|
1915
|
+
case: {
|
|
1916
|
+
$or: [
|
|
1917
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_source", ""] }, ""] }] },
|
|
1918
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_medium", ""] }, ""] }] },
|
|
1919
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_campaign", ""] }, ""] }] }
|
|
1920
|
+
]
|
|
1921
|
+
},
|
|
1922
|
+
then: "Other"
|
|
1923
|
+
}
|
|
1924
|
+
],
|
|
1925
|
+
default: "Direct"
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1523
1929
|
function buildFilterMatch(filters) {
|
|
1524
1930
|
if (!filters) return {};
|
|
1525
1931
|
const map = {
|
|
@@ -1530,6 +1936,10 @@ function buildFilterMatch(filters) {
|
|
|
1530
1936
|
"device.type": "device_type",
|
|
1531
1937
|
"device.browser": "browser",
|
|
1532
1938
|
"device.os": "os",
|
|
1939
|
+
"device.osVersion": "os_version",
|
|
1940
|
+
"device.deviceModel": "device_model",
|
|
1941
|
+
"device.deviceBrand": "device_brand",
|
|
1942
|
+
"device.appVersion": "app_version",
|
|
1533
1943
|
"utm.source": "utm_source",
|
|
1534
1944
|
"utm.medium": "utm_medium",
|
|
1535
1945
|
"utm.campaign": "utm_campaign",
|
|
@@ -1545,7 +1955,9 @@ function buildFilterMatch(filters) {
|
|
|
1545
1955
|
};
|
|
1546
1956
|
const match = {};
|
|
1547
1957
|
for (const [key, value] of Object.entries(filters)) {
|
|
1548
|
-
if (!value
|
|
1958
|
+
if (!value) continue;
|
|
1959
|
+
if (key === "channel") continue;
|
|
1960
|
+
if (!map[key]) continue;
|
|
1549
1961
|
match[map[key]] = value;
|
|
1550
1962
|
}
|
|
1551
1963
|
return match;
|
|
@@ -1614,6 +2026,13 @@ var MongoDBAdapter = class {
|
|
|
1614
2026
|
utm_term: e.utm?.term ?? null,
|
|
1615
2027
|
utm_content: e.utm?.content ?? null,
|
|
1616
2028
|
ip: e.ip ?? null,
|
|
2029
|
+
os_version: e.device?.osVersion ?? null,
|
|
2030
|
+
device_model: e.device?.deviceModel ?? null,
|
|
2031
|
+
device_brand: e.device?.deviceBrand ?? null,
|
|
2032
|
+
app_version: e.device?.appVersion ?? null,
|
|
2033
|
+
app_build: e.device?.appBuild ?? null,
|
|
2034
|
+
sdk_name: e.device?.sdkName ?? null,
|
|
2035
|
+
sdk_version: e.device?.sdkVersion ?? null,
|
|
1617
2036
|
created_at: /* @__PURE__ */ new Date()
|
|
1618
2037
|
}));
|
|
1619
2038
|
await this.collection.insertMany(docs);
|
|
@@ -1627,12 +2046,22 @@ var MongoDBAdapter = class {
|
|
|
1627
2046
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1628
2047
|
};
|
|
1629
2048
|
const filterMatch = buildFilterMatch(q.filters);
|
|
2049
|
+
const matchStages = (extra) => {
|
|
2050
|
+
const stages = [
|
|
2051
|
+
{ $match: { ...baseMatch, ...filterMatch, ...extra } }
|
|
2052
|
+
];
|
|
2053
|
+
if (q.filters?.channel) {
|
|
2054
|
+
stages.push({ $addFields: { _channel: channelClassificationSwitch() } });
|
|
2055
|
+
stages.push({ $match: { _channel: q.filters.channel } });
|
|
2056
|
+
}
|
|
2057
|
+
return stages;
|
|
2058
|
+
};
|
|
1630
2059
|
let data = [];
|
|
1631
2060
|
let total = 0;
|
|
1632
2061
|
switch (q.metric) {
|
|
1633
2062
|
case "pageviews": {
|
|
1634
2063
|
const [result2] = await this.collection.aggregate([
|
|
1635
|
-
{
|
|
2064
|
+
...matchStages({ type: "pageview" }),
|
|
1636
2065
|
{ $count: "count" }
|
|
1637
2066
|
]).toArray();
|
|
1638
2067
|
total = result2?.count ?? 0;
|
|
@@ -1641,7 +2070,7 @@ var MongoDBAdapter = class {
|
|
|
1641
2070
|
}
|
|
1642
2071
|
case "visitors": {
|
|
1643
2072
|
const [result2] = await this.collection.aggregate([
|
|
1644
|
-
|
|
2073
|
+
...matchStages(),
|
|
1645
2074
|
{ $group: { _id: "$visitor_id" } },
|
|
1646
2075
|
{ $count: "count" }
|
|
1647
2076
|
]).toArray();
|
|
@@ -1651,7 +2080,7 @@ var MongoDBAdapter = class {
|
|
|
1651
2080
|
}
|
|
1652
2081
|
case "sessions": {
|
|
1653
2082
|
const [result2] = await this.collection.aggregate([
|
|
1654
|
-
|
|
2083
|
+
...matchStages(),
|
|
1655
2084
|
{ $group: { _id: "$session_id" } },
|
|
1656
2085
|
{ $count: "count" }
|
|
1657
2086
|
]).toArray();
|
|
@@ -1661,7 +2090,7 @@ var MongoDBAdapter = class {
|
|
|
1661
2090
|
}
|
|
1662
2091
|
case "events": {
|
|
1663
2092
|
const [result2] = await this.collection.aggregate([
|
|
1664
|
-
{
|
|
2093
|
+
...matchStages({ type: "event" }),
|
|
1665
2094
|
{ $count: "count" }
|
|
1666
2095
|
]).toArray();
|
|
1667
2096
|
total = result2?.count ?? 0;
|
|
@@ -1676,7 +2105,7 @@ var MongoDBAdapter = class {
|
|
|
1676
2105
|
break;
|
|
1677
2106
|
}
|
|
1678
2107
|
const [result2] = await this.collection.aggregate([
|
|
1679
|
-
{
|
|
2108
|
+
...matchStages({ type: "event", event_name: { $in: conversionEvents } }),
|
|
1680
2109
|
{ $count: "count" }
|
|
1681
2110
|
]).toArray();
|
|
1682
2111
|
total = result2?.count ?? 0;
|
|
@@ -1685,7 +2114,7 @@ var MongoDBAdapter = class {
|
|
|
1685
2114
|
}
|
|
1686
2115
|
case "top_pages": {
|
|
1687
2116
|
const rows = await this.collection.aggregate([
|
|
1688
|
-
{
|
|
2117
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1689
2118
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1690
2119
|
{ $sort: { value: -1 } },
|
|
1691
2120
|
{ $limit: limit }
|
|
@@ -1696,7 +2125,7 @@ var MongoDBAdapter = class {
|
|
|
1696
2125
|
}
|
|
1697
2126
|
case "top_referrers": {
|
|
1698
2127
|
const rows = await this.collection.aggregate([
|
|
1699
|
-
{
|
|
2128
|
+
...matchStages({ type: "pageview", referrer: { $nin: [null, ""] } }),
|
|
1700
2129
|
{ $group: { _id: "$referrer", value: { $sum: 1 } } },
|
|
1701
2130
|
{ $sort: { value: -1 } },
|
|
1702
2131
|
{ $limit: limit }
|
|
@@ -1707,7 +2136,7 @@ var MongoDBAdapter = class {
|
|
|
1707
2136
|
}
|
|
1708
2137
|
case "top_countries": {
|
|
1709
2138
|
const rows = await this.collection.aggregate([
|
|
1710
|
-
{
|
|
2139
|
+
...matchStages({ country: { $ne: null } }),
|
|
1711
2140
|
{ $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
|
|
1712
2141
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1713
2142
|
{ $sort: { value: -1 } },
|
|
@@ -1719,7 +2148,7 @@ var MongoDBAdapter = class {
|
|
|
1719
2148
|
}
|
|
1720
2149
|
case "top_cities": {
|
|
1721
2150
|
const rows = await this.collection.aggregate([
|
|
1722
|
-
{
|
|
2151
|
+
...matchStages({ city: { $ne: null } }),
|
|
1723
2152
|
{ $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
|
|
1724
2153
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1725
2154
|
{ $sort: { value: -1 } },
|
|
@@ -1731,7 +2160,7 @@ var MongoDBAdapter = class {
|
|
|
1731
2160
|
}
|
|
1732
2161
|
case "top_events": {
|
|
1733
2162
|
const rows = await this.collection.aggregate([
|
|
1734
|
-
{
|
|
2163
|
+
...matchStages({ type: "event", event_name: { $ne: null } }),
|
|
1735
2164
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1736
2165
|
{ $sort: { value: -1 } },
|
|
1737
2166
|
{ $limit: limit }
|
|
@@ -1748,7 +2177,7 @@ var MongoDBAdapter = class {
|
|
|
1748
2177
|
break;
|
|
1749
2178
|
}
|
|
1750
2179
|
const rows = await this.collection.aggregate([
|
|
1751
|
-
{
|
|
2180
|
+
...matchStages({ type: "event", event_name: { $in: conversionEvents } }),
|
|
1752
2181
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1753
2182
|
{ $sort: { value: -1 } },
|
|
1754
2183
|
{ $limit: limit }
|
|
@@ -1759,7 +2188,7 @@ var MongoDBAdapter = class {
|
|
|
1759
2188
|
}
|
|
1760
2189
|
case "top_exit_pages": {
|
|
1761
2190
|
const rows = await this.collection.aggregate([
|
|
1762
|
-
{
|
|
2191
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1763
2192
|
{ $sort: { timestamp: 1 } },
|
|
1764
2193
|
{ $group: { _id: "$session_id", url: { $last: "$url" } } },
|
|
1765
2194
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
@@ -1772,7 +2201,7 @@ var MongoDBAdapter = class {
|
|
|
1772
2201
|
}
|
|
1773
2202
|
case "top_transitions": {
|
|
1774
2203
|
const rows = await this.collection.aggregate([
|
|
1775
|
-
{
|
|
2204
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1776
2205
|
{
|
|
1777
2206
|
$setWindowFields: {
|
|
1778
2207
|
partitionBy: "$session_id",
|
|
@@ -1793,7 +2222,7 @@ var MongoDBAdapter = class {
|
|
|
1793
2222
|
}
|
|
1794
2223
|
case "top_scroll_pages": {
|
|
1795
2224
|
const rows = await this.collection.aggregate([
|
|
1796
|
-
{
|
|
2225
|
+
...matchStages({ type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } }),
|
|
1797
2226
|
{ $group: { _id: "$page_path", value: { $sum: 1 } } },
|
|
1798
2227
|
{ $sort: { value: -1 } },
|
|
1799
2228
|
{ $limit: limit }
|
|
@@ -1804,15 +2233,11 @@ var MongoDBAdapter = class {
|
|
|
1804
2233
|
}
|
|
1805
2234
|
case "top_button_clicks": {
|
|
1806
2235
|
const rows = await this.collection.aggregate([
|
|
1807
|
-
{
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
event_subtype: "button_click",
|
|
1813
|
-
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
1814
|
-
}
|
|
1815
|
-
},
|
|
2236
|
+
...matchStages({
|
|
2237
|
+
type: "event",
|
|
2238
|
+
event_subtype: "button_click",
|
|
2239
|
+
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
2240
|
+
}),
|
|
1816
2241
|
{ $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
|
|
1817
2242
|
{ $sort: { value: -1 } },
|
|
1818
2243
|
{ $limit: limit }
|
|
@@ -1823,15 +2248,11 @@ var MongoDBAdapter = class {
|
|
|
1823
2248
|
}
|
|
1824
2249
|
case "top_link_targets": {
|
|
1825
2250
|
const rows = await this.collection.aggregate([
|
|
1826
|
-
{
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
1832
|
-
target_url_path: { $ne: null }
|
|
1833
|
-
}
|
|
1834
|
-
},
|
|
2251
|
+
...matchStages({
|
|
2252
|
+
type: "event",
|
|
2253
|
+
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
2254
|
+
target_url_path: { $ne: null }
|
|
2255
|
+
}),
|
|
1835
2256
|
{ $group: { _id: "$target_url_path", value: { $sum: 1 } } },
|
|
1836
2257
|
{ $sort: { value: -1 } },
|
|
1837
2258
|
{ $limit: limit }
|
|
@@ -1842,7 +2263,7 @@ var MongoDBAdapter = class {
|
|
|
1842
2263
|
}
|
|
1843
2264
|
case "top_devices": {
|
|
1844
2265
|
const rows = await this.collection.aggregate([
|
|
1845
|
-
{
|
|
2266
|
+
...matchStages({ device_type: { $ne: null } }),
|
|
1846
2267
|
{ $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
|
|
1847
2268
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1848
2269
|
{ $sort: { value: -1 } },
|
|
@@ -1854,7 +2275,7 @@ var MongoDBAdapter = class {
|
|
|
1854
2275
|
}
|
|
1855
2276
|
case "top_browsers": {
|
|
1856
2277
|
const rows = await this.collection.aggregate([
|
|
1857
|
-
{
|
|
2278
|
+
...matchStages({ browser: { $ne: null } }),
|
|
1858
2279
|
{ $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
|
|
1859
2280
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1860
2281
|
{ $sort: { value: -1 } },
|
|
@@ -1866,7 +2287,7 @@ var MongoDBAdapter = class {
|
|
|
1866
2287
|
}
|
|
1867
2288
|
case "top_os": {
|
|
1868
2289
|
const rows = await this.collection.aggregate([
|
|
1869
|
-
{
|
|
2290
|
+
...matchStages({ os: { $ne: null } }),
|
|
1870
2291
|
{ $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
|
|
1871
2292
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1872
2293
|
{ $sort: { value: -1 } },
|
|
@@ -1876,10 +2297,47 @@ var MongoDBAdapter = class {
|
|
|
1876
2297
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1877
2298
|
break;
|
|
1878
2299
|
}
|
|
2300
|
+
case "top_os_versions": {
|
|
2301
|
+
const rows = await this.collection.aggregate([
|
|
2302
|
+
...matchStages({ os: { $ne: null }, os_version: { $ne: null } }),
|
|
2303
|
+
{ $group: { _id: { $concat: ["$os", " ", "$os_version"] }, value: { $addToSet: "$visitor_id" } } },
|
|
2304
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2305
|
+
{ $sort: { value: -1 } },
|
|
2306
|
+
{ $limit: limit }
|
|
2307
|
+
]).toArray();
|
|
2308
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2309
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2310
|
+
break;
|
|
2311
|
+
}
|
|
2312
|
+
case "top_device_models": {
|
|
2313
|
+
const rows = await this.collection.aggregate([
|
|
2314
|
+
...matchStages({ device_model: { $ne: null } }),
|
|
2315
|
+
{ $group: { _id: { $trim: { input: { $concat: [{ $ifNull: ["$device_brand", ""] }, " ", "$device_model"] } } }, value: { $addToSet: "$visitor_id" } } },
|
|
2316
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2317
|
+
{ $sort: { value: -1 } },
|
|
2318
|
+
{ $limit: limit }
|
|
2319
|
+
]).toArray();
|
|
2320
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2321
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2322
|
+
break;
|
|
2323
|
+
}
|
|
2324
|
+
case "top_app_versions": {
|
|
2325
|
+
const rows = await this.collection.aggregate([
|
|
2326
|
+
...matchStages({ app_version: { $ne: null } }),
|
|
2327
|
+
{ $group: { _id: "$app_version", value: { $addToSet: "$visitor_id" } } },
|
|
2328
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2329
|
+
{ $sort: { value: -1 } },
|
|
2330
|
+
{ $limit: limit }
|
|
2331
|
+
]).toArray();
|
|
2332
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2333
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2334
|
+
break;
|
|
2335
|
+
}
|
|
1879
2336
|
case "top_utm_sources": {
|
|
1880
2337
|
const rows = await this.collection.aggregate([
|
|
1881
|
-
{
|
|
1882
|
-
{ $
|
|
2338
|
+
...matchStages({ utm_source: { $nin: [null, ""] } }),
|
|
2339
|
+
{ $addFields: { _normalized_source: normalizedUtmSourceSwitch() } },
|
|
2340
|
+
{ $group: { _id: "$_normalized_source", value: { $addToSet: "$visitor_id" } } },
|
|
1883
2341
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1884
2342
|
{ $sort: { value: -1 } },
|
|
1885
2343
|
{ $limit: limit }
|
|
@@ -1890,8 +2348,9 @@ var MongoDBAdapter = class {
|
|
|
1890
2348
|
}
|
|
1891
2349
|
case "top_utm_mediums": {
|
|
1892
2350
|
const rows = await this.collection.aggregate([
|
|
1893
|
-
{
|
|
1894
|
-
{ $
|
|
2351
|
+
...matchStages({ utm_medium: { $nin: [null, ""] } }),
|
|
2352
|
+
{ $addFields: { _normalized_medium: normalizedUtmMediumSwitch() } },
|
|
2353
|
+
{ $group: { _id: "$_normalized_medium", value: { $addToSet: "$visitor_id" } } },
|
|
1895
2354
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1896
2355
|
{ $sort: { value: -1 } },
|
|
1897
2356
|
{ $limit: limit }
|
|
@@ -1902,7 +2361,7 @@ var MongoDBAdapter = class {
|
|
|
1902
2361
|
}
|
|
1903
2362
|
case "top_utm_campaigns": {
|
|
1904
2363
|
const rows = await this.collection.aggregate([
|
|
1905
|
-
{
|
|
2364
|
+
...matchStages({ utm_campaign: { $nin: [null, ""] } }),
|
|
1906
2365
|
{ $group: { _id: "$utm_campaign", value: { $addToSet: "$visitor_id" } } },
|
|
1907
2366
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1908
2367
|
{ $sort: { value: -1 } },
|
|
@@ -1912,6 +2371,43 @@ var MongoDBAdapter = class {
|
|
|
1912
2371
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1913
2372
|
break;
|
|
1914
2373
|
}
|
|
2374
|
+
case "top_utm_terms": {
|
|
2375
|
+
const rows = await this.collection.aggregate([
|
|
2376
|
+
...matchStages({ utm_term: { $nin: [null, ""] } }),
|
|
2377
|
+
{ $group: { _id: "$utm_term", value: { $addToSet: "$visitor_id" } } },
|
|
2378
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2379
|
+
{ $sort: { value: -1 } },
|
|
2380
|
+
{ $limit: limit }
|
|
2381
|
+
]).toArray();
|
|
2382
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2383
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2384
|
+
break;
|
|
2385
|
+
}
|
|
2386
|
+
case "top_utm_contents": {
|
|
2387
|
+
const rows = await this.collection.aggregate([
|
|
2388
|
+
...matchStages({ utm_content: { $nin: [null, ""] } }),
|
|
2389
|
+
{ $group: { _id: "$utm_content", value: { $addToSet: "$visitor_id" } } },
|
|
2390
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2391
|
+
{ $sort: { value: -1 } },
|
|
2392
|
+
{ $limit: limit }
|
|
2393
|
+
]).toArray();
|
|
2394
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2395
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2396
|
+
break;
|
|
2397
|
+
}
|
|
2398
|
+
case "top_channels": {
|
|
2399
|
+
const rows = await this.collection.aggregate([
|
|
2400
|
+
...matchStages(),
|
|
2401
|
+
{ $addFields: { _channel: channelClassificationSwitch() } },
|
|
2402
|
+
{ $group: { _id: "$_channel", value: { $addToSet: "$visitor_id" } } },
|
|
2403
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2404
|
+
{ $sort: { value: -1 } },
|
|
2405
|
+
{ $limit: limit }
|
|
2406
|
+
]).toArray();
|
|
2407
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2408
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2409
|
+
break;
|
|
2410
|
+
}
|
|
1915
2411
|
}
|
|
1916
2412
|
const result = { metric: q.metric, period, data, total };
|
|
1917
2413
|
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
@@ -2374,7 +2870,18 @@ var MongoDBAdapter = class {
|
|
|
2374
2870
|
userId: doc.user_id ?? void 0,
|
|
2375
2871
|
traits: doc.traits ?? void 0,
|
|
2376
2872
|
geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
|
|
2377
|
-
device: doc.device_type ? {
|
|
2873
|
+
device: doc.device_type ? {
|
|
2874
|
+
type: doc.device_type,
|
|
2875
|
+
browser: doc.browser ?? "",
|
|
2876
|
+
os: doc.os ?? "",
|
|
2877
|
+
osVersion: doc.os_version ?? void 0,
|
|
2878
|
+
deviceModel: doc.device_model ?? void 0,
|
|
2879
|
+
deviceBrand: doc.device_brand ?? void 0,
|
|
2880
|
+
appVersion: doc.app_version ?? void 0,
|
|
2881
|
+
appBuild: doc.app_build ?? void 0,
|
|
2882
|
+
sdkName: doc.sdk_name ?? void 0,
|
|
2883
|
+
sdkVersion: doc.sdk_version ?? void 0
|
|
2884
|
+
} : void 0,
|
|
2378
2885
|
language: doc.language ?? void 0,
|
|
2379
2886
|
utm: doc.utm_source ? {
|
|
2380
2887
|
source: doc.utm_source ?? void 0,
|
|
@@ -2392,6 +2899,7 @@ var MongoDBAdapter = class {
|
|
|
2392
2899
|
site_id: generateSiteId(),
|
|
2393
2900
|
secret_key: generateSecretKey(),
|
|
2394
2901
|
name: data.name,
|
|
2902
|
+
type: data.type ?? "web",
|
|
2395
2903
|
domain: data.domain ?? null,
|
|
2396
2904
|
allowed_origins: data.allowedOrigins ?? null,
|
|
2397
2905
|
conversion_events: data.conversionEvents ?? null,
|
|
@@ -2416,6 +2924,7 @@ var MongoDBAdapter = class {
|
|
|
2416
2924
|
async updateSite(siteId, data) {
|
|
2417
2925
|
const updates = { updated_at: /* @__PURE__ */ new Date() };
|
|
2418
2926
|
if (data.name !== void 0) updates.name = data.name;
|
|
2927
|
+
if (data.type !== void 0) updates.type = data.type;
|
|
2419
2928
|
if (data.domain !== void 0) updates.domain = data.domain || null;
|
|
2420
2929
|
if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
|
|
2421
2930
|
if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
|
|
@@ -2447,6 +2956,7 @@ var MongoDBAdapter = class {
|
|
|
2447
2956
|
siteId: doc.site_id,
|
|
2448
2957
|
secretKey: doc.secret_key,
|
|
2449
2958
|
name: doc.name,
|
|
2959
|
+
type: doc.type ?? "web",
|
|
2450
2960
|
domain: doc.domain ?? void 0,
|
|
2451
2961
|
allowedOrigins: doc.allowed_origins ?? void 0,
|
|
2452
2962
|
conversionEvents: doc.conversion_events ?? void 0,
|
|
@@ -2715,9 +3225,26 @@ async function createCollector(config) {
|
|
|
2715
3225
|
return false;
|
|
2716
3226
|
}
|
|
2717
3227
|
function enrichEvents(events, ip, userAgent) {
|
|
2718
|
-
const
|
|
3228
|
+
const uaDevice = parseUserAgent(userAgent);
|
|
2719
3229
|
return events.map((event) => {
|
|
2720
3230
|
const geo = resolveGeo(ip, event.timezone);
|
|
3231
|
+
let device;
|
|
3232
|
+
if (event.mobile?.platform) {
|
|
3233
|
+
device = {
|
|
3234
|
+
type: "mobile",
|
|
3235
|
+
browser: "App",
|
|
3236
|
+
os: event.mobile.platform === "ios" ? "iOS" : "Android",
|
|
3237
|
+
osVersion: event.mobile.osVersion,
|
|
3238
|
+
deviceModel: event.mobile.deviceModel,
|
|
3239
|
+
deviceBrand: event.mobile.deviceBrand,
|
|
3240
|
+
appVersion: event.mobile.appVersion,
|
|
3241
|
+
appBuild: event.mobile.appBuild,
|
|
3242
|
+
sdkName: event.mobile.sdkName,
|
|
3243
|
+
sdkVersion: event.mobile.sdkVersion
|
|
3244
|
+
};
|
|
3245
|
+
} else {
|
|
3246
|
+
device = uaDevice;
|
|
3247
|
+
}
|
|
2721
3248
|
return { ...event, ip, geo, device };
|
|
2722
3249
|
});
|
|
2723
3250
|
}
|
|
@@ -2777,6 +3304,22 @@ async function createCollector(config) {
|
|
|
2777
3304
|
}
|
|
2778
3305
|
return req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || "";
|
|
2779
3306
|
}
|
|
3307
|
+
function extractRequestHostname(req) {
|
|
3308
|
+
const headerValue = (value) => {
|
|
3309
|
+
if (Array.isArray(value)) return value[0];
|
|
3310
|
+
if (typeof value === "string") return value;
|
|
3311
|
+
return void 0;
|
|
3312
|
+
};
|
|
3313
|
+
const origin = headerValue(req.headers?.origin);
|
|
3314
|
+
const referer = headerValue(req.headers?.referer) || headerValue(req.headers?.referrer);
|
|
3315
|
+
const raw = origin ?? referer;
|
|
3316
|
+
if (!raw || raw === "null") return void 0;
|
|
3317
|
+
try {
|
|
3318
|
+
return new URL(raw).hostname.toLowerCase();
|
|
3319
|
+
} catch {
|
|
3320
|
+
return void 0;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
2780
3323
|
function handler() {
|
|
2781
3324
|
return async (req, res) => {
|
|
2782
3325
|
res.setHeader?.("Access-Control-Allow-Origin", "*");
|
|
@@ -2802,6 +3345,12 @@ async function createCollector(config) {
|
|
|
2802
3345
|
sendJson(res, 400, { ok: false, error: "Too many events (max 100)" });
|
|
2803
3346
|
return;
|
|
2804
3347
|
}
|
|
3348
|
+
const siteIds = new Set(payload.events.map((event) => event.siteId).filter(Boolean));
|
|
3349
|
+
if (siteIds.size !== 1) {
|
|
3350
|
+
sendJson(res, 200, { ok: true });
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
const siteId = Array.from(siteIds)[0];
|
|
2805
3354
|
const userAgent = req.headers?.["user-agent"] || "";
|
|
2806
3355
|
if (isBot(userAgent)) {
|
|
2807
3356
|
sendJson(res, 200, { ok: true });
|
|
@@ -2809,26 +3358,15 @@ async function createCollector(config) {
|
|
|
2809
3358
|
}
|
|
2810
3359
|
const ip = extractIp(req);
|
|
2811
3360
|
const enriched = enrichEvents(payload.events, ip, userAgent);
|
|
2812
|
-
const
|
|
2813
|
-
if (
|
|
2814
|
-
const
|
|
2815
|
-
if (
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
return allowed.has(hostname);
|
|
2822
|
-
} catch {
|
|
2823
|
-
return true;
|
|
2824
|
-
}
|
|
2825
|
-
});
|
|
2826
|
-
if (filtered.length === 0) {
|
|
2827
|
-
sendJson(res, 200, { ok: true });
|
|
2828
|
-
return;
|
|
2829
|
-
}
|
|
2830
|
-
await processIdentity(filtered);
|
|
2831
|
-
await db.insertEvents(filtered);
|
|
3361
|
+
const site = await db.getSite(siteId);
|
|
3362
|
+
if (site?.allowedOrigins && site.allowedOrigins.length > 0) {
|
|
3363
|
+
const requestHostname = extractRequestHostname(req);
|
|
3364
|
+
if (!requestHostname) {
|
|
3365
|
+
sendJson(res, 200, { ok: true });
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
const allowed = new Set(site.allowedOrigins.map((h) => h.toLowerCase()));
|
|
3369
|
+
if (!allowed.has(requestHostname)) {
|
|
2832
3370
|
sendJson(res, 200, { ok: true });
|
|
2833
3371
|
return;
|
|
2834
3372
|
}
|
|
@@ -2868,7 +3406,8 @@ async function createCollector(config) {
|
|
|
2868
3406
|
dateFrom: params.dateFrom,
|
|
2869
3407
|
dateTo: params.dateTo,
|
|
2870
3408
|
granularity: q.granularity,
|
|
2871
|
-
filters: q.filters ? JSON.parse(q.filters) : void 0
|
|
3409
|
+
filters: q.filters ? JSON.parse(q.filters) : void 0,
|
|
3410
|
+
timezone: params.timezone
|
|
2872
3411
|
};
|
|
2873
3412
|
if (tsParams.metric === "conversions") {
|
|
2874
3413
|
const site = await db.getSite(params.siteId);
|
|
@@ -3170,19 +3709,6 @@ async function parseBody(req) {
|
|
|
3170
3709
|
req.on("error", reject);
|
|
3171
3710
|
});
|
|
3172
3711
|
}
|
|
3173
|
-
function extractQueryParams(req) {
|
|
3174
|
-
const q = req.query ?? Object.fromEntries(new URL(req.url, "http://localhost").searchParams);
|
|
3175
|
-
return {
|
|
3176
|
-
siteId: q.siteId,
|
|
3177
|
-
metric: q.metric,
|
|
3178
|
-
period: q.period,
|
|
3179
|
-
dateFrom: q.dateFrom,
|
|
3180
|
-
dateTo: q.dateTo,
|
|
3181
|
-
limit: q.limit ? parseInt(q.limit, 10) : void 0,
|
|
3182
|
-
filters: q.filters ? JSON.parse(q.filters) : void 0,
|
|
3183
|
-
compare: q.compare === "true" || q.compare === "1"
|
|
3184
|
-
};
|
|
3185
|
-
}
|
|
3186
3712
|
function sendJson(res, status, body) {
|
|
3187
3713
|
if (typeof res.status === "function" && typeof res.json === "function") {
|
|
3188
3714
|
res.status(status).json(body);
|