@litemetrics/node 0.2.0 → 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/dist/index.cjs +132 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +132 -66
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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";
|
|
@@ -260,8 +328,15 @@ CREATE TABLE IF NOT EXISTS ${SITES_TABLE} (
|
|
|
260
328
|
SETTINGS index_granularity = 8192
|
|
261
329
|
`;
|
|
262
330
|
function toCHDateTime(d) {
|
|
263
|
-
const
|
|
264
|
-
|
|
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}`;
|
|
265
340
|
}
|
|
266
341
|
function normalizedUtmSourceExpr() {
|
|
267
342
|
return `multiIf(
|
|
@@ -981,7 +1056,7 @@ var ClickHouseAdapter = class {
|
|
|
981
1056
|
dateTo: params.dateTo
|
|
982
1057
|
});
|
|
983
1058
|
const granularity = params.granularity ?? autoGranularity(period);
|
|
984
|
-
const bucketFn = this.granularityToClickHouseFunc(granularity);
|
|
1059
|
+
const bucketFn = this.granularityToClickHouseFunc(granularity, params.timezone);
|
|
985
1060
|
const dateFormat = granularityToDateFormat(granularity);
|
|
986
1061
|
const filter = buildFilterConditions(params.filters);
|
|
987
1062
|
const filterSql = filter.conditions.length > 0 ? ` AND ${filter.conditions.join(" AND ")}` : "";
|
|
@@ -1030,38 +1105,41 @@ var ClickHouseAdapter = class {
|
|
|
1030
1105
|
new Date(dateRange.to),
|
|
1031
1106
|
granularity,
|
|
1032
1107
|
dateFormat,
|
|
1033
|
-
mappedRows
|
|
1108
|
+
mappedRows,
|
|
1109
|
+
params.timezone
|
|
1034
1110
|
);
|
|
1035
1111
|
return { metric: params.metric, granularity, data };
|
|
1036
1112
|
}
|
|
1037
|
-
granularityToClickHouseFunc(g) {
|
|
1113
|
+
granularityToClickHouseFunc(g, timezone) {
|
|
1114
|
+
const safeTz = timezone && isValidTimezone(timezone) ? timezone : void 0;
|
|
1115
|
+
const tz = safeTz ? `, '${safeTz}'` : "";
|
|
1038
1116
|
switch (g) {
|
|
1039
1117
|
case "hour":
|
|
1040
|
-
return
|
|
1118
|
+
return `toStartOfHour(timestamp${tz})`;
|
|
1041
1119
|
case "day":
|
|
1042
|
-
return
|
|
1120
|
+
return `toStartOfDay(timestamp${tz})`;
|
|
1043
1121
|
case "week":
|
|
1044
|
-
return
|
|
1122
|
+
return `toStartOfWeek(timestamp, 1${tz})`;
|
|
1045
1123
|
// 1 = Monday
|
|
1046
1124
|
case "month":
|
|
1047
|
-
return
|
|
1125
|
+
return `toStartOfMonth(timestamp${tz})`;
|
|
1048
1126
|
}
|
|
1049
1127
|
}
|
|
1050
1128
|
convertClickHouseBucket(bucket, granularity) {
|
|
1051
|
-
const date =
|
|
1052
|
-
const y = date.
|
|
1053
|
-
const m = String(date.
|
|
1054
|
-
const d = String(date.
|
|
1055
|
-
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");
|
|
1056
1134
|
switch (granularity) {
|
|
1057
1135
|
case "hour":
|
|
1058
1136
|
return `${y}-${m}-${d}T${h}:00`;
|
|
1059
1137
|
case "day":
|
|
1060
1138
|
return `${y}-${m}-${d}`;
|
|
1061
1139
|
case "week": {
|
|
1062
|
-
const jan4 = new Date(y, 0, 4);
|
|
1063
|
-
const dayOfYear = Math.ceil((date.getTime() -
|
|
1064
|
-
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;
|
|
1065
1143
|
const weekNum = Math.ceil((dayOfYear + jan4Day - 1) / 7);
|
|
1066
1144
|
return `${y}-W${String(weekNum).padStart(2, "0")}`;
|
|
1067
1145
|
}
|
|
@@ -1090,7 +1168,7 @@ var ClickHouseAdapter = class {
|
|
|
1090
1168
|
);
|
|
1091
1169
|
const cohortMap = /* @__PURE__ */ new Map();
|
|
1092
1170
|
for (const v of rows) {
|
|
1093
|
-
const firstDate =
|
|
1171
|
+
const firstDate = toUTCDate(v.first_event);
|
|
1094
1172
|
const cohortWeek = getISOWeek(firstDate);
|
|
1095
1173
|
if (!cohortMap.has(cohortWeek)) {
|
|
1096
1174
|
cohortMap.set(cohortWeek, { visitors: /* @__PURE__ */ new Set(), weekSets: /* @__PURE__ */ new Map() });
|
|
@@ -1098,7 +1176,7 @@ var ClickHouseAdapter = class {
|
|
|
1098
1176
|
const cohort = cohortMap.get(cohortWeek);
|
|
1099
1177
|
cohort.visitors.add(v.visitor_id);
|
|
1100
1178
|
const eventWeeks = (Array.isArray(v.active_weeks) ? v.active_weeks : []).map((w) => {
|
|
1101
|
-
const d =
|
|
1179
|
+
const d = toUTCDate(w);
|
|
1102
1180
|
return getISOWeek(d);
|
|
1103
1181
|
});
|
|
1104
1182
|
for (const w of eventWeeks) {
|
|
@@ -1265,8 +1343,8 @@ var ClickHouseAdapter = class {
|
|
|
1265
1343
|
visitorId: String(u.visitor_id),
|
|
1266
1344
|
userId: u.userId ? String(u.userId) : void 0,
|
|
1267
1345
|
traits: this.parseJSON(u.traits),
|
|
1268
|
-
firstSeen:
|
|
1269
|
-
lastSeen:
|
|
1346
|
+
firstSeen: toUTCDate(String(u.firstSeen)).toISOString(),
|
|
1347
|
+
lastSeen: toUTCDate(String(u.lastSeen)).toISOString(),
|
|
1270
1348
|
totalEvents: Number(u.totalEvents),
|
|
1271
1349
|
totalPageviews: Number(u.totalPageviews),
|
|
1272
1350
|
totalSessions: Number(u.totalSessions),
|
|
@@ -1389,8 +1467,8 @@ var ClickHouseAdapter = class {
|
|
|
1389
1467
|
visitorIds,
|
|
1390
1468
|
userId,
|
|
1391
1469
|
traits: this.parseJSON(u.traits),
|
|
1392
|
-
firstSeen:
|
|
1393
|
-
lastSeen:
|
|
1470
|
+
firstSeen: toUTCDate(String(u.firstSeen)).toISOString(),
|
|
1471
|
+
lastSeen: toUTCDate(String(u.lastSeen)).toISOString(),
|
|
1394
1472
|
totalEvents: Number(u.totalEvents),
|
|
1395
1473
|
totalPageviews: Number(u.totalPageviews),
|
|
1396
1474
|
totalSessions: Number(u.totalSessions),
|
|
@@ -1664,15 +1742,15 @@ var ClickHouseAdapter = class {
|
|
|
1664
1742
|
domain: row.domain ? String(row.domain) : void 0,
|
|
1665
1743
|
allowedOrigins: row.allowed_origins ? JSON.parse(String(row.allowed_origins)) : void 0,
|
|
1666
1744
|
conversionEvents: row.conversion_events ? JSON.parse(String(row.conversion_events)) : void 0,
|
|
1667
|
-
createdAt:
|
|
1668
|
-
updatedAt:
|
|
1745
|
+
createdAt: toUTCDate(String(row.created_at)).toISOString(),
|
|
1746
|
+
updatedAt: toUTCDate(String(row.updated_at)).toISOString()
|
|
1669
1747
|
};
|
|
1670
1748
|
}
|
|
1671
1749
|
toEventListItem(row) {
|
|
1672
1750
|
return {
|
|
1673
1751
|
id: String(row.event_id ?? ""),
|
|
1674
1752
|
type: String(row.type),
|
|
1675
|
-
timestamp:
|
|
1753
|
+
timestamp: toUTCDate(String(row.timestamp)).toISOString(),
|
|
1676
1754
|
visitorId: String(row.visitor_id),
|
|
1677
1755
|
sessionId: String(row.session_id),
|
|
1678
1756
|
url: row.url ? String(row.url) : void 0,
|
|
@@ -3328,7 +3406,8 @@ async function createCollector(config) {
|
|
|
3328
3406
|
dateFrom: params.dateFrom,
|
|
3329
3407
|
dateTo: params.dateTo,
|
|
3330
3408
|
granularity: q.granularity,
|
|
3331
|
-
filters: q.filters ? JSON.parse(q.filters) : void 0
|
|
3409
|
+
filters: q.filters ? JSON.parse(q.filters) : void 0,
|
|
3410
|
+
timezone: params.timezone
|
|
3332
3411
|
};
|
|
3333
3412
|
if (tsParams.metric === "conversions") {
|
|
3334
3413
|
const site = await db.getSite(params.siteId);
|
|
@@ -3630,19 +3709,6 @@ async function parseBody(req) {
|
|
|
3630
3709
|
req.on("error", reject);
|
|
3631
3710
|
});
|
|
3632
3711
|
}
|
|
3633
|
-
function extractQueryParams(req) {
|
|
3634
|
-
const q = req.query ?? Object.fromEntries(new URL(req.url, "http://localhost").searchParams);
|
|
3635
|
-
return {
|
|
3636
|
-
siteId: q.siteId,
|
|
3637
|
-
metric: q.metric,
|
|
3638
|
-
period: q.period,
|
|
3639
|
-
dateFrom: q.dateFrom,
|
|
3640
|
-
dateTo: q.dateTo,
|
|
3641
|
-
limit: q.limit ? parseInt(q.limit, 10) : void 0,
|
|
3642
|
-
filters: q.filters ? JSON.parse(q.filters) : void 0,
|
|
3643
|
-
compare: q.compare === "true" || q.compare === "1"
|
|
3644
|
-
};
|
|
3645
|
-
}
|
|
3646
3712
|
function sendJson(res, status, body) {
|
|
3647
3713
|
if (typeof res.status === "function" && typeof res.json === "function") {
|
|
3648
3714
|
res.status(status).json(body);
|