@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.js
CHANGED
|
@@ -3,6 +3,36 @@ import { createClient } from "@clickhouse/client";
|
|
|
3
3
|
|
|
4
4
|
// src/adapters/utils.ts
|
|
5
5
|
import { randomBytes } from "crypto";
|
|
6
|
+
function getTimezoneOffsetMs(date, timezone) {
|
|
7
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
8
|
+
timeZone: timezone,
|
|
9
|
+
year: "numeric",
|
|
10
|
+
month: "2-digit",
|
|
11
|
+
day: "2-digit",
|
|
12
|
+
hour: "2-digit",
|
|
13
|
+
minute: "2-digit",
|
|
14
|
+
second: "2-digit",
|
|
15
|
+
hour12: false
|
|
16
|
+
});
|
|
17
|
+
const parts = formatter.formatToParts(date);
|
|
18
|
+
const get = (type) => parts.find((p) => p.type === type).value;
|
|
19
|
+
const y = parseInt(get("year"));
|
|
20
|
+
const m = parseInt(get("month")) - 1;
|
|
21
|
+
const d = parseInt(get("day"));
|
|
22
|
+
const h = parseInt(get("hour") === "24" ? "0" : get("hour"));
|
|
23
|
+
const mi = parseInt(get("minute"));
|
|
24
|
+
const s = parseInt(get("second"));
|
|
25
|
+
return Date.UTC(y, m, d, h, mi, s) - date.getTime();
|
|
26
|
+
}
|
|
27
|
+
function toUTCDate(value) {
|
|
28
|
+
if (value instanceof Date) return value;
|
|
29
|
+
if (typeof value === "number") return new Date(value);
|
|
30
|
+
const s = String(value).trim();
|
|
31
|
+
if (s.length >= 10 && !s.endsWith("Z") && !/[+-]\d{2}:?\d{2}$/.test(s)) {
|
|
32
|
+
return /* @__PURE__ */ new Date(s.replace(" ", "T") + "Z");
|
|
33
|
+
}
|
|
34
|
+
return new Date(s);
|
|
35
|
+
}
|
|
6
36
|
function resolvePeriod(q) {
|
|
7
37
|
const now = /* @__PURE__ */ new Date();
|
|
8
38
|
const period = q.period ?? "7d";
|
|
@@ -69,60 +99,66 @@ function granularityToDateFormat(g) {
|
|
|
69
99
|
return "%Y-%m";
|
|
70
100
|
}
|
|
71
101
|
}
|
|
72
|
-
function fillBuckets(from, to, granularity, dateFormat, rows) {
|
|
102
|
+
function fillBuckets(from, to, granularity, dateFormat, rows, timezone) {
|
|
73
103
|
const map = new Map(rows.map((r) => [r._id, r.value]));
|
|
74
104
|
const points = [];
|
|
75
|
-
const
|
|
105
|
+
const fromOffset = timezone ? getTimezoneOffsetMs(from, timezone) : 0;
|
|
106
|
+
const toOffset = timezone ? getTimezoneOffsetMs(to, timezone) : 0;
|
|
107
|
+
const current = new Date(from.getTime() + fromOffset);
|
|
108
|
+
const toWall = new Date(to.getTime() + toOffset);
|
|
76
109
|
if (granularity === "hour") {
|
|
77
|
-
current.
|
|
110
|
+
current.setUTCMinutes(0, 0, 0);
|
|
78
111
|
} else if (granularity === "day") {
|
|
79
|
-
current.
|
|
112
|
+
current.setUTCHours(0, 0, 0, 0);
|
|
80
113
|
} else if (granularity === "week") {
|
|
81
|
-
const day = current.
|
|
114
|
+
const day = current.getUTCDay();
|
|
82
115
|
const diff = day === 0 ? -6 : 1 - day;
|
|
83
|
-
current.
|
|
84
|
-
current.
|
|
116
|
+
current.setUTCDate(current.getUTCDate() + diff);
|
|
117
|
+
current.setUTCHours(0, 0, 0, 0);
|
|
85
118
|
} else if (granularity === "month") {
|
|
86
|
-
current.
|
|
87
|
-
current.
|
|
119
|
+
current.setUTCDate(1);
|
|
120
|
+
current.setUTCHours(0, 0, 0, 0);
|
|
88
121
|
}
|
|
89
|
-
while (current <=
|
|
122
|
+
while (current <= toWall) {
|
|
90
123
|
const key = formatDateBucket(current, dateFormat);
|
|
91
|
-
|
|
124
|
+
const approxUtc = new Date(current.getTime() - fromOffset);
|
|
125
|
+
const exactOffset = timezone ? getTimezoneOffsetMs(approxUtc, timezone) : 0;
|
|
126
|
+
const realUtc = new Date(current.getTime() - exactOffset);
|
|
127
|
+
points.push({ date: realUtc.toISOString(), value: map.get(key) ?? 0 });
|
|
92
128
|
if (granularity === "hour") {
|
|
93
|
-
current.
|
|
129
|
+
current.setUTCHours(current.getUTCHours() + 1);
|
|
94
130
|
} else if (granularity === "day") {
|
|
95
|
-
current.
|
|
131
|
+
current.setUTCDate(current.getUTCDate() + 1);
|
|
96
132
|
} else if (granularity === "week") {
|
|
97
|
-
current.
|
|
133
|
+
current.setUTCDate(current.getUTCDate() + 7);
|
|
98
134
|
} else if (granularity === "month") {
|
|
99
|
-
current.
|
|
135
|
+
current.setUTCMonth(current.getUTCMonth() + 1);
|
|
100
136
|
}
|
|
101
137
|
}
|
|
102
138
|
return points;
|
|
103
139
|
}
|
|
104
140
|
function formatDateBucket(date, format) {
|
|
105
|
-
const y = date.
|
|
106
|
-
const m = String(date.
|
|
107
|
-
const d = String(date.
|
|
108
|
-
const h = String(date.
|
|
141
|
+
const y = date.getUTCFullYear();
|
|
142
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
143
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
144
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
109
145
|
if (format === "%Y-%m-%dT%H:00") return `${y}-${m}-${d}T${h}:00`;
|
|
110
146
|
if (format === "%Y-%m-%d") return `${y}-${m}-${d}`;
|
|
111
147
|
if (format === "%Y-%m") return `${y}-${m}`;
|
|
112
148
|
if (format === "%G-W%V") {
|
|
113
|
-
const jan4 = new Date(y, 0, 4);
|
|
114
|
-
const dayOfYear = Math.ceil((date.getTime() -
|
|
115
|
-
const jan4Day = jan4.
|
|
149
|
+
const jan4 = new Date(Date.UTC(y, 0, 4));
|
|
150
|
+
const dayOfYear = Math.ceil((date.getTime() - Date.UTC(y, 0, 1)) / 864e5) + 1;
|
|
151
|
+
const jan4Day = jan4.getUTCDay() || 7;
|
|
116
152
|
const weekNum = Math.ceil((dayOfYear + jan4Day - 1) / 7);
|
|
117
153
|
return `${y}-W${String(weekNum).padStart(2, "0")}`;
|
|
118
154
|
}
|
|
119
155
|
return date.toISOString();
|
|
120
156
|
}
|
|
121
157
|
function getISOWeek(date) {
|
|
122
|
-
const y = date.
|
|
123
|
-
const jan4 = new Date(y, 0, 4);
|
|
124
|
-
const dayOfYear = Math.ceil((date.getTime() -
|
|
125
|
-
const jan4Day = jan4.
|
|
158
|
+
const y = date.getUTCFullYear();
|
|
159
|
+
const jan4 = new Date(Date.UTC(y, 0, 4));
|
|
160
|
+
const dayOfYear = Math.ceil((date.getTime() - Date.UTC(y, 0, 1)) / 864e5) + 1;
|
|
161
|
+
const jan4Day = jan4.getUTCDay() || 7;
|
|
126
162
|
const weekNum = Math.ceil((dayOfYear + jan4Day - 1) / 7);
|
|
127
163
|
return `${y}-W${String(weekNum).padStart(2, "0")}`;
|
|
128
164
|
}
|
|
@@ -137,6 +173,38 @@ function generateSecretKey() {
|
|
|
137
173
|
return `sk_${randomBytes(32).toString("hex")}`;
|
|
138
174
|
}
|
|
139
175
|
|
|
176
|
+
// src/query-helpers.ts
|
|
177
|
+
function isValidTimezone(tz) {
|
|
178
|
+
try {
|
|
179
|
+
Intl.DateTimeFormat(void 0, { timeZone: tz });
|
|
180
|
+
return true;
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function extractQueryParams(req) {
|
|
186
|
+
const q = req.query ?? Object.fromEntries(new URL(req.url, "http://localhost").searchParams);
|
|
187
|
+
let timezone;
|
|
188
|
+
if (typeof q.timezone === "string" && q.timezone) {
|
|
189
|
+
if (isValidTimezone(q.timezone)) {
|
|
190
|
+
timezone = q.timezone;
|
|
191
|
+
} else {
|
|
192
|
+
console.warn(`[litemetrics] Invalid timezone "${q.timezone}", falling back to UTC`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
siteId: q.siteId,
|
|
197
|
+
metric: q.metric,
|
|
198
|
+
period: q.period,
|
|
199
|
+
dateFrom: q.dateFrom,
|
|
200
|
+
dateTo: q.dateTo,
|
|
201
|
+
limit: q.limit ? parseInt(q.limit, 10) : void 0,
|
|
202
|
+
filters: q.filters ? JSON.parse(q.filters) : void 0,
|
|
203
|
+
compare: q.compare === "true" || q.compare === "1",
|
|
204
|
+
timezone
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
140
208
|
// src/adapters/clickhouse.ts
|
|
141
209
|
var EVENTS_TABLE = "litemetrics_events";
|
|
142
210
|
var SITES_TABLE = "litemetrics_sites";
|
|
@@ -190,6 +258,13 @@ CREATE TABLE IF NOT EXISTS ${EVENTS_TABLE} (
|
|
|
190
258
|
utm_term Nullable(String),
|
|
191
259
|
utm_content Nullable(String),
|
|
192
260
|
ip Nullable(String),
|
|
261
|
+
os_version LowCardinality(Nullable(String)),
|
|
262
|
+
device_model LowCardinality(Nullable(String)),
|
|
263
|
+
device_brand LowCardinality(Nullable(String)),
|
|
264
|
+
app_version LowCardinality(Nullable(String)),
|
|
265
|
+
app_build Nullable(String),
|
|
266
|
+
sdk_name LowCardinality(Nullable(String)),
|
|
267
|
+
sdk_version LowCardinality(Nullable(String)),
|
|
193
268
|
created_at DateTime64(3) DEFAULT now64(3)
|
|
194
269
|
) ENGINE = MergeTree()
|
|
195
270
|
PARTITION BY toYYYYMM(timestamp)
|
|
@@ -201,6 +276,7 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
201
276
|
site_id String,
|
|
202
277
|
secret_key String,
|
|
203
278
|
name String,
|
|
279
|
+
type LowCardinality(Nullable(String)) DEFAULT 'web',
|
|
204
280
|
domain Nullable(String),
|
|
205
281
|
allowed_origins Nullable(String),
|
|
206
282
|
conversion_events Nullable(String),
|
|
@@ -213,8 +289,74 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
213
289
|
SETTINGS index_granularity = 8192
|
|
214
290
|
`;
|
|
215
291
|
function toCHDateTime(d) {
|
|
216
|
-
const
|
|
217
|
-
|
|
292
|
+
const date = d instanceof Date ? d : toUTCDate(d);
|
|
293
|
+
const y = date.getUTCFullYear();
|
|
294
|
+
const mo = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
295
|
+
const da = String(date.getUTCDate()).padStart(2, "0");
|
|
296
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
297
|
+
const mi = String(date.getUTCMinutes()).padStart(2, "0");
|
|
298
|
+
const s = String(date.getUTCSeconds()).padStart(2, "0");
|
|
299
|
+
const ms = String(date.getUTCMilliseconds()).padStart(3, "0");
|
|
300
|
+
return `${y}-${mo}-${da} ${h}:${mi}:${s}.${ms}`;
|
|
301
|
+
}
|
|
302
|
+
function normalizedUtmSourceExpr() {
|
|
303
|
+
return `multiIf(
|
|
304
|
+
lower(utm_source) IN ('ig','instagram','instagram.com'), 'Instagram',
|
|
305
|
+
lower(utm_source) IN ('fb','facebook','facebook.com','fb.com'), 'Facebook',
|
|
306
|
+
lower(utm_source) IN ('tw','twitter','twitter.com','x','x.com','t.co'), 'X (Twitter)',
|
|
307
|
+
lower(utm_source) IN ('li','linkedin','linkedin.com'), 'LinkedIn',
|
|
308
|
+
lower(utm_source) IN ('yt','youtube','youtube.com'), 'YouTube',
|
|
309
|
+
lower(utm_source) IN ('goog','google','google.com'), 'Google',
|
|
310
|
+
lower(utm_source) IN ('gh','github','github.com'), 'GitHub',
|
|
311
|
+
lower(utm_source) IN ('reddit','reddit.com'), 'Reddit',
|
|
312
|
+
lower(utm_source) IN ('pinterest','pinterest.com'), 'Pinterest',
|
|
313
|
+
lower(utm_source) IN ('tiktok','tiktok.com'), 'TikTok',
|
|
314
|
+
lower(utm_source) IN ('openai','chatgpt','chat.openai.com'), 'OpenAI',
|
|
315
|
+
lower(utm_source) IN ('perplexity','perplexity.ai'), 'Perplexity',
|
|
316
|
+
utm_source
|
|
317
|
+
)`;
|
|
318
|
+
}
|
|
319
|
+
function normalizedUtmMediumExpr() {
|
|
320
|
+
return `multiIf(
|
|
321
|
+
lower(utm_medium) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid'), 'Paid',
|
|
322
|
+
lower(utm_medium) IN ('organic'), 'Organic',
|
|
323
|
+
lower(utm_medium) IN ('social','social-media','social_media'), 'Social',
|
|
324
|
+
lower(utm_medium) IN ('email','e-mail','e_mail'), 'Email',
|
|
325
|
+
lower(utm_medium) IN ('display','banner','cpm'), 'Display',
|
|
326
|
+
lower(utm_medium) IN ('affiliate'), 'Affiliate',
|
|
327
|
+
lower(utm_medium) IN ('referral'), 'Referral',
|
|
328
|
+
utm_medium
|
|
329
|
+
)`;
|
|
330
|
+
}
|
|
331
|
+
function channelClassificationExpr() {
|
|
332
|
+
return `multiIf(
|
|
333
|
+
lower(ifNull(utm_medium,'')) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')
|
|
334
|
+
AND (lower(ifNull(utm_source,'')) IN ('google','goog','bing','yahoo','duckduckgo','ecosia','baidu','yandex')
|
|
335
|
+
OR multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['google','bing','yahoo','duckduckgo','ecosia','baidu','yandex','search.brave']) > 0),
|
|
336
|
+
'Paid Search',
|
|
337
|
+
lower(ifNull(utm_medium,'')) IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')
|
|
338
|
+
AND (lower(ifNull(utm_source,'')) IN ('instagram','ig','facebook','fb','twitter','tw','x','linkedin','li','youtube','yt','tiktok','pinterest','reddit','snapchat')
|
|
339
|
+
OR multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['instagram','facebook','twitter','x.com','t.co','linkedin','youtube','tiktok','pinterest','reddit','snapchat']) > 0),
|
|
340
|
+
'Paid Social',
|
|
341
|
+
lower(ifNull(utm_medium,'')) IN ('email','e-mail','e_mail'),
|
|
342
|
+
'Email',
|
|
343
|
+
lower(ifNull(utm_medium,'')) IN ('display','banner','cpm'),
|
|
344
|
+
'Display',
|
|
345
|
+
lower(ifNull(utm_medium,'')) IN ('affiliate'),
|
|
346
|
+
'Affiliate',
|
|
347
|
+
multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['google','bing','yahoo','duckduckgo','ecosia','baidu','yandex','search.brave']) > 0
|
|
348
|
+
AND (ifNull(utm_medium,'') = '' OR lower(utm_medium) NOT IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')),
|
|
349
|
+
'Organic Search',
|
|
350
|
+
(multiSearchAnyCaseInsensitive(ifNull(referrer,''), ['instagram','facebook','twitter','x.com','t.co','linkedin','youtube','tiktok','pinterest','reddit','snapchat','mastodon','tumblr']) > 0
|
|
351
|
+
OR lower(ifNull(utm_source,'')) IN ('instagram','ig','facebook','fb','twitter','tw','x','linkedin','li','youtube','yt','tiktok','pinterest','reddit','snapchat'))
|
|
352
|
+
AND (ifNull(utm_medium,'') = '' OR lower(utm_medium) NOT IN ('cpc','ppc','paidsearch','paid-search','paid_search','paid')),
|
|
353
|
+
'Organic Social',
|
|
354
|
+
ifNull(referrer,'') != '' AND length(ifNull(referrer,'')) > 0,
|
|
355
|
+
'Referral',
|
|
356
|
+
(ifNull(utm_source,'') != '' OR ifNull(utm_medium,'') != '' OR ifNull(utm_campaign,'') != ''),
|
|
357
|
+
'Other',
|
|
358
|
+
'Direct'
|
|
359
|
+
)`;
|
|
218
360
|
}
|
|
219
361
|
function buildFilterConditions(filters) {
|
|
220
362
|
if (!filters) return { conditions: [], params: {} };
|
|
@@ -226,6 +368,10 @@ function buildFilterConditions(filters) {
|
|
|
226
368
|
"device.type": "device_type",
|
|
227
369
|
"device.browser": "browser",
|
|
228
370
|
"device.os": "os",
|
|
371
|
+
"device.osVersion": "os_version",
|
|
372
|
+
"device.deviceModel": "device_model",
|
|
373
|
+
"device.deviceBrand": "device_brand",
|
|
374
|
+
"device.appVersion": "app_version",
|
|
229
375
|
"utm.source": "utm_source",
|
|
230
376
|
"utm.medium": "utm_medium",
|
|
231
377
|
"utm.campaign": "utm_campaign",
|
|
@@ -242,7 +388,14 @@ function buildFilterConditions(filters) {
|
|
|
242
388
|
const conditions = [];
|
|
243
389
|
const params = {};
|
|
244
390
|
for (const [key, value] of Object.entries(filters)) {
|
|
245
|
-
if (!value
|
|
391
|
+
if (!value) continue;
|
|
392
|
+
if (key === "channel") {
|
|
393
|
+
const paramKey2 = "f_channel";
|
|
394
|
+
conditions.push(`${channelClassificationExpr()} = {${paramKey2}:String}`);
|
|
395
|
+
params[paramKey2] = value;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (!map[key]) continue;
|
|
246
399
|
const paramKey = `f_${key.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
247
400
|
conditions.push(`${map[key]} = {${paramKey}:String}`);
|
|
248
401
|
params[paramKey] = value;
|
|
@@ -273,6 +426,14 @@ var ClickHouseAdapter = class {
|
|
|
273
426
|
await this.client.command({
|
|
274
427
|
query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS conversion_events Nullable(String)`
|
|
275
428
|
});
|
|
429
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS os_version LowCardinality(Nullable(String))` });
|
|
430
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS device_model LowCardinality(Nullable(String))` });
|
|
431
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS device_brand LowCardinality(Nullable(String))` });
|
|
432
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS app_version LowCardinality(Nullable(String))` });
|
|
433
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS app_build Nullable(String)` });
|
|
434
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS sdk_name LowCardinality(Nullable(String))` });
|
|
435
|
+
await this.client.command({ query: `ALTER TABLE ${EVENTS_TABLE} ADD COLUMN IF NOT EXISTS sdk_version LowCardinality(Nullable(String))` });
|
|
436
|
+
await this.client.command({ query: `ALTER TABLE ${SITES_TABLE} ADD COLUMN IF NOT EXISTS type LowCardinality(Nullable(String)) DEFAULT 'web'` });
|
|
276
437
|
}
|
|
277
438
|
async close() {
|
|
278
439
|
await this.client.close();
|
|
@@ -315,7 +476,14 @@ var ClickHouseAdapter = class {
|
|
|
315
476
|
utm_campaign: e.utm?.campaign ?? null,
|
|
316
477
|
utm_term: e.utm?.term ?? null,
|
|
317
478
|
utm_content: e.utm?.content ?? null,
|
|
318
|
-
ip: e.ip ?? null
|
|
479
|
+
ip: e.ip ?? null,
|
|
480
|
+
os_version: e.device?.osVersion ?? null,
|
|
481
|
+
device_model: e.device?.deviceModel ?? null,
|
|
482
|
+
device_brand: e.device?.deviceBrand ?? null,
|
|
483
|
+
app_version: e.device?.appVersion ?? null,
|
|
484
|
+
app_build: e.device?.appBuild ?? null,
|
|
485
|
+
sdk_name: e.device?.sdkName ?? null,
|
|
486
|
+
sdk_version: e.device?.sdkVersion ?? null
|
|
319
487
|
}));
|
|
320
488
|
await this.client.insert({
|
|
321
489
|
table: EVENTS_TABLE,
|
|
@@ -666,15 +834,67 @@ var ClickHouseAdapter = class {
|
|
|
666
834
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
667
835
|
break;
|
|
668
836
|
}
|
|
837
|
+
case "top_os_versions": {
|
|
838
|
+
const rows = await this.queryRows(
|
|
839
|
+
`SELECT concat(os, ' ', ifNull(os_version, '')) AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
840
|
+
WHERE site_id = {siteId:String}
|
|
841
|
+
AND timestamp >= {from:String}
|
|
842
|
+
AND timestamp <= {to:String}
|
|
843
|
+
AND os IS NOT NULL
|
|
844
|
+
AND os_version IS NOT NULL
|
|
845
|
+
${filterSql}
|
|
846
|
+
GROUP BY key
|
|
847
|
+
ORDER BY value DESC
|
|
848
|
+
LIMIT {limit:UInt32}`,
|
|
849
|
+
{ ...params, ...filter.params }
|
|
850
|
+
);
|
|
851
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
852
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
case "top_device_models": {
|
|
856
|
+
const rows = await this.queryRows(
|
|
857
|
+
`SELECT trim(concat(ifNull(device_brand, ''), ' ', device_model)) AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
858
|
+
WHERE site_id = {siteId:String}
|
|
859
|
+
AND timestamp >= {from:String}
|
|
860
|
+
AND timestamp <= {to:String}
|
|
861
|
+
AND device_model IS NOT NULL
|
|
862
|
+
${filterSql}
|
|
863
|
+
GROUP BY key
|
|
864
|
+
ORDER BY value DESC
|
|
865
|
+
LIMIT {limit:UInt32}`,
|
|
866
|
+
{ ...params, ...filter.params }
|
|
867
|
+
);
|
|
868
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
869
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
case "top_app_versions": {
|
|
873
|
+
const rows = await this.queryRows(
|
|
874
|
+
`SELECT app_version AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
875
|
+
WHERE site_id = {siteId:String}
|
|
876
|
+
AND timestamp >= {from:String}
|
|
877
|
+
AND timestamp <= {to:String}
|
|
878
|
+
AND app_version IS NOT NULL
|
|
879
|
+
${filterSql}
|
|
880
|
+
GROUP BY app_version
|
|
881
|
+
ORDER BY value DESC
|
|
882
|
+
LIMIT {limit:UInt32}`,
|
|
883
|
+
{ ...params, ...filter.params }
|
|
884
|
+
);
|
|
885
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
886
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
669
889
|
case "top_utm_sources": {
|
|
670
890
|
const rows = await this.queryRows(
|
|
671
|
-
`SELECT
|
|
891
|
+
`SELECT ${normalizedUtmSourceExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
672
892
|
WHERE site_id = {siteId:String}
|
|
673
893
|
AND timestamp >= {from:String}
|
|
674
894
|
AND timestamp <= {to:String}
|
|
675
895
|
AND utm_source IS NOT NULL AND utm_source != ''
|
|
676
896
|
${filterSql}
|
|
677
|
-
GROUP BY
|
|
897
|
+
GROUP BY key
|
|
678
898
|
ORDER BY value DESC
|
|
679
899
|
LIMIT {limit:UInt32}`,
|
|
680
900
|
{ ...params, ...filter.params }
|
|
@@ -685,13 +905,13 @@ var ClickHouseAdapter = class {
|
|
|
685
905
|
}
|
|
686
906
|
case "top_utm_mediums": {
|
|
687
907
|
const rows = await this.queryRows(
|
|
688
|
-
`SELECT
|
|
908
|
+
`SELECT ${normalizedUtmMediumExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
689
909
|
WHERE site_id = {siteId:String}
|
|
690
910
|
AND timestamp >= {from:String}
|
|
691
911
|
AND timestamp <= {to:String}
|
|
692
912
|
AND utm_medium IS NOT NULL AND utm_medium != ''
|
|
693
913
|
${filterSql}
|
|
694
|
-
GROUP BY
|
|
914
|
+
GROUP BY key
|
|
695
915
|
ORDER BY value DESC
|
|
696
916
|
LIMIT {limit:UInt32}`,
|
|
697
917
|
{ ...params, ...filter.params }
|
|
@@ -717,6 +937,56 @@ var ClickHouseAdapter = class {
|
|
|
717
937
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
718
938
|
break;
|
|
719
939
|
}
|
|
940
|
+
case "top_utm_terms": {
|
|
941
|
+
const rows = await this.queryRows(
|
|
942
|
+
`SELECT utm_term AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
943
|
+
WHERE site_id = {siteId:String}
|
|
944
|
+
AND timestamp >= {from:String}
|
|
945
|
+
AND timestamp <= {to:String}
|
|
946
|
+
AND utm_term IS NOT NULL AND utm_term != ''
|
|
947
|
+
${filterSql}
|
|
948
|
+
GROUP BY utm_term
|
|
949
|
+
ORDER BY value DESC
|
|
950
|
+
LIMIT {limit:UInt32}`,
|
|
951
|
+
{ ...params, ...filter.params }
|
|
952
|
+
);
|
|
953
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
954
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
case "top_utm_contents": {
|
|
958
|
+
const rows = await this.queryRows(
|
|
959
|
+
`SELECT utm_content AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
960
|
+
WHERE site_id = {siteId:String}
|
|
961
|
+
AND timestamp >= {from:String}
|
|
962
|
+
AND timestamp <= {to:String}
|
|
963
|
+
AND utm_content IS NOT NULL AND utm_content != ''
|
|
964
|
+
${filterSql}
|
|
965
|
+
GROUP BY utm_content
|
|
966
|
+
ORDER BY value DESC
|
|
967
|
+
LIMIT {limit:UInt32}`,
|
|
968
|
+
{ ...params, ...filter.params }
|
|
969
|
+
);
|
|
970
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
971
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
case "top_channels": {
|
|
975
|
+
const rows = await this.queryRows(
|
|
976
|
+
`SELECT ${channelClassificationExpr()} AS key, uniq(visitor_id) AS value FROM ${EVENTS_TABLE}
|
|
977
|
+
WHERE site_id = {siteId:String}
|
|
978
|
+
AND timestamp >= {from:String}
|
|
979
|
+
AND timestamp <= {to:String}
|
|
980
|
+
${filterSql}
|
|
981
|
+
GROUP BY key
|
|
982
|
+
ORDER BY value DESC
|
|
983
|
+
LIMIT {limit:UInt32}`,
|
|
984
|
+
{ ...params, ...filter.params }
|
|
985
|
+
);
|
|
986
|
+
data = rows.map((r) => ({ key: r.key, value: Number(r.value) }));
|
|
987
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
720
990
|
}
|
|
721
991
|
const result = { metric: q.metric, period, data, total };
|
|
722
992
|
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
@@ -747,7 +1017,7 @@ var ClickHouseAdapter = class {
|
|
|
747
1017
|
dateTo: params.dateTo
|
|
748
1018
|
});
|
|
749
1019
|
const granularity = params.granularity ?? autoGranularity(period);
|
|
750
|
-
const bucketFn = this.granularityToClickHouseFunc(granularity);
|
|
1020
|
+
const bucketFn = this.granularityToClickHouseFunc(granularity, params.timezone);
|
|
751
1021
|
const dateFormat = granularityToDateFormat(granularity);
|
|
752
1022
|
const filter = buildFilterConditions(params.filters);
|
|
753
1023
|
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
@@ -796,38 +1066,41 @@ var ClickHouseAdapter = class {
|
|
|
796
1066
|
new Date(dateRange.to),
|
|
797
1067
|
granularity,
|
|
798
1068
|
dateFormat,
|
|
799
|
-
mappedRows
|
|
1069
|
+
mappedRows,
|
|
1070
|
+
params.timezone
|
|
800
1071
|
);
|
|
801
1072
|
return { metric: params.metric, granularity, data };
|
|
802
1073
|
}
|
|
803
|
-
granularityToClickHouseFunc(g) {
|
|
1074
|
+
granularityToClickHouseFunc(g, timezone) {
|
|
1075
|
+
const safeTz = timezone && isValidTimezone(timezone) ? timezone : void 0;
|
|
1076
|
+
const tz = safeTz ? `, '${safeTz}'` : "";
|
|
804
1077
|
switch (g) {
|
|
805
1078
|
case "hour":
|
|
806
|
-
return
|
|
1079
|
+
return `toStartOfHour(timestamp${tz})`;
|
|
807
1080
|
case "day":
|
|
808
|
-
return
|
|
1081
|
+
return `toStartOfDay(timestamp${tz})`;
|
|
809
1082
|
case "week":
|
|
810
|
-
return
|
|
1083
|
+
return `toStartOfWeek(timestamp, 1${tz})`;
|
|
811
1084
|
// 1 = Monday
|
|
812
1085
|
case "month":
|
|
813
|
-
return
|
|
1086
|
+
return `toStartOfMonth(timestamp${tz})`;
|
|
814
1087
|
}
|
|
815
1088
|
}
|
|
816
1089
|
convertClickHouseBucket(bucket, granularity) {
|
|
817
|
-
const date =
|
|
818
|
-
const y = date.
|
|
819
|
-
const m = String(date.
|
|
820
|
-
const d = String(date.
|
|
821
|
-
const h = String(date.
|
|
1090
|
+
const date = toUTCDate(bucket);
|
|
1091
|
+
const y = date.getUTCFullYear();
|
|
1092
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
1093
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
1094
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
822
1095
|
switch (granularity) {
|
|
823
1096
|
case "hour":
|
|
824
1097
|
return `${y}-${m}-${d}T${h}:00`;
|
|
825
1098
|
case "day":
|
|
826
1099
|
return `${y}-${m}-${d}`;
|
|
827
1100
|
case "week": {
|
|
828
|
-
const jan4 = new Date(y, 0, 4);
|
|
829
|
-
const dayOfYear = Math.ceil((date.getTime() -
|
|
830
|
-
const jan4Day = jan4.
|
|
1101
|
+
const jan4 = new Date(Date.UTC(y, 0, 4));
|
|
1102
|
+
const dayOfYear = Math.ceil((date.getTime() - Date.UTC(y, 0, 1)) / 864e5) + 1;
|
|
1103
|
+
const jan4Day = jan4.getUTCDay() || 7;
|
|
831
1104
|
const weekNum = Math.ceil((dayOfYear + jan4Day - 1) / 7);
|
|
832
1105
|
return `${y}-W${String(weekNum).padStart(2, "0")}`;
|
|
833
1106
|
}
|
|
@@ -856,7 +1129,7 @@ var ClickHouseAdapter = class {
|
|
|
856
1129
|
);
|
|
857
1130
|
const cohortMap = /* @__PURE__ */ new Map();
|
|
858
1131
|
for (const v of rows) {
|
|
859
|
-
const firstDate =
|
|
1132
|
+
const firstDate = toUTCDate(v.first_event);
|
|
860
1133
|
const cohortWeek = getISOWeek(firstDate);
|
|
861
1134
|
if (!cohortMap.has(cohortWeek)) {
|
|
862
1135
|
cohortMap.set(cohortWeek, { visitors: /* @__PURE__ */ new Set(), weekSets: /* @__PURE__ */ new Map() });
|
|
@@ -864,7 +1137,7 @@ var ClickHouseAdapter = class {
|
|
|
864
1137
|
const cohort = cohortMap.get(cohortWeek);
|
|
865
1138
|
cohort.visitors.add(v.visitor_id);
|
|
866
1139
|
const eventWeeks = (Array.isArray(v.active_weeks) ? v.active_weeks : []).map((w) => {
|
|
867
|
-
const d =
|
|
1140
|
+
const d = toUTCDate(w);
|
|
868
1141
|
return getISOWeek(d);
|
|
869
1142
|
});
|
|
870
1143
|
for (const w of eventWeeks) {
|
|
@@ -1031,8 +1304,8 @@ var ClickHouseAdapter = class {
|
|
|
1031
1304
|
visitorId: String(u.visitor_id),
|
|
1032
1305
|
userId: u.userId ? String(u.userId) : void 0,
|
|
1033
1306
|
traits: this.parseJSON(u.traits),
|
|
1034
|
-
firstSeen:
|
|
1035
|
-
lastSeen:
|
|
1307
|
+
firstSeen: toUTCDate(String(u.firstSeen)).toISOString(),
|
|
1308
|
+
lastSeen: toUTCDate(String(u.lastSeen)).toISOString(),
|
|
1036
1309
|
totalEvents: Number(u.totalEvents),
|
|
1037
1310
|
totalPageviews: Number(u.totalPageviews),
|
|
1038
1311
|
totalSessions: Number(u.totalSessions),
|
|
@@ -1119,7 +1392,7 @@ var ClickHouseAdapter = class {
|
|
|
1119
1392
|
async getMergedUserDetail(siteId, userId, visitorIds) {
|
|
1120
1393
|
const rows = await this.queryRows(
|
|
1121
1394
|
`SELECT
|
|
1122
|
-
anyLast(visitor_id) AS
|
|
1395
|
+
anyLast(visitor_id) AS last_visitor_id,
|
|
1123
1396
|
anyLast(traits) AS traits,
|
|
1124
1397
|
min(timestamp) AS firstSeen,
|
|
1125
1398
|
max(timestamp) AS lastSeen,
|
|
@@ -1151,12 +1424,12 @@ var ClickHouseAdapter = class {
|
|
|
1151
1424
|
if (rows.length === 0) return null;
|
|
1152
1425
|
const u = rows[0];
|
|
1153
1426
|
return {
|
|
1154
|
-
visitorId: String(u.
|
|
1427
|
+
visitorId: String(u.last_visitor_id),
|
|
1155
1428
|
visitorIds,
|
|
1156
1429
|
userId,
|
|
1157
1430
|
traits: this.parseJSON(u.traits),
|
|
1158
|
-
firstSeen:
|
|
1159
|
-
lastSeen:
|
|
1431
|
+
firstSeen: toUTCDate(String(u.firstSeen)).toISOString(),
|
|
1432
|
+
lastSeen: toUTCDate(String(u.lastSeen)).toISOString(),
|
|
1160
1433
|
totalEvents: Number(u.totalEvents),
|
|
1161
1434
|
totalPageviews: Number(u.totalPageviews),
|
|
1162
1435
|
totalSessions: Number(u.totalSessions),
|
|
@@ -1240,6 +1513,7 @@ var ClickHouseAdapter = class {
|
|
|
1240
1513
|
siteId: generateSiteId(),
|
|
1241
1514
|
secretKey: generateSecretKey(),
|
|
1242
1515
|
name: data.name,
|
|
1516
|
+
type: data.type ?? "web",
|
|
1243
1517
|
domain: data.domain,
|
|
1244
1518
|
allowedOrigins: data.allowedOrigins,
|
|
1245
1519
|
conversionEvents: data.conversionEvents,
|
|
@@ -1252,6 +1526,7 @@ var ClickHouseAdapter = class {
|
|
|
1252
1526
|
site_id: site.siteId,
|
|
1253
1527
|
secret_key: site.secretKey,
|
|
1254
1528
|
name: site.name,
|
|
1529
|
+
type: site.type ?? "web",
|
|
1255
1530
|
domain: site.domain ?? null,
|
|
1256
1531
|
allowed_origins: site.allowedOrigins ? JSON.stringify(site.allowedOrigins) : null,
|
|
1257
1532
|
conversion_events: site.conversionEvents ? JSON.stringify(site.conversionEvents) : null,
|
|
@@ -1266,7 +1541,7 @@ var ClickHouseAdapter = class {
|
|
|
1266
1541
|
}
|
|
1267
1542
|
async getSite(siteId) {
|
|
1268
1543
|
const rows = await this.queryRows(
|
|
1269
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1544
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1270
1545
|
FROM ${SITES_TABLE} FINAL
|
|
1271
1546
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1272
1547
|
{ siteId }
|
|
@@ -1275,7 +1550,7 @@ var ClickHouseAdapter = class {
|
|
|
1275
1550
|
}
|
|
1276
1551
|
async getSiteBySecret(secretKey) {
|
|
1277
1552
|
const rows = await this.queryRows(
|
|
1278
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1553
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1279
1554
|
FROM ${SITES_TABLE} FINAL
|
|
1280
1555
|
WHERE secret_key = {secretKey:String} AND is_deleted = 0`,
|
|
1281
1556
|
{ secretKey }
|
|
@@ -1284,7 +1559,7 @@ var ClickHouseAdapter = class {
|
|
|
1284
1559
|
}
|
|
1285
1560
|
async listSites() {
|
|
1286
1561
|
const rows = await this.queryRows(
|
|
1287
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1562
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at
|
|
1288
1563
|
FROM ${SITES_TABLE} FINAL
|
|
1289
1564
|
WHERE is_deleted = 0
|
|
1290
1565
|
ORDER BY created_at DESC`,
|
|
@@ -1294,7 +1569,7 @@ var ClickHouseAdapter = class {
|
|
|
1294
1569
|
}
|
|
1295
1570
|
async updateSite(siteId, data) {
|
|
1296
1571
|
const currentRows = await this.queryRows(
|
|
1297
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
1572
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, updated_at, version
|
|
1298
1573
|
FROM ${SITES_TABLE} FINAL
|
|
1299
1574
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1300
1575
|
{ siteId }
|
|
@@ -1306,6 +1581,7 @@ var ClickHouseAdapter = class {
|
|
|
1306
1581
|
const nowCH = toCHDateTime(now);
|
|
1307
1582
|
const newVersion = Number(current.version) + 1;
|
|
1308
1583
|
const newName = data.name !== void 0 ? data.name : String(current.name);
|
|
1584
|
+
const newType = data.type !== void 0 ? data.type : current.type ? String(current.type) : "web";
|
|
1309
1585
|
const newDomain = data.domain !== void 0 ? data.domain || null : current.domain ? String(current.domain) : null;
|
|
1310
1586
|
const newOrigins = data.allowedOrigins !== void 0 ? data.allowedOrigins.length > 0 ? JSON.stringify(data.allowedOrigins) : null : current.allowed_origins ? String(current.allowed_origins) : null;
|
|
1311
1587
|
const newConversions = data.conversionEvents !== void 0 ? data.conversionEvents.length > 0 ? JSON.stringify(data.conversionEvents) : null : current.conversion_events ? String(current.conversion_events) : null;
|
|
@@ -1315,6 +1591,7 @@ var ClickHouseAdapter = class {
|
|
|
1315
1591
|
site_id: String(current.site_id),
|
|
1316
1592
|
secret_key: String(current.secret_key),
|
|
1317
1593
|
name: newName,
|
|
1594
|
+
type: newType,
|
|
1318
1595
|
domain: newDomain,
|
|
1319
1596
|
allowed_origins: newOrigins,
|
|
1320
1597
|
conversion_events: newConversions,
|
|
@@ -1329,6 +1606,7 @@ var ClickHouseAdapter = class {
|
|
|
1329
1606
|
siteId: String(current.site_id),
|
|
1330
1607
|
secretKey: String(current.secret_key),
|
|
1331
1608
|
name: newName,
|
|
1609
|
+
type: newType,
|
|
1332
1610
|
domain: newDomain ?? void 0,
|
|
1333
1611
|
allowedOrigins: newOrigins ? JSON.parse(newOrigins) : void 0,
|
|
1334
1612
|
conversionEvents: newConversions ? JSON.parse(newConversions) : void 0,
|
|
@@ -1338,7 +1616,7 @@ var ClickHouseAdapter = class {
|
|
|
1338
1616
|
}
|
|
1339
1617
|
async deleteSite(siteId) {
|
|
1340
1618
|
const currentRows = await this.queryRows(
|
|
1341
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
1619
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, version
|
|
1342
1620
|
FROM ${SITES_TABLE} FINAL
|
|
1343
1621
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1344
1622
|
{ siteId }
|
|
@@ -1352,6 +1630,7 @@ var ClickHouseAdapter = class {
|
|
|
1352
1630
|
site_id: String(current.site_id),
|
|
1353
1631
|
secret_key: String(current.secret_key),
|
|
1354
1632
|
name: String(current.name),
|
|
1633
|
+
type: current.type ? String(current.type) : "web",
|
|
1355
1634
|
domain: current.domain ? String(current.domain) : null,
|
|
1356
1635
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1357
1636
|
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
@@ -1366,7 +1645,7 @@ var ClickHouseAdapter = class {
|
|
|
1366
1645
|
}
|
|
1367
1646
|
async regenerateSecret(siteId) {
|
|
1368
1647
|
const currentRows = await this.queryRows(
|
|
1369
|
-
`SELECT site_id, secret_key, name, domain, allowed_origins, conversion_events, created_at, version
|
|
1648
|
+
`SELECT site_id, secret_key, name, type, domain, allowed_origins, conversion_events, created_at, version
|
|
1370
1649
|
FROM ${SITES_TABLE} FINAL
|
|
1371
1650
|
WHERE site_id = {siteId:String} AND is_deleted = 0`,
|
|
1372
1651
|
{ siteId }
|
|
@@ -1383,6 +1662,7 @@ var ClickHouseAdapter = class {
|
|
|
1383
1662
|
site_id: String(current.site_id),
|
|
1384
1663
|
secret_key: newSecret,
|
|
1385
1664
|
name: String(current.name),
|
|
1665
|
+
type: current.type ? String(current.type) : "web",
|
|
1386
1666
|
domain: current.domain ? String(current.domain) : null,
|
|
1387
1667
|
allowed_origins: current.allowed_origins ? String(current.allowed_origins) : null,
|
|
1388
1668
|
conversion_events: current.conversion_events ? String(current.conversion_events) : null,
|
|
@@ -1397,6 +1677,7 @@ var ClickHouseAdapter = class {
|
|
|
1397
1677
|
siteId: String(current.site_id),
|
|
1398
1678
|
secretKey: newSecret,
|
|
1399
1679
|
name: String(current.name),
|
|
1680
|
+
type: current.type ? String(current.type) : "web",
|
|
1400
1681
|
domain: current.domain ? String(current.domain) : void 0,
|
|
1401
1682
|
allowedOrigins: current.allowed_origins ? JSON.parse(String(current.allowed_origins)) : void 0,
|
|
1402
1683
|
conversionEvents: current.conversion_events ? JSON.parse(String(current.conversion_events)) : void 0,
|
|
@@ -1418,18 +1699,19 @@ var ClickHouseAdapter = class {
|
|
|
1418
1699
|
siteId: String(row.site_id),
|
|
1419
1700
|
secretKey: String(row.secret_key),
|
|
1420
1701
|
name: String(row.name),
|
|
1702
|
+
type: row.type ? String(row.type) : "web",
|
|
1421
1703
|
domain: row.domain ? String(row.domain) : void 0,
|
|
1422
1704
|
allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
|
|
1423
1705
|
conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
|
|
1424
|
-
createdAt:
|
|
1425
|
-
updatedAt:
|
|
1706
|
+
createdAt: toUTCDate(String(row.created_at)).toISOString(),
|
|
1707
|
+
updatedAt: toUTCDate(String(row.updated_at)).toISOString()
|
|
1426
1708
|
};
|
|
1427
1709
|
}
|
|
1428
1710
|
toEventListItem(row) {
|
|
1429
1711
|
return {
|
|
1430
1712
|
id: String(row.event_id ?? ""),
|
|
1431
1713
|
type: String(row.type),
|
|
1432
|
-
timestamp:
|
|
1714
|
+
timestamp: toUTCDate(String(row.timestamp)).toISOString(),
|
|
1433
1715
|
visitorId: String(row.visitor_id),
|
|
1434
1716
|
sessionId: String(row.session_id),
|
|
1435
1717
|
url: row.url ? String(row.url) : void 0,
|
|
@@ -1481,6 +1763,130 @@ import { MongoClient } from "mongodb";
|
|
|
1481
1763
|
var EVENTS_COLLECTION = "litemetrics_events";
|
|
1482
1764
|
var SITES_COLLECTION = "litemetrics_sites";
|
|
1483
1765
|
var IDENTITY_MAP_COLLECTION = "litemetrics_identity_map";
|
|
1766
|
+
function normalizedUtmSourceSwitch() {
|
|
1767
|
+
return {
|
|
1768
|
+
$switch: {
|
|
1769
|
+
branches: [
|
|
1770
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["ig", "instagram", "instagram.com"]] }, then: "Instagram" },
|
|
1771
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["fb", "facebook", "facebook.com", "fb.com"]] }, then: "Facebook" },
|
|
1772
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["tw", "twitter", "twitter.com", "x", "x.com", "t.co"]] }, then: "X (Twitter)" },
|
|
1773
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["li", "linkedin", "linkedin.com"]] }, then: "LinkedIn" },
|
|
1774
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["yt", "youtube", "youtube.com"]] }, then: "YouTube" },
|
|
1775
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["goog", "google", "google.com"]] }, then: "Google" },
|
|
1776
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["gh", "github", "github.com"]] }, then: "GitHub" },
|
|
1777
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["reddit", "reddit.com"]] }, then: "Reddit" },
|
|
1778
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["pinterest", "pinterest.com"]] }, then: "Pinterest" },
|
|
1779
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["tiktok", "tiktok.com"]] }, then: "TikTok" },
|
|
1780
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["openai", "chatgpt", "chat.openai.com"]] }, then: "OpenAI" },
|
|
1781
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_source", ""] } }, ["perplexity", "perplexity.ai"]] }, then: "Perplexity" }
|
|
1782
|
+
],
|
|
1783
|
+
default: "$utm_source"
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function normalizedUtmMediumSwitch() {
|
|
1788
|
+
return {
|
|
1789
|
+
$switch: {
|
|
1790
|
+
branches: [
|
|
1791
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["cpc", "ppc", "paidsearch", "paid-search", "paid_search", "paid"]] }, then: "Paid" },
|
|
1792
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["organic"]] }, then: "Organic" },
|
|
1793
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["social", "social-media", "social_media"]] }, then: "Social" },
|
|
1794
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["email", "e-mail", "e_mail"]] }, then: "Email" },
|
|
1795
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["display", "banner", "cpm"]] }, then: "Display" },
|
|
1796
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["affiliate"]] }, then: "Affiliate" },
|
|
1797
|
+
{ case: { $in: [{ $toLower: { $ifNull: ["$utm_medium", ""] } }, ["referral"]] }, then: "Referral" }
|
|
1798
|
+
],
|
|
1799
|
+
default: "$utm_medium"
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
var SEARCH_ENGINES = /google|bing|yahoo|duckduckgo|ecosia|baidu|yandex|search\.brave/i;
|
|
1804
|
+
var SOCIAL_NETWORKS = /instagram|facebook|twitter|x\.com|t\.co|linkedin|youtube|tiktok|pinterest|reddit|snapchat|mastodon|tumblr/i;
|
|
1805
|
+
var PAID_MEDIUMS = ["cpc", "ppc", "paidsearch", "paid-search", "paid_search", "paid"];
|
|
1806
|
+
var SOCIAL_SOURCES = ["instagram", "ig", "facebook", "fb", "twitter", "tw", "x", "linkedin", "li", "youtube", "yt", "tiktok", "pinterest", "reddit", "snapchat"];
|
|
1807
|
+
function channelClassificationSwitch() {
|
|
1808
|
+
const lMedium = { $toLower: { $ifNull: ["$utm_medium", ""] } };
|
|
1809
|
+
const lSource = { $toLower: { $ifNull: ["$utm_source", ""] } };
|
|
1810
|
+
const refStr = { $ifNull: ["$referrer", ""] };
|
|
1811
|
+
return {
|
|
1812
|
+
$switch: {
|
|
1813
|
+
branches: [
|
|
1814
|
+
// Paid Search
|
|
1815
|
+
{
|
|
1816
|
+
case: {
|
|
1817
|
+
$and: [
|
|
1818
|
+
{ $in: [lMedium, PAID_MEDIUMS] },
|
|
1819
|
+
{ $or: [
|
|
1820
|
+
{ $in: [lSource, ["google", "goog", "bing", "yahoo", "duckduckgo", "ecosia", "baidu", "yandex"]] },
|
|
1821
|
+
{ $regexMatch: { input: refStr, regex: SEARCH_ENGINES } }
|
|
1822
|
+
] }
|
|
1823
|
+
]
|
|
1824
|
+
},
|
|
1825
|
+
then: "Paid Search"
|
|
1826
|
+
},
|
|
1827
|
+
// Paid Social
|
|
1828
|
+
{
|
|
1829
|
+
case: {
|
|
1830
|
+
$and: [
|
|
1831
|
+
{ $in: [lMedium, PAID_MEDIUMS] },
|
|
1832
|
+
{ $or: [
|
|
1833
|
+
{ $in: [lSource, SOCIAL_SOURCES] },
|
|
1834
|
+
{ $regexMatch: { input: refStr, regex: SOCIAL_NETWORKS } }
|
|
1835
|
+
] }
|
|
1836
|
+
]
|
|
1837
|
+
},
|
|
1838
|
+
then: "Paid Social"
|
|
1839
|
+
},
|
|
1840
|
+
// Email
|
|
1841
|
+
{ case: { $in: [lMedium, ["email", "e-mail", "e_mail"]] }, then: "Email" },
|
|
1842
|
+
// Display
|
|
1843
|
+
{ case: { $in: [lMedium, ["display", "banner", "cpm"]] }, then: "Display" },
|
|
1844
|
+
// Affiliate
|
|
1845
|
+
{ case: { $in: [lMedium, ["affiliate"]] }, then: "Affiliate" },
|
|
1846
|
+
// Organic Search
|
|
1847
|
+
{
|
|
1848
|
+
case: {
|
|
1849
|
+
$and: [
|
|
1850
|
+
{ $regexMatch: { input: refStr, regex: SEARCH_ENGINES } },
|
|
1851
|
+
{ $not: [{ $in: [lMedium, PAID_MEDIUMS] }] }
|
|
1852
|
+
]
|
|
1853
|
+
},
|
|
1854
|
+
then: "Organic Search"
|
|
1855
|
+
},
|
|
1856
|
+
// Organic Social
|
|
1857
|
+
{
|
|
1858
|
+
case: {
|
|
1859
|
+
$and: [
|
|
1860
|
+
{ $or: [
|
|
1861
|
+
{ $regexMatch: { input: refStr, regex: SOCIAL_NETWORKS } },
|
|
1862
|
+
{ $in: [lSource, SOCIAL_SOURCES] }
|
|
1863
|
+
] },
|
|
1864
|
+
{ $not: [{ $in: [lMedium, PAID_MEDIUMS] }] }
|
|
1865
|
+
]
|
|
1866
|
+
},
|
|
1867
|
+
then: "Organic Social"
|
|
1868
|
+
},
|
|
1869
|
+
// Referral
|
|
1870
|
+
{
|
|
1871
|
+
case: { $and: [{ $ne: [refStr, ""] }, { $gt: [{ $strLenCP: refStr }, 0] }] },
|
|
1872
|
+
then: "Referral"
|
|
1873
|
+
},
|
|
1874
|
+
// Other (has UTM but no referrer)
|
|
1875
|
+
{
|
|
1876
|
+
case: {
|
|
1877
|
+
$or: [
|
|
1878
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_source", ""] }, ""] }] },
|
|
1879
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_medium", ""] }, ""] }] },
|
|
1880
|
+
{ $and: [{ $ne: [{ $ifNull: ["$utm_campaign", ""] }, ""] }] }
|
|
1881
|
+
]
|
|
1882
|
+
},
|
|
1883
|
+
then: "Other"
|
|
1884
|
+
}
|
|
1885
|
+
],
|
|
1886
|
+
default: "Direct"
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1484
1890
|
function buildFilterMatch(filters) {
|
|
1485
1891
|
if (!filters) return {};
|
|
1486
1892
|
const map = {
|
|
@@ -1491,6 +1897,10 @@ function buildFilterMatch(filters) {
|
|
|
1491
1897
|
"device.type": "device_type",
|
|
1492
1898
|
"device.browser": "browser",
|
|
1493
1899
|
"device.os": "os",
|
|
1900
|
+
"device.osVersion": "os_version",
|
|
1901
|
+
"device.deviceModel": "device_model",
|
|
1902
|
+
"device.deviceBrand": "device_brand",
|
|
1903
|
+
"device.appVersion": "app_version",
|
|
1494
1904
|
"utm.source": "utm_source",
|
|
1495
1905
|
"utm.medium": "utm_medium",
|
|
1496
1906
|
"utm.campaign": "utm_campaign",
|
|
@@ -1506,7 +1916,9 @@ function buildFilterMatch(filters) {
|
|
|
1506
1916
|
};
|
|
1507
1917
|
const match = {};
|
|
1508
1918
|
for (const [key, value] of Object.entries(filters)) {
|
|
1509
|
-
if (!value
|
|
1919
|
+
if (!value) continue;
|
|
1920
|
+
if (key === "channel") continue;
|
|
1921
|
+
if (!map[key]) continue;
|
|
1510
1922
|
match[map[key]] = value;
|
|
1511
1923
|
}
|
|
1512
1924
|
return match;
|
|
@@ -1575,6 +1987,13 @@ var MongoDBAdapter = class {
|
|
|
1575
1987
|
utm_term: e.utm?.term ?? null,
|
|
1576
1988
|
utm_content: e.utm?.content ?? null,
|
|
1577
1989
|
ip: e.ip ?? null,
|
|
1990
|
+
os_version: e.device?.osVersion ?? null,
|
|
1991
|
+
device_model: e.device?.deviceModel ?? null,
|
|
1992
|
+
device_brand: e.device?.deviceBrand ?? null,
|
|
1993
|
+
app_version: e.device?.appVersion ?? null,
|
|
1994
|
+
app_build: e.device?.appBuild ?? null,
|
|
1995
|
+
sdk_name: e.device?.sdkName ?? null,
|
|
1996
|
+
sdk_version: e.device?.sdkVersion ?? null,
|
|
1578
1997
|
created_at: /* @__PURE__ */ new Date()
|
|
1579
1998
|
}));
|
|
1580
1999
|
await this.collection.insertMany(docs);
|
|
@@ -1588,12 +2007,22 @@ var MongoDBAdapter = class {
|
|
|
1588
2007
|
timestamp: { $gte: new Date(dateRange.from), $lte: new Date(dateRange.to) }
|
|
1589
2008
|
};
|
|
1590
2009
|
const filterMatch = buildFilterMatch(q.filters);
|
|
2010
|
+
const matchStages = (extra) => {
|
|
2011
|
+
const stages = [
|
|
2012
|
+
{ $match: { ...baseMatch, ...filterMatch, ...extra } }
|
|
2013
|
+
];
|
|
2014
|
+
if (q.filters?.channel) {
|
|
2015
|
+
stages.push({ $addFields: { _channel: channelClassificationSwitch() } });
|
|
2016
|
+
stages.push({ $match: { _channel: q.filters.channel } });
|
|
2017
|
+
}
|
|
2018
|
+
return stages;
|
|
2019
|
+
};
|
|
1591
2020
|
let data = [];
|
|
1592
2021
|
let total = 0;
|
|
1593
2022
|
switch (q.metric) {
|
|
1594
2023
|
case "pageviews": {
|
|
1595
2024
|
const [result2] = await this.collection.aggregate([
|
|
1596
|
-
{
|
|
2025
|
+
...matchStages({ type: "pageview" }),
|
|
1597
2026
|
{ $count: "count" }
|
|
1598
2027
|
]).toArray();
|
|
1599
2028
|
total = result2?.count ?? 0;
|
|
@@ -1602,7 +2031,7 @@ var MongoDBAdapter = class {
|
|
|
1602
2031
|
}
|
|
1603
2032
|
case "visitors": {
|
|
1604
2033
|
const [result2] = await this.collection.aggregate([
|
|
1605
|
-
|
|
2034
|
+
...matchStages(),
|
|
1606
2035
|
{ $group: { _id: "$visitor_id" } },
|
|
1607
2036
|
{ $count: "count" }
|
|
1608
2037
|
]).toArray();
|
|
@@ -1612,7 +2041,7 @@ var MongoDBAdapter = class {
|
|
|
1612
2041
|
}
|
|
1613
2042
|
case "sessions": {
|
|
1614
2043
|
const [result2] = await this.collection.aggregate([
|
|
1615
|
-
|
|
2044
|
+
...matchStages(),
|
|
1616
2045
|
{ $group: { _id: "$session_id" } },
|
|
1617
2046
|
{ $count: "count" }
|
|
1618
2047
|
]).toArray();
|
|
@@ -1622,7 +2051,7 @@ var MongoDBAdapter = class {
|
|
|
1622
2051
|
}
|
|
1623
2052
|
case "events": {
|
|
1624
2053
|
const [result2] = await this.collection.aggregate([
|
|
1625
|
-
{
|
|
2054
|
+
...matchStages({ type: "event" }),
|
|
1626
2055
|
{ $count: "count" }
|
|
1627
2056
|
]).toArray();
|
|
1628
2057
|
total = result2?.count ?? 0;
|
|
@@ -1637,7 +2066,7 @@ var MongoDBAdapter = class {
|
|
|
1637
2066
|
break;
|
|
1638
2067
|
}
|
|
1639
2068
|
const [result2] = await this.collection.aggregate([
|
|
1640
|
-
{
|
|
2069
|
+
...matchStages({ type: "event", event_name: { $in: conversionEvents } }),
|
|
1641
2070
|
{ $count: "count" }
|
|
1642
2071
|
]).toArray();
|
|
1643
2072
|
total = result2?.count ?? 0;
|
|
@@ -1646,7 +2075,7 @@ var MongoDBAdapter = class {
|
|
|
1646
2075
|
}
|
|
1647
2076
|
case "top_pages": {
|
|
1648
2077
|
const rows = await this.collection.aggregate([
|
|
1649
|
-
{
|
|
2078
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1650
2079
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
1651
2080
|
{ $sort: { value: -1 } },
|
|
1652
2081
|
{ $limit: limit }
|
|
@@ -1657,7 +2086,7 @@ var MongoDBAdapter = class {
|
|
|
1657
2086
|
}
|
|
1658
2087
|
case "top_referrers": {
|
|
1659
2088
|
const rows = await this.collection.aggregate([
|
|
1660
|
-
{
|
|
2089
|
+
...matchStages({ type: "pageview", referrer: { $nin: [null, ""] } }),
|
|
1661
2090
|
{ $group: { _id: "$referrer", value: { $sum: 1 } } },
|
|
1662
2091
|
{ $sort: { value: -1 } },
|
|
1663
2092
|
{ $limit: limit }
|
|
@@ -1668,7 +2097,7 @@ var MongoDBAdapter = class {
|
|
|
1668
2097
|
}
|
|
1669
2098
|
case "top_countries": {
|
|
1670
2099
|
const rows = await this.collection.aggregate([
|
|
1671
|
-
{
|
|
2100
|
+
...matchStages({ country: { $ne: null } }),
|
|
1672
2101
|
{ $group: { _id: "$country", value: { $addToSet: "$visitor_id" } } },
|
|
1673
2102
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1674
2103
|
{ $sort: { value: -1 } },
|
|
@@ -1680,7 +2109,7 @@ var MongoDBAdapter = class {
|
|
|
1680
2109
|
}
|
|
1681
2110
|
case "top_cities": {
|
|
1682
2111
|
const rows = await this.collection.aggregate([
|
|
1683
|
-
{
|
|
2112
|
+
...matchStages({ city: { $ne: null } }),
|
|
1684
2113
|
{ $group: { _id: "$city", value: { $addToSet: "$visitor_id" } } },
|
|
1685
2114
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1686
2115
|
{ $sort: { value: -1 } },
|
|
@@ -1692,7 +2121,7 @@ var MongoDBAdapter = class {
|
|
|
1692
2121
|
}
|
|
1693
2122
|
case "top_events": {
|
|
1694
2123
|
const rows = await this.collection.aggregate([
|
|
1695
|
-
{
|
|
2124
|
+
...matchStages({ type: "event", event_name: { $ne: null } }),
|
|
1696
2125
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1697
2126
|
{ $sort: { value: -1 } },
|
|
1698
2127
|
{ $limit: limit }
|
|
@@ -1709,7 +2138,7 @@ var MongoDBAdapter = class {
|
|
|
1709
2138
|
break;
|
|
1710
2139
|
}
|
|
1711
2140
|
const rows = await this.collection.aggregate([
|
|
1712
|
-
{
|
|
2141
|
+
...matchStages({ type: "event", event_name: { $in: conversionEvents } }),
|
|
1713
2142
|
{ $group: { _id: "$event_name", value: { $sum: 1 } } },
|
|
1714
2143
|
{ $sort: { value: -1 } },
|
|
1715
2144
|
{ $limit: limit }
|
|
@@ -1720,7 +2149,7 @@ var MongoDBAdapter = class {
|
|
|
1720
2149
|
}
|
|
1721
2150
|
case "top_exit_pages": {
|
|
1722
2151
|
const rows = await this.collection.aggregate([
|
|
1723
|
-
{
|
|
2152
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1724
2153
|
{ $sort: { timestamp: 1 } },
|
|
1725
2154
|
{ $group: { _id: "$session_id", url: { $last: "$url" } } },
|
|
1726
2155
|
{ $group: { _id: "$url", value: { $sum: 1 } } },
|
|
@@ -1733,7 +2162,7 @@ var MongoDBAdapter = class {
|
|
|
1733
2162
|
}
|
|
1734
2163
|
case "top_transitions": {
|
|
1735
2164
|
const rows = await this.collection.aggregate([
|
|
1736
|
-
{
|
|
2165
|
+
...matchStages({ type: "pageview", url: { $ne: null } }),
|
|
1737
2166
|
{
|
|
1738
2167
|
$setWindowFields: {
|
|
1739
2168
|
partitionBy: "$session_id",
|
|
@@ -1754,7 +2183,7 @@ var MongoDBAdapter = class {
|
|
|
1754
2183
|
}
|
|
1755
2184
|
case "top_scroll_pages": {
|
|
1756
2185
|
const rows = await this.collection.aggregate([
|
|
1757
|
-
{
|
|
2186
|
+
...matchStages({ type: "event", event_subtype: "scroll_depth", page_path: { $ne: null } }),
|
|
1758
2187
|
{ $group: { _id: "$page_path", value: { $sum: 1 } } },
|
|
1759
2188
|
{ $sort: { value: -1 } },
|
|
1760
2189
|
{ $limit: limit }
|
|
@@ -1765,15 +2194,11 @@ var MongoDBAdapter = class {
|
|
|
1765
2194
|
}
|
|
1766
2195
|
case "top_button_clicks": {
|
|
1767
2196
|
const rows = await this.collection.aggregate([
|
|
1768
|
-
{
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
event_subtype: "button_click",
|
|
1774
|
-
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
1775
|
-
}
|
|
1776
|
-
},
|
|
2197
|
+
...matchStages({
|
|
2198
|
+
type: "event",
|
|
2199
|
+
event_subtype: "button_click",
|
|
2200
|
+
$or: [{ element_text: { $ne: null } }, { element_selector: { $ne: null } }]
|
|
2201
|
+
}),
|
|
1777
2202
|
{ $group: { _id: { $ifNull: ["$element_text", "$element_selector"] }, value: { $sum: 1 } } },
|
|
1778
2203
|
{ $sort: { value: -1 } },
|
|
1779
2204
|
{ $limit: limit }
|
|
@@ -1784,15 +2209,11 @@ var MongoDBAdapter = class {
|
|
|
1784
2209
|
}
|
|
1785
2210
|
case "top_link_targets": {
|
|
1786
2211
|
const rows = await this.collection.aggregate([
|
|
1787
|
-
{
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
1793
|
-
target_url_path: { $ne: null }
|
|
1794
|
-
}
|
|
1795
|
-
},
|
|
2212
|
+
...matchStages({
|
|
2213
|
+
type: "event",
|
|
2214
|
+
event_subtype: { $in: ["link_click", "outbound_click"] },
|
|
2215
|
+
target_url_path: { $ne: null }
|
|
2216
|
+
}),
|
|
1796
2217
|
{ $group: { _id: "$target_url_path", value: { $sum: 1 } } },
|
|
1797
2218
|
{ $sort: { value: -1 } },
|
|
1798
2219
|
{ $limit: limit }
|
|
@@ -1803,7 +2224,7 @@ var MongoDBAdapter = class {
|
|
|
1803
2224
|
}
|
|
1804
2225
|
case "top_devices": {
|
|
1805
2226
|
const rows = await this.collection.aggregate([
|
|
1806
|
-
{
|
|
2227
|
+
...matchStages({ device_type: { $ne: null } }),
|
|
1807
2228
|
{ $group: { _id: "$device_type", value: { $addToSet: "$visitor_id" } } },
|
|
1808
2229
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1809
2230
|
{ $sort: { value: -1 } },
|
|
@@ -1815,7 +2236,7 @@ var MongoDBAdapter = class {
|
|
|
1815
2236
|
}
|
|
1816
2237
|
case "top_browsers": {
|
|
1817
2238
|
const rows = await this.collection.aggregate([
|
|
1818
|
-
{
|
|
2239
|
+
...matchStages({ browser: { $ne: null } }),
|
|
1819
2240
|
{ $group: { _id: "$browser", value: { $addToSet: "$visitor_id" } } },
|
|
1820
2241
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1821
2242
|
{ $sort: { value: -1 } },
|
|
@@ -1827,7 +2248,7 @@ var MongoDBAdapter = class {
|
|
|
1827
2248
|
}
|
|
1828
2249
|
case "top_os": {
|
|
1829
2250
|
const rows = await this.collection.aggregate([
|
|
1830
|
-
{
|
|
2251
|
+
...matchStages({ os: { $ne: null } }),
|
|
1831
2252
|
{ $group: { _id: "$os", value: { $addToSet: "$visitor_id" } } },
|
|
1832
2253
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1833
2254
|
{ $sort: { value: -1 } },
|
|
@@ -1837,10 +2258,47 @@ var MongoDBAdapter = class {
|
|
|
1837
2258
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1838
2259
|
break;
|
|
1839
2260
|
}
|
|
2261
|
+
case "top_os_versions": {
|
|
2262
|
+
const rows = await this.collection.aggregate([
|
|
2263
|
+
...matchStages({ os: { $ne: null }, os_version: { $ne: null } }),
|
|
2264
|
+
{ $group: { _id: { $concat: ["$os", " ", "$os_version"] }, value: { $addToSet: "$visitor_id" } } },
|
|
2265
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2266
|
+
{ $sort: { value: -1 } },
|
|
2267
|
+
{ $limit: limit }
|
|
2268
|
+
]).toArray();
|
|
2269
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2270
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2271
|
+
break;
|
|
2272
|
+
}
|
|
2273
|
+
case "top_device_models": {
|
|
2274
|
+
const rows = await this.collection.aggregate([
|
|
2275
|
+
...matchStages({ device_model: { $ne: null } }),
|
|
2276
|
+
{ $group: { _id: { $trim: { input: { $concat: [{ $ifNull: ["$device_brand", ""] }, " ", "$device_model"] } } }, value: { $addToSet: "$visitor_id" } } },
|
|
2277
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2278
|
+
{ $sort: { value: -1 } },
|
|
2279
|
+
{ $limit: limit }
|
|
2280
|
+
]).toArray();
|
|
2281
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2282
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
case "top_app_versions": {
|
|
2286
|
+
const rows = await this.collection.aggregate([
|
|
2287
|
+
...matchStages({ app_version: { $ne: null } }),
|
|
2288
|
+
{ $group: { _id: "$app_version", value: { $addToSet: "$visitor_id" } } },
|
|
2289
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2290
|
+
{ $sort: { value: -1 } },
|
|
2291
|
+
{ $limit: limit }
|
|
2292
|
+
]).toArray();
|
|
2293
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2294
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2295
|
+
break;
|
|
2296
|
+
}
|
|
1840
2297
|
case "top_utm_sources": {
|
|
1841
2298
|
const rows = await this.collection.aggregate([
|
|
1842
|
-
{
|
|
1843
|
-
{ $
|
|
2299
|
+
...matchStages({ utm_source: { $nin: [null, ""] } }),
|
|
2300
|
+
{ $addFields: { _normalized_source: normalizedUtmSourceSwitch() } },
|
|
2301
|
+
{ $group: { _id: "$_normalized_source", value: { $addToSet: "$visitor_id" } } },
|
|
1844
2302
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1845
2303
|
{ $sort: { value: -1 } },
|
|
1846
2304
|
{ $limit: limit }
|
|
@@ -1851,8 +2309,9 @@ var MongoDBAdapter = class {
|
|
|
1851
2309
|
}
|
|
1852
2310
|
case "top_utm_mediums": {
|
|
1853
2311
|
const rows = await this.collection.aggregate([
|
|
1854
|
-
{
|
|
1855
|
-
{ $
|
|
2312
|
+
...matchStages({ utm_medium: { $nin: [null, ""] } }),
|
|
2313
|
+
{ $addFields: { _normalized_medium: normalizedUtmMediumSwitch() } },
|
|
2314
|
+
{ $group: { _id: "$_normalized_medium", value: { $addToSet: "$visitor_id" } } },
|
|
1856
2315
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1857
2316
|
{ $sort: { value: -1 } },
|
|
1858
2317
|
{ $limit: limit }
|
|
@@ -1863,7 +2322,7 @@ var MongoDBAdapter = class {
|
|
|
1863
2322
|
}
|
|
1864
2323
|
case "top_utm_campaigns": {
|
|
1865
2324
|
const rows = await this.collection.aggregate([
|
|
1866
|
-
{
|
|
2325
|
+
...matchStages({ utm_campaign: { $nin: [null, ""] } }),
|
|
1867
2326
|
{ $group: { _id: "$utm_campaign", value: { $addToSet: "$visitor_id" } } },
|
|
1868
2327
|
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
1869
2328
|
{ $sort: { value: -1 } },
|
|
@@ -1873,6 +2332,43 @@ var MongoDBAdapter = class {
|
|
|
1873
2332
|
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
1874
2333
|
break;
|
|
1875
2334
|
}
|
|
2335
|
+
case "top_utm_terms": {
|
|
2336
|
+
const rows = await this.collection.aggregate([
|
|
2337
|
+
...matchStages({ utm_term: { $nin: [null, ""] } }),
|
|
2338
|
+
{ $group: { _id: "$utm_term", value: { $addToSet: "$visitor_id" } } },
|
|
2339
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2340
|
+
{ $sort: { value: -1 } },
|
|
2341
|
+
{ $limit: limit }
|
|
2342
|
+
]).toArray();
|
|
2343
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2344
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2345
|
+
break;
|
|
2346
|
+
}
|
|
2347
|
+
case "top_utm_contents": {
|
|
2348
|
+
const rows = await this.collection.aggregate([
|
|
2349
|
+
...matchStages({ utm_content: { $nin: [null, ""] } }),
|
|
2350
|
+
{ $group: { _id: "$utm_content", value: { $addToSet: "$visitor_id" } } },
|
|
2351
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2352
|
+
{ $sort: { value: -1 } },
|
|
2353
|
+
{ $limit: limit }
|
|
2354
|
+
]).toArray();
|
|
2355
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2356
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
case "top_channels": {
|
|
2360
|
+
const rows = await this.collection.aggregate([
|
|
2361
|
+
...matchStages(),
|
|
2362
|
+
{ $addFields: { _channel: channelClassificationSwitch() } },
|
|
2363
|
+
{ $group: { _id: "$_channel", value: { $addToSet: "$visitor_id" } } },
|
|
2364
|
+
{ $project: { _id: 1, value: { $size: "$value" } } },
|
|
2365
|
+
{ $sort: { value: -1 } },
|
|
2366
|
+
{ $limit: limit }
|
|
2367
|
+
]).toArray();
|
|
2368
|
+
data = rows.map((r) => ({ key: r._id, value: r.value }));
|
|
2369
|
+
total = data.reduce((sum, d) => sum + d.value, 0);
|
|
2370
|
+
break;
|
|
2371
|
+
}
|
|
1876
2372
|
}
|
|
1877
2373
|
const result = { metric: q.metric, period, data, total };
|
|
1878
2374
|
if (q.compare && ["pageviews", "visitors", "sessions", "events", "conversions"].includes(q.metric)) {
|
|
@@ -2335,7 +2831,18 @@ var MongoDBAdapter = class {
|
|
|
2335
2831
|
userId: doc.user_id ?? void 0,
|
|
2336
2832
|
traits: doc.traits ?? void 0,
|
|
2337
2833
|
geo: doc.country ? { country: doc.country, city: doc.city ?? void 0, region: doc.region ?? void 0 } : void 0,
|
|
2338
|
-
device: doc.device_type ? {
|
|
2834
|
+
device: doc.device_type ? {
|
|
2835
|
+
type: doc.device_type,
|
|
2836
|
+
browser: doc.browser ?? "",
|
|
2837
|
+
os: doc.os ?? "",
|
|
2838
|
+
osVersion: doc.os_version ?? void 0,
|
|
2839
|
+
deviceModel: doc.device_model ?? void 0,
|
|
2840
|
+
deviceBrand: doc.device_brand ?? void 0,
|
|
2841
|
+
appVersion: doc.app_version ?? void 0,
|
|
2842
|
+
appBuild: doc.app_build ?? void 0,
|
|
2843
|
+
sdkName: doc.sdk_name ?? void 0,
|
|
2844
|
+
sdkVersion: doc.sdk_version ?? void 0
|
|
2845
|
+
} : void 0,
|
|
2339
2846
|
language: doc.language ?? void 0,
|
|
2340
2847
|
utm: doc.utm_source ? {
|
|
2341
2848
|
source: doc.utm_source ?? void 0,
|
|
@@ -2353,6 +2860,7 @@ var MongoDBAdapter = class {
|
|
|
2353
2860
|
site_id: generateSiteId(),
|
|
2354
2861
|
secret_key: generateSecretKey(),
|
|
2355
2862
|
name: data.name,
|
|
2863
|
+
type: data.type ?? "web",
|
|
2356
2864
|
domain: data.domain ?? null,
|
|
2357
2865
|
allowed_origins: data.allowedOrigins ?? null,
|
|
2358
2866
|
conversion_events: data.conversionEvents ?? null,
|
|
@@ -2377,6 +2885,7 @@ var MongoDBAdapter = class {
|
|
|
2377
2885
|
async updateSite(siteId, data) {
|
|
2378
2886
|
const updates = { updated_at: /* @__PURE__ */ new Date() };
|
|
2379
2887
|
if (data.name !== void 0) updates.name = data.name;
|
|
2888
|
+
if (data.type !== void 0) updates.type = data.type;
|
|
2380
2889
|
if (data.domain !== void 0) updates.domain = data.domain || null;
|
|
2381
2890
|
if (data.allowedOrigins !== void 0) updates.allowed_origins = data.allowedOrigins.length > 0 ? data.allowedOrigins : null;
|
|
2382
2891
|
if (data.conversionEvents !== void 0) updates.conversion_events = data.conversionEvents.length > 0 ? data.conversionEvents : null;
|
|
@@ -2408,6 +2917,7 @@ var MongoDBAdapter = class {
|
|
|
2408
2917
|
siteId: doc.site_id,
|
|
2409
2918
|
secretKey: doc.secret_key,
|
|
2410
2919
|
name: doc.name,
|
|
2920
|
+
type: doc.type ?? "web",
|
|
2411
2921
|
domain: doc.domain ?? void 0,
|
|
2412
2922
|
allowedOrigins: doc.allowed_origins ?? void 0,
|
|
2413
2923
|
conversionEvents: doc.conversion_events ?? void 0,
|
|
@@ -2676,9 +3186,26 @@ async function createCollector(config) {
|
|
|
2676
3186
|
return false;
|
|
2677
3187
|
}
|
|
2678
3188
|
function enrichEvents(events, ip, userAgent) {
|
|
2679
|
-
const
|
|
3189
|
+
const uaDevice = parseUserAgent(userAgent);
|
|
2680
3190
|
return events.map((event) => {
|
|
2681
3191
|
const geo = resolveGeo(ip, event.timezone);
|
|
3192
|
+
let device;
|
|
3193
|
+
if (event.mobile?.platform) {
|
|
3194
|
+
device = {
|
|
3195
|
+
type: "mobile",
|
|
3196
|
+
browser: "App",
|
|
3197
|
+
os: event.mobile.platform === "ios" ? "iOS" : "Android",
|
|
3198
|
+
osVersion: event.mobile.osVersion,
|
|
3199
|
+
deviceModel: event.mobile.deviceModel,
|
|
3200
|
+
deviceBrand: event.mobile.deviceBrand,
|
|
3201
|
+
appVersion: event.mobile.appVersion,
|
|
3202
|
+
appBuild: event.mobile.appBuild,
|
|
3203
|
+
sdkName: event.mobile.sdkName,
|
|
3204
|
+
sdkVersion: event.mobile.sdkVersion
|
|
3205
|
+
};
|
|
3206
|
+
} else {
|
|
3207
|
+
device = uaDevice;
|
|
3208
|
+
}
|
|
2682
3209
|
return { ...event, ip, geo, device };
|
|
2683
3210
|
});
|
|
2684
3211
|
}
|
|
@@ -2738,6 +3265,22 @@ async function createCollector(config) {
|
|
|
2738
3265
|
}
|
|
2739
3266
|
return req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || "";
|
|
2740
3267
|
}
|
|
3268
|
+
function extractRequestHostname(req) {
|
|
3269
|
+
const headerValue = (value) => {
|
|
3270
|
+
if (Array.isArray(value)) return value[0];
|
|
3271
|
+
if (typeof value === "string") return value;
|
|
3272
|
+
return void 0;
|
|
3273
|
+
};
|
|
3274
|
+
const origin = headerValue(req.headers?.origin);
|
|
3275
|
+
const referer = headerValue(req.headers?.referer) || headerValue(req.headers?.referrer);
|
|
3276
|
+
const raw = origin ?? referer;
|
|
3277
|
+
if (!raw || raw === "null") return void 0;
|
|
3278
|
+
try {
|
|
3279
|
+
return new URL(raw).hostname.toLowerCase();
|
|
3280
|
+
} catch {
|
|
3281
|
+
return void 0;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
2741
3284
|
function handler() {
|
|
2742
3285
|
return async (req, res) => {
|
|
2743
3286
|
res.setHeader?.("Access-Control-Allow-Origin", "*");
|
|
@@ -2763,6 +3306,12 @@ async function createCollector(config) {
|
|
|
2763
3306
|
sendJson(res, 400, { ok: false, error: "Too many events (max 100)" });
|
|
2764
3307
|
return;
|
|
2765
3308
|
}
|
|
3309
|
+
const siteIds = new Set(payload.events.map((event) => event.siteId).filter(Boolean));
|
|
3310
|
+
if (siteIds.size !== 1) {
|
|
3311
|
+
sendJson(res, 200, { ok: true });
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
const siteId = Array.from(siteIds)[0];
|
|
2766
3315
|
const userAgent = req.headers?.["user-agent"] || "";
|
|
2767
3316
|
if (isBot(userAgent)) {
|
|
2768
3317
|
sendJson(res, 200, { ok: true });
|
|
@@ -2770,26 +3319,15 @@ async function createCollector(config) {
|
|
|
2770
3319
|
}
|
|
2771
3320
|
const ip = extractIp(req);
|
|
2772
3321
|
const enriched = enrichEvents(payload.events, ip, userAgent);
|
|
2773
|
-
const
|
|
2774
|
-
if (
|
|
2775
|
-
const
|
|
2776
|
-
if (
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
return allowed.has(hostname);
|
|
2783
|
-
} catch {
|
|
2784
|
-
return true;
|
|
2785
|
-
}
|
|
2786
|
-
});
|
|
2787
|
-
if (filtered.length === 0) {
|
|
2788
|
-
sendJson(res, 200, { ok: true });
|
|
2789
|
-
return;
|
|
2790
|
-
}
|
|
2791
|
-
await processIdentity(filtered);
|
|
2792
|
-
await db.insertEvents(filtered);
|
|
3322
|
+
const site = await db.getSite(siteId);
|
|
3323
|
+
if (site?.allowedOrigins && site.allowedOrigins.length > 0) {
|
|
3324
|
+
const requestHostname = extractRequestHostname(req);
|
|
3325
|
+
if (!requestHostname) {
|
|
3326
|
+
sendJson(res, 200, { ok: true });
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
const allowed = new Set(site.allowedOrigins.map((h) => h.toLowerCase()));
|
|
3330
|
+
if (!allowed.has(requestHostname)) {
|
|
2793
3331
|
sendJson(res, 200, { ok: true });
|
|
2794
3332
|
return;
|
|
2795
3333
|
}
|
|
@@ -2829,7 +3367,8 @@ async function createCollector(config) {
|
|
|
2829
3367
|
dateFrom: params.dateFrom,
|
|
2830
3368
|
dateTo: params.dateTo,
|
|
2831
3369
|
granularity: q.granularity,
|
|
2832
|
-
filters: q.filters ? JSON.parse(q.filters) : void 0
|
|
3370
|
+
filters: q.filters ? JSON.parse(q.filters) : void 0,
|
|
3371
|
+
timezone: params.timezone
|
|
2833
3372
|
};
|
|
2834
3373
|
if (tsParams.metric === "conversions") {
|
|
2835
3374
|
const site = await db.getSite(params.siteId);
|
|
@@ -3131,19 +3670,6 @@ async function parseBody(req) {
|
|
|
3131
3670
|
req.on("error", reject);
|
|
3132
3671
|
});
|
|
3133
3672
|
}
|
|
3134
|
-
function extractQueryParams(req) {
|
|
3135
|
-
const q = req.query ?? Object.fromEntries(new URL(req.url, "http://localhost").searchParams);
|
|
3136
|
-
return {
|
|
3137
|
-
siteId: q.siteId,
|
|
3138
|
-
metric: q.metric,
|
|
3139
|
-
period: q.period,
|
|
3140
|
-
dateFrom: q.dateFrom,
|
|
3141
|
-
dateTo: q.dateTo,
|
|
3142
|
-
limit: q.limit ? parseInt(q.limit, 10) : void 0,
|
|
3143
|
-
filters: q.filters ? JSON.parse(q.filters) : void 0,
|
|
3144
|
-
compare: q.compare === "true" || q.compare === "1"
|
|
3145
|
-
};
|
|
3146
|
-
}
|
|
3147
3673
|
function sendJson(res, status, body) {
|
|
3148
3674
|
if (typeof res.status === "function" && typeof res.json === "function") {
|
|
3149
3675
|
res.status(status).json(body);
|