@jayfarei/lazyanalytics 0.1.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/CHANGELOG.md +32 -0
- package/README.md +67 -9
- package/cli/dist/commands/active.d.ts +2 -0
- package/cli/dist/commands/active.js +30 -0
- package/cli/dist/commands/base.js +9 -83
- package/cli/dist/commands/setup.js +15 -2
- package/cli/dist/index.js +11 -0
- package/cli/dist/lib/api.d.ts +10 -0
- package/cli/dist/lib/api.js +78 -3
- package/cli/dist/lib/wrangler.d.ts +2 -0
- package/cli/dist/lib/wrangler.js +26 -0
- package/dist/worker.js +724 -52
- package/package.json +1 -1
- package/skill/SKILL.md +15 -1
- package/templates/wrangler.toml +3 -0
package/dist/worker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// node_modules/hono/dist/compose.js
|
|
2
2
|
var compose = (middleware, onError, onNotFound) => {
|
|
3
3
|
return (context, next) => {
|
|
4
4
|
let index = -1;
|
|
@@ -42,10 +42,10 @@ var compose = (middleware, onError, onNotFound) => {
|
|
|
42
42
|
};
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// node_modules/hono/dist/request/constants.js
|
|
46
46
|
var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// node_modules/hono/dist/utils/body.js
|
|
49
49
|
var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
|
|
50
50
|
const { all = false, dot = false } = options;
|
|
51
51
|
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers;
|
|
@@ -117,7 +117,7 @@ var handleParsingNestedValues = (form, key, value) => {
|
|
|
117
117
|
});
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
//
|
|
120
|
+
// node_modules/hono/dist/utils/url.js
|
|
121
121
|
var splitPath = (path) => {
|
|
122
122
|
const paths = path.split("/");
|
|
123
123
|
if (paths[0] === "") {
|
|
@@ -321,7 +321,7 @@ var getQueryParams = (url, key) => {
|
|
|
321
321
|
};
|
|
322
322
|
var decodeURIComponent_ = decodeURIComponent;
|
|
323
323
|
|
|
324
|
-
//
|
|
324
|
+
// node_modules/hono/dist/request.js
|
|
325
325
|
var tryDecodeURIComponent = (str) => tryDecode(str, decodeURIComponent_);
|
|
326
326
|
var HonoRequest = class {
|
|
327
327
|
/**
|
|
@@ -466,6 +466,21 @@ var HonoRequest = class {
|
|
|
466
466
|
arrayBuffer() {
|
|
467
467
|
return this.#cachedBody("arrayBuffer");
|
|
468
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* `.bytes()` parses the request body as a `Uint8Array`.
|
|
471
|
+
*
|
|
472
|
+
* @see {@link https://hono.dev/docs/api/request#bytes}
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* ```ts
|
|
476
|
+
* app.post('/entry', async (c) => {
|
|
477
|
+
* const body = await c.req.bytes()
|
|
478
|
+
* })
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
bytes() {
|
|
482
|
+
return this.#cachedBody("arrayBuffer").then((buffer) => new Uint8Array(buffer));
|
|
483
|
+
}
|
|
469
484
|
/**
|
|
470
485
|
* Parses the request body as a `Blob`.
|
|
471
486
|
* @example
|
|
@@ -589,7 +604,7 @@ var HonoRequest = class {
|
|
|
589
604
|
}
|
|
590
605
|
};
|
|
591
606
|
|
|
592
|
-
//
|
|
607
|
+
// node_modules/hono/dist/utils/html.js
|
|
593
608
|
var HtmlEscapedCallbackPhase = {
|
|
594
609
|
Stringify: 1,
|
|
595
610
|
BeforeStream: 2,
|
|
@@ -631,7 +646,7 @@ var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) =>
|
|
|
631
646
|
}
|
|
632
647
|
};
|
|
633
648
|
|
|
634
|
-
//
|
|
649
|
+
// node_modules/hono/dist/context.js
|
|
635
650
|
var TEXT_PLAIN = "text/plain; charset=UTF-8";
|
|
636
651
|
var setDefaultContentType = (contentType, headers) => {
|
|
637
652
|
return {
|
|
@@ -1038,7 +1053,7 @@ var Context = class {
|
|
|
1038
1053
|
};
|
|
1039
1054
|
};
|
|
1040
1055
|
|
|
1041
|
-
//
|
|
1056
|
+
// node_modules/hono/dist/router.js
|
|
1042
1057
|
var METHOD_NAME_ALL = "ALL";
|
|
1043
1058
|
var METHOD_NAME_ALL_LOWERCASE = "all";
|
|
1044
1059
|
var METHODS = ["get", "post", "put", "delete", "options", "patch"];
|
|
@@ -1046,10 +1061,10 @@ var MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is
|
|
|
1046
1061
|
var UnsupportedPathError = class extends Error {
|
|
1047
1062
|
};
|
|
1048
1063
|
|
|
1049
|
-
//
|
|
1064
|
+
// node_modules/hono/dist/utils/constants.js
|
|
1050
1065
|
var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
|
|
1051
1066
|
|
|
1052
|
-
//
|
|
1067
|
+
// node_modules/hono/dist/hono-base.js
|
|
1053
1068
|
var notFoundHandler = (c) => {
|
|
1054
1069
|
return c.text("404 Not Found", 404);
|
|
1055
1070
|
};
|
|
@@ -1164,7 +1179,7 @@ var Hono = class _Hono {
|
|
|
1164
1179
|
handler = async (c, next) => (await compose([], app2.errorHandler)(c, () => r.handler(c, next))).res;
|
|
1165
1180
|
handler[COMPOSED_HANDLER] = r.handler;
|
|
1166
1181
|
}
|
|
1167
|
-
subApp.#addRoute(r.method, r.path, handler);
|
|
1182
|
+
subApp.#addRoute(r.method, r.path, handler, r.basePath);
|
|
1168
1183
|
});
|
|
1169
1184
|
return this;
|
|
1170
1185
|
}
|
|
@@ -1288,7 +1303,7 @@ var Hono = class _Hono {
|
|
|
1288
1303
|
const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
|
|
1289
1304
|
return (request) => {
|
|
1290
1305
|
const url = new URL(request.url);
|
|
1291
|
-
url.pathname =
|
|
1306
|
+
url.pathname = this.getPath(request).slice(pathPrefixLength) || "/";
|
|
1292
1307
|
return new Request(url, request);
|
|
1293
1308
|
};
|
|
1294
1309
|
})();
|
|
@@ -1302,10 +1317,15 @@ var Hono = class _Hono {
|
|
|
1302
1317
|
this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler);
|
|
1303
1318
|
return this;
|
|
1304
1319
|
}
|
|
1305
|
-
#addRoute(method, path, handler) {
|
|
1320
|
+
#addRoute(method, path, handler, baseRoutePath) {
|
|
1306
1321
|
method = method.toUpperCase();
|
|
1307
1322
|
path = mergePath(this._basePath, path);
|
|
1308
|
-
const r = {
|
|
1323
|
+
const r = {
|
|
1324
|
+
basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
|
|
1325
|
+
path,
|
|
1326
|
+
method,
|
|
1327
|
+
handler
|
|
1328
|
+
};
|
|
1309
1329
|
this.router.add(method, path, [handler, r]);
|
|
1310
1330
|
this.routes.push(r);
|
|
1311
1331
|
}
|
|
@@ -1420,7 +1440,7 @@ var Hono = class _Hono {
|
|
|
1420
1440
|
};
|
|
1421
1441
|
};
|
|
1422
1442
|
|
|
1423
|
-
//
|
|
1443
|
+
// node_modules/hono/dist/router/reg-exp-router/matcher.js
|
|
1424
1444
|
var emptyParam = [];
|
|
1425
1445
|
function match(method, path) {
|
|
1426
1446
|
const matchers = this.buildAllMatchers();
|
|
@@ -1441,7 +1461,7 @@ function match(method, path) {
|
|
|
1441
1461
|
return match2(method, path);
|
|
1442
1462
|
}
|
|
1443
1463
|
|
|
1444
|
-
//
|
|
1464
|
+
// node_modules/hono/dist/router/reg-exp-router/node.js
|
|
1445
1465
|
var LABEL_REG_EXP_STR = "[^/]+";
|
|
1446
1466
|
var ONLY_WILDCARD_REG_EXP_STR = ".*";
|
|
1447
1467
|
var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
|
|
@@ -1549,7 +1569,7 @@ var Node = class _Node {
|
|
|
1549
1569
|
}
|
|
1550
1570
|
};
|
|
1551
1571
|
|
|
1552
|
-
//
|
|
1572
|
+
// node_modules/hono/dist/router/reg-exp-router/trie.js
|
|
1553
1573
|
var Trie = class {
|
|
1554
1574
|
#context = { varIndex: 0 };
|
|
1555
1575
|
#root = new Node();
|
|
@@ -1605,7 +1625,7 @@ var Trie = class {
|
|
|
1605
1625
|
}
|
|
1606
1626
|
};
|
|
1607
1627
|
|
|
1608
|
-
//
|
|
1628
|
+
// node_modules/hono/dist/router/reg-exp-router/router.js
|
|
1609
1629
|
var nullMatcher = [/^$/, [], /* @__PURE__ */ Object.create(null)];
|
|
1610
1630
|
var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
|
|
1611
1631
|
function buildWildcardRegExp(path) {
|
|
@@ -1784,7 +1804,7 @@ var RegExpRouter = class {
|
|
|
1784
1804
|
}
|
|
1785
1805
|
};
|
|
1786
1806
|
|
|
1787
|
-
//
|
|
1807
|
+
// node_modules/hono/dist/router/smart-router/router.js
|
|
1788
1808
|
var SmartRouter = class {
|
|
1789
1809
|
name = "SmartRouter";
|
|
1790
1810
|
#routers = [];
|
|
@@ -1839,7 +1859,7 @@ var SmartRouter = class {
|
|
|
1839
1859
|
}
|
|
1840
1860
|
};
|
|
1841
1861
|
|
|
1842
|
-
//
|
|
1862
|
+
// node_modules/hono/dist/router/trie-router/node.js
|
|
1843
1863
|
var emptyParams = /* @__PURE__ */ Object.create(null);
|
|
1844
1864
|
var hasChildren = (children) => {
|
|
1845
1865
|
for (const _ in children) {
|
|
@@ -2014,7 +2034,7 @@ var Node2 = class _Node2 {
|
|
|
2014
2034
|
}
|
|
2015
2035
|
};
|
|
2016
2036
|
|
|
2017
|
-
//
|
|
2037
|
+
// node_modules/hono/dist/router/trie-router/router.js
|
|
2018
2038
|
var TrieRouter = class {
|
|
2019
2039
|
name = "TrieRouter";
|
|
2020
2040
|
#node;
|
|
@@ -2036,7 +2056,7 @@ var TrieRouter = class {
|
|
|
2036
2056
|
}
|
|
2037
2057
|
};
|
|
2038
2058
|
|
|
2039
|
-
//
|
|
2059
|
+
// node_modules/hono/dist/hono.js
|
|
2040
2060
|
var Hono2 = class extends Hono {
|
|
2041
2061
|
/**
|
|
2042
2062
|
* Creates an instance of the Hono class.
|
|
@@ -2051,24 +2071,18 @@ var Hono2 = class extends Hono {
|
|
|
2051
2071
|
}
|
|
2052
2072
|
};
|
|
2053
2073
|
|
|
2054
|
-
//
|
|
2074
|
+
// node_modules/hono/dist/middleware/cors/index.js
|
|
2055
2075
|
var cors = (options) => {
|
|
2056
|
-
const
|
|
2076
|
+
const opts = {
|
|
2057
2077
|
origin: "*",
|
|
2058
2078
|
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
|
|
2059
2079
|
allowHeaders: [],
|
|
2060
|
-
exposeHeaders: []
|
|
2061
|
-
};
|
|
2062
|
-
const opts = {
|
|
2063
|
-
...defaults,
|
|
2080
|
+
exposeHeaders: [],
|
|
2064
2081
|
...options
|
|
2065
2082
|
};
|
|
2066
2083
|
const findAllowOrigin = ((optsOrigin) => {
|
|
2067
2084
|
if (typeof optsOrigin === "string") {
|
|
2068
2085
|
if (optsOrigin === "*") {
|
|
2069
|
-
if (opts.credentials) {
|
|
2070
|
-
return (origin) => origin || null;
|
|
2071
|
-
}
|
|
2072
2086
|
return () => optsOrigin;
|
|
2073
2087
|
} else {
|
|
2074
2088
|
return (origin) => optsOrigin === origin ? origin : null;
|
|
@@ -2103,7 +2117,7 @@ var cors = (options) => {
|
|
|
2103
2117
|
set("Access-Control-Expose-Headers", opts.exposeHeaders.join(","));
|
|
2104
2118
|
}
|
|
2105
2119
|
if (c.req.method === "OPTIONS") {
|
|
2106
|
-
if (opts.origin !== "*"
|
|
2120
|
+
if (opts.origin !== "*") {
|
|
2107
2121
|
set("Vary", "Origin");
|
|
2108
2122
|
}
|
|
2109
2123
|
if (opts.maxAge != null) {
|
|
@@ -2133,7 +2147,7 @@ var cors = (options) => {
|
|
|
2133
2147
|
});
|
|
2134
2148
|
}
|
|
2135
2149
|
await next();
|
|
2136
|
-
if (opts.origin !== "*"
|
|
2150
|
+
if (opts.origin !== "*") {
|
|
2137
2151
|
c.header("Vary", "Origin", { append: true });
|
|
2138
2152
|
}
|
|
2139
2153
|
};
|
|
@@ -2182,8 +2196,31 @@ async function hashVisitor(site, ip, ua, date, salt) {
|
|
|
2182
2196
|
function todayUTC() {
|
|
2183
2197
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2184
2198
|
}
|
|
2199
|
+
function sessionWindow(epochMs, slotMs = 30 * 60 * 1e3) {
|
|
2200
|
+
return String(Math.floor(epochMs / slotMs));
|
|
2201
|
+
}
|
|
2185
2202
|
|
|
2186
|
-
// worker/src/lib/
|
|
2203
|
+
// worker/src/lib/crawlers.ts
|
|
2204
|
+
var AI_RULES = [
|
|
2205
|
+
{ pattern: /chatgpt-user/i, name: "ChatGPT-User", operator: "OpenAI", type: "user" },
|
|
2206
|
+
{ pattern: /oai-searchbot/i, name: "OAI-SearchBot", operator: "OpenAI", type: "search" },
|
|
2207
|
+
{ pattern: /gptbot/i, name: "GPTBot", operator: "OpenAI", type: "train" },
|
|
2208
|
+
{ pattern: /claude-user/i, name: "Claude-User", operator: "Anthropic", type: "user" },
|
|
2209
|
+
{ pattern: /claude-searchbot/i, name: "Claude-SearchBot", operator: "Anthropic", type: "search" },
|
|
2210
|
+
{ pattern: /claude-web/i, name: "Claude-Web", operator: "Anthropic", type: "agent" },
|
|
2211
|
+
{ pattern: /claudebot/i, name: "ClaudeBot", operator: "Anthropic", type: "train" },
|
|
2212
|
+
{ pattern: /anthropic-ai/i, name: "Anthropic-AI", operator: "Anthropic", type: "train" },
|
|
2213
|
+
{ pattern: /perplexity-user/i, name: "Perplexity-User", operator: "Perplexity", type: "user" },
|
|
2214
|
+
{ pattern: /perplexitybot/i, name: "PerplexityBot", operator: "Perplexity", type: "search" },
|
|
2215
|
+
{ pattern: /meta-externalfetcher/i, name: "meta-externalfetcher", operator: "Meta", type: "user" },
|
|
2216
|
+
{ pattern: /meta-externalagent/i, name: "meta-externalagent", operator: "Meta", type: "agent" },
|
|
2217
|
+
{ pattern: /google-extended/i, name: "Google-Extended", operator: "Google", type: "train" },
|
|
2218
|
+
{ pattern: /googleother/i, name: "GoogleOther", operator: "Google", type: "train" },
|
|
2219
|
+
{ pattern: /gemini/i, name: "Gemini", operator: "Google", type: "agent" },
|
|
2220
|
+
{ pattern: /copilot/i, name: "Copilot", operator: "Microsoft", type: "coding" },
|
|
2221
|
+
{ pattern: /bytespider/i, name: "Bytespider", operator: "ByteDance", type: "train" },
|
|
2222
|
+
{ pattern: /ccbot/i, name: "CCBot", operator: "Common Crawl", type: "train" }
|
|
2223
|
+
];
|
|
2187
2224
|
var BOT_PATTERNS = [
|
|
2188
2225
|
/bot/i,
|
|
2189
2226
|
/crawl/i,
|
|
@@ -2210,10 +2247,6 @@ var BOT_PATTERNS = [
|
|
|
2210
2247
|
/puppeteer/i,
|
|
2211
2248
|
/lighthouse/i,
|
|
2212
2249
|
/pagespeed/i,
|
|
2213
|
-
/gptbot/i,
|
|
2214
|
-
/claudebot/i,
|
|
2215
|
-
/anthropic/i,
|
|
2216
|
-
/chatgpt/i,
|
|
2217
2250
|
/prerender/i,
|
|
2218
2251
|
/preview/i,
|
|
2219
2252
|
/wget/i,
|
|
@@ -2224,9 +2257,87 @@ var BOT_PATTERNS = [
|
|
|
2224
2257
|
/node-fetch/i,
|
|
2225
2258
|
/go-http-client/i
|
|
2226
2259
|
];
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2260
|
+
var BOT = { kind: "bot", name: "", operator: "", type: "" };
|
|
2261
|
+
var HUMAN = { kind: "human", name: "", operator: "", type: "" };
|
|
2262
|
+
function classifyCrawler(ua) {
|
|
2263
|
+
try {
|
|
2264
|
+
const value = typeof ua === "string" ? ua : String(ua);
|
|
2265
|
+
if (!value || value.length < 10) return BOT;
|
|
2266
|
+
for (const rule of AI_RULES) {
|
|
2267
|
+
if (rule.pattern.test(value)) {
|
|
2268
|
+
return { kind: "ai", name: rule.name, operator: rule.operator, type: rule.type };
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
if (BOT_PATTERNS.some((pattern) => pattern.test(value))) return BOT;
|
|
2272
|
+
return HUMAN;
|
|
2273
|
+
} catch {
|
|
2274
|
+
return BOT;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// worker/src/lib/channels.ts
|
|
2279
|
+
var SEARCH_DOMAINS = [
|
|
2280
|
+
"google.",
|
|
2281
|
+
"bing.com",
|
|
2282
|
+
"duckduckgo.com",
|
|
2283
|
+
"yahoo.",
|
|
2284
|
+
"baidu.com",
|
|
2285
|
+
"yandex.",
|
|
2286
|
+
"ecosia.org",
|
|
2287
|
+
"brave.com",
|
|
2288
|
+
"search.aol.",
|
|
2289
|
+
"naver.com",
|
|
2290
|
+
"seznam.",
|
|
2291
|
+
"qwant.com"
|
|
2292
|
+
];
|
|
2293
|
+
var SOCIAL_DOMAINS = [
|
|
2294
|
+
"facebook.com",
|
|
2295
|
+
"instagram.com",
|
|
2296
|
+
"threads.net",
|
|
2297
|
+
"twitter.com",
|
|
2298
|
+
"x.com",
|
|
2299
|
+
"linkedin.com",
|
|
2300
|
+
"t.co",
|
|
2301
|
+
"reddit.com",
|
|
2302
|
+
"pinterest.",
|
|
2303
|
+
"youtube.com",
|
|
2304
|
+
"tiktok.com",
|
|
2305
|
+
"bsky.app",
|
|
2306
|
+
"mastodon."
|
|
2307
|
+
];
|
|
2308
|
+
var AI_DOMAINS = [
|
|
2309
|
+
"chatgpt.com",
|
|
2310
|
+
"openai.com",
|
|
2311
|
+
"claude.ai",
|
|
2312
|
+
"anthropic.com",
|
|
2313
|
+
"perplexity.ai",
|
|
2314
|
+
"gemini.google.com",
|
|
2315
|
+
"copilot.microsoft.com"
|
|
2316
|
+
];
|
|
2317
|
+
var EMAIL_SOURCES = ["mailchimp", "sendgrid", "postmark", "customer.io", "convertkit", "substack"];
|
|
2318
|
+
var PAID_MEDIUM_RE = /^(.*cp.*|ppc|retargeting|paid.*)$/i;
|
|
2319
|
+
function includesAny(value, needles) {
|
|
2320
|
+
return needles.some((needle) => value.includes(needle));
|
|
2321
|
+
}
|
|
2322
|
+
function classifyChannel(referrerDomain, utmSource = "", utmMedium = "") {
|
|
2323
|
+
try {
|
|
2324
|
+
const ref = String(referrerDomain || "").toLowerCase();
|
|
2325
|
+
const source = String(utmSource || "").toLowerCase();
|
|
2326
|
+
const medium = String(utmMedium || "").toLowerCase();
|
|
2327
|
+
const joined = `${ref} ${source}`;
|
|
2328
|
+
if (includesAny(joined, AI_DOMAINS)) return "AI Assistants";
|
|
2329
|
+
if (medium === "email" || includesAny(source, EMAIL_SOURCES)) return "Email";
|
|
2330
|
+
if (PAID_MEDIUM_RE.test(medium) && (includesAny(joined, SEARCH_DOMAINS) || medium.includes("search"))) {
|
|
2331
|
+
return "Paid Search";
|
|
2332
|
+
}
|
|
2333
|
+
if (includesAny(joined, SEARCH_DOMAINS)) return "Organic Search";
|
|
2334
|
+
if (PAID_MEDIUM_RE.test(medium) && includesAny(joined, SOCIAL_DOMAINS)) return "Paid Social";
|
|
2335
|
+
if (includesAny(joined, SOCIAL_DOMAINS)) return "Organic Social";
|
|
2336
|
+
if (ref || medium === "referral" || medium === "link") return "Referral";
|
|
2337
|
+
return "Direct";
|
|
2338
|
+
} catch {
|
|
2339
|
+
return "Direct";
|
|
2340
|
+
}
|
|
2230
2341
|
}
|
|
2231
2342
|
|
|
2232
2343
|
// worker/src/collect.ts
|
|
@@ -2235,7 +2346,11 @@ async function collect(c) {
|
|
|
2235
2346
|
return c.text("Method not allowed", 405);
|
|
2236
2347
|
}
|
|
2237
2348
|
const ua = c.req.header("User-Agent") || "";
|
|
2238
|
-
|
|
2349
|
+
const cls = classifyCrawler(ua);
|
|
2350
|
+
if (cls.kind === "bot") {
|
|
2351
|
+
return c.newResponse(null, 204);
|
|
2352
|
+
}
|
|
2353
|
+
if (cls.kind === "ai" && c.env.TRACK_AI_CRAWLERS !== "true") {
|
|
2239
2354
|
return c.newResponse(null, 204);
|
|
2240
2355
|
}
|
|
2241
2356
|
let payload;
|
|
@@ -2278,7 +2393,12 @@ async function collect(c) {
|
|
|
2278
2393
|
const ip = c.req.header("CF-Connecting-IP") || c.req.header("X-Forwarded-For") || "unknown";
|
|
2279
2394
|
const country = c.req.header("CF-IPCountry") || "XX";
|
|
2280
2395
|
const parsed = parseUA(ua);
|
|
2396
|
+
const channel = classifyChannel(referrerDomain, payload.us || "", payload.um || "");
|
|
2397
|
+
const now = Date.now();
|
|
2281
2398
|
const visitorHash = await hashVisitor(payload.sid, ip, ua, todayUTC(), c.env.HASH_SALT);
|
|
2399
|
+
const sessionHash = await hashVisitor(payload.sid, ip, ua, sessionWindow(now), c.env.HASH_SALT);
|
|
2400
|
+
const eventType = payload.t === "eng" ? "eng" : "pv";
|
|
2401
|
+
const engagementMs = Math.max(0, Math.round(Number(payload.em) || 0));
|
|
2282
2402
|
c.env.ANALYTICS.writeDataPoint({
|
|
2283
2403
|
indexes: [visitorHash],
|
|
2284
2404
|
blobs: [
|
|
@@ -2298,14 +2418,32 @@ async function collect(c) {
|
|
|
2298
2418
|
// blob7: device type
|
|
2299
2419
|
payload.us || "",
|
|
2300
2420
|
// blob8: utm_source
|
|
2301
|
-
payload.um || ""
|
|
2421
|
+
payload.um || "",
|
|
2302
2422
|
// blob9: utm_medium
|
|
2423
|
+
cls.kind === "ai" ? "ai" : "",
|
|
2424
|
+
// blob10: traffic_class
|
|
2425
|
+
channel,
|
|
2426
|
+
// blob11: channel
|
|
2427
|
+
sessionHash,
|
|
2428
|
+
// blob12: session_id
|
|
2429
|
+
cls.name || "",
|
|
2430
|
+
// blob13: crawler_name
|
|
2431
|
+
cls.operator || "",
|
|
2432
|
+
// blob14: crawler_operator
|
|
2433
|
+
cls.type || "",
|
|
2434
|
+
// blob15: crawler_type
|
|
2435
|
+
eventType
|
|
2436
|
+
// blob16: event_type
|
|
2303
2437
|
],
|
|
2304
2438
|
doubles: [
|
|
2305
2439
|
1,
|
|
2306
2440
|
// double1: count
|
|
2307
|
-
payload.sw || 0
|
|
2441
|
+
payload.sw || 0,
|
|
2308
2442
|
// double2: screen width
|
|
2443
|
+
0,
|
|
2444
|
+
// double3: reserved
|
|
2445
|
+
engagementMs
|
|
2446
|
+
// double4: engagement_ms
|
|
2309
2447
|
]
|
|
2310
2448
|
});
|
|
2311
2449
|
return c.newResponse(null, 204);
|
|
@@ -2314,7 +2452,7 @@ async function collect(c) {
|
|
|
2314
2452
|
// worker/src/tracker.ts
|
|
2315
2453
|
var TRACKER_SCRIPT = `(function(){
|
|
2316
2454
|
"use strict";
|
|
2317
|
-
var d=document,w=window,l=d.currentScript;
|
|
2455
|
+
var d=document,w=window,l=d.currentScript,t0=0,sent=0,last="";
|
|
2318
2456
|
if(!l)return;
|
|
2319
2457
|
var sid=l.getAttribute("data-site-id");
|
|
2320
2458
|
if(!sid)return;
|
|
@@ -2349,9 +2487,12 @@ var TRACKER_SCRIPT = `(function(){
|
|
|
2349
2487
|
}
|
|
2350
2488
|
|
|
2351
2489
|
function track(){
|
|
2490
|
+
t0=performance&&performance.now?performance.now():Date.now();
|
|
2491
|
+
sent=0;
|
|
2492
|
+
last=stripUrl(w.location.href);
|
|
2352
2493
|
send({
|
|
2353
2494
|
sid:sid,
|
|
2354
|
-
url:
|
|
2495
|
+
url:last,
|
|
2355
2496
|
ref:stripRef(d.referrer),
|
|
2356
2497
|
sw:w.screen?w.screen.width:0,
|
|
2357
2498
|
us:getUTM("utm_source"),
|
|
@@ -2359,6 +2500,13 @@ var TRACKER_SCRIPT = `(function(){
|
|
|
2359
2500
|
});
|
|
2360
2501
|
}
|
|
2361
2502
|
|
|
2503
|
+
function dwell(){
|
|
2504
|
+
if(sent||!last)return;
|
|
2505
|
+
sent=1;
|
|
2506
|
+
var now=performance&&performance.now?performance.now():Date.now();
|
|
2507
|
+
send({sid:sid,url:last,em:Math.max(0,Math.round(now-t0)),t:"eng"});
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2362
2510
|
// Track on page load
|
|
2363
2511
|
if(d.readyState==="complete"){
|
|
2364
2512
|
track();
|
|
@@ -2373,6 +2521,8 @@ var TRACKER_SCRIPT = `(function(){
|
|
|
2373
2521
|
setTimeout(track,10);
|
|
2374
2522
|
};
|
|
2375
2523
|
w.addEventListener("popstate",function(){setTimeout(track,10);});
|
|
2524
|
+
w.addEventListener("pagehide",dwell);
|
|
2525
|
+
d.addEventListener("visibilitychange",function(){if(d.visibilityState==="hidden")dwell();});
|
|
2376
2526
|
})();`;
|
|
2377
2527
|
function serveTracker(c) {
|
|
2378
2528
|
return c.newResponse(TRACKER_SCRIPT, 200, {
|
|
@@ -2968,14 +3118,20 @@ async function queryAE(env, sql) {
|
|
|
2968
3118
|
function formatTimestamp(d) {
|
|
2969
3119
|
return d.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
|
|
2970
3120
|
}
|
|
2971
|
-
|
|
3121
|
+
var HUMAN_TRAFFIC_PREDICATE = "(blob10 = '' OR blob10 IS NULL)";
|
|
3122
|
+
var PAGEVIEW_EVENT_PREDICATE = "(blob16 = 'pv' OR blob16 = '' OR blob16 IS NULL)";
|
|
3123
|
+
function buildWhereClause(opts, filters = {}) {
|
|
2972
3124
|
const site = opts.site.replace(/'/g, "''");
|
|
2973
3125
|
const start = formatTimestamp(opts.startAt);
|
|
2974
3126
|
const end = formatTimestamp(opts.endAt);
|
|
2975
|
-
|
|
3127
|
+
const trafficClass = filters.trafficClass ?? "human";
|
|
3128
|
+
const trafficClause = trafficClass === "human" ? ` AND ${HUMAN_TRAFFIC_PREDICATE}` : trafficClass === "ai" ? " AND blob10 = 'ai'" : "";
|
|
3129
|
+
const eventType = filters.eventType ?? "pv";
|
|
3130
|
+
const eventClause = eventType === "pv" ? ` AND ${PAGEVIEW_EVENT_PREDICATE}` : "";
|
|
3131
|
+
return `WHERE blob1 = '${site}' AND timestamp >= toDateTime('${start}') AND timestamp <= toDateTime('${end}')${trafficClause}${eventClause}`;
|
|
2976
3132
|
}
|
|
2977
|
-
function envelope(data, site, period, sampled) {
|
|
2978
|
-
return { data, meta: { site, period, sampled } };
|
|
3133
|
+
function envelope(data, site, period, sampled, extraMeta) {
|
|
3134
|
+
return { data, meta: { site, period, sampled, ...extraMeta || {} } };
|
|
2979
3135
|
}
|
|
2980
3136
|
function extractSampled(rows) {
|
|
2981
3137
|
let sampled = false;
|
|
@@ -2996,6 +3152,52 @@ function extractParams(c, env) {
|
|
|
2996
3152
|
const { startAt, endAt } = parsePeriod(period);
|
|
2997
3153
|
return { site, period, limit, opts: { site, startAt, endAt } };
|
|
2998
3154
|
}
|
|
3155
|
+
function parseLongPeriod(params, now = /* @__PURE__ */ new Date()) {
|
|
3156
|
+
if (params.from || params.to) {
|
|
3157
|
+
if (!params.from || !params.to) {
|
|
3158
|
+
throw new Error("History requires both from and to when either is provided");
|
|
3159
|
+
}
|
|
3160
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(params.from) || !/^\d{4}-\d{2}-\d{2}$/.test(params.to)) {
|
|
3161
|
+
throw new Error("History from/to must be YYYY-MM-DD");
|
|
3162
|
+
}
|
|
3163
|
+
const startAt2 = /* @__PURE__ */ new Date(`${params.from}T00:00:00.000Z`);
|
|
3164
|
+
const endAt2 = /* @__PURE__ */ new Date(`${params.to}T23:59:59.999Z`);
|
|
3165
|
+
if (Number.isNaN(startAt2.getTime()) || Number.isNaN(endAt2.getTime()) || startAt2 > endAt2) {
|
|
3166
|
+
throw new Error("Invalid history date range");
|
|
3167
|
+
}
|
|
3168
|
+
return { startAt: startAt2, endAt: endAt2, period: `${params.from}..${params.to}` };
|
|
3169
|
+
}
|
|
3170
|
+
const days = params.days ? parseInt(params.days, 10) : 90;
|
|
3171
|
+
if (!Number.isFinite(days) || days < 1 || days > 3650) {
|
|
3172
|
+
throw new Error(`History days must be between 1 and 3650. Got: ${params.days}`);
|
|
3173
|
+
}
|
|
3174
|
+
const endAt = now;
|
|
3175
|
+
const startAt = new Date(Date.UTC(
|
|
3176
|
+
now.getUTCFullYear(),
|
|
3177
|
+
now.getUTCMonth(),
|
|
3178
|
+
now.getUTCDate() - days + 1
|
|
3179
|
+
));
|
|
3180
|
+
return { startAt, endAt, period: `${days}d` };
|
|
3181
|
+
}
|
|
3182
|
+
function utcDayKey(date) {
|
|
3183
|
+
return date.toISOString().slice(0, 10);
|
|
3184
|
+
}
|
|
3185
|
+
function utcDayBounds(day) {
|
|
3186
|
+
return {
|
|
3187
|
+
startAt: /* @__PURE__ */ new Date(`${day}T00:00:00.000Z`),
|
|
3188
|
+
endAt: /* @__PURE__ */ new Date(`${day}T23:59:59.999Z`)
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3191
|
+
function dayKeys(startAt, endAt) {
|
|
3192
|
+
const keys = [];
|
|
3193
|
+
const cursor = new Date(Date.UTC(startAt.getUTCFullYear(), startAt.getUTCMonth(), startAt.getUTCDate()));
|
|
3194
|
+
const last = new Date(Date.UTC(endAt.getUTCFullYear(), endAt.getUTCMonth(), endAt.getUTCDate()));
|
|
3195
|
+
while (cursor <= last) {
|
|
3196
|
+
keys.push(utcDayKey(cursor));
|
|
3197
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
3198
|
+
}
|
|
3199
|
+
return keys;
|
|
3200
|
+
}
|
|
2999
3201
|
|
|
3000
3202
|
// worker/src/api/stats.ts
|
|
3001
3203
|
async function statsHandler(c) {
|
|
@@ -3196,8 +3398,466 @@ async function sitesHandler(c) {
|
|
|
3196
3398
|
});
|
|
3197
3399
|
}
|
|
3198
3400
|
|
|
3401
|
+
// worker/src/api/active.ts
|
|
3402
|
+
function parseWindow(raw2) {
|
|
3403
|
+
const parsed = raw2 ? parseInt(raw2, 10) : 5;
|
|
3404
|
+
if (!Number.isFinite(parsed)) return 5;
|
|
3405
|
+
return Math.min(Math.max(parsed, 1), 60);
|
|
3406
|
+
}
|
|
3407
|
+
async function activeHandler(c) {
|
|
3408
|
+
try {
|
|
3409
|
+
const site = c.req.query("site");
|
|
3410
|
+
if (!site) throw new Error("Missing required parameter: site");
|
|
3411
|
+
if (!validateSite(site, c.env.ALLOWED_SITES)) throw new Error(`Unknown site: ${site}`);
|
|
3412
|
+
const windowMinutes = parseWindow(c.req.query("window"));
|
|
3413
|
+
const endAt = /* @__PURE__ */ new Date();
|
|
3414
|
+
const startAt = new Date(endAt.getTime() - windowMinutes * 60 * 1e3);
|
|
3415
|
+
const where = buildWhereClause({ site, startAt, endAt });
|
|
3416
|
+
const sql = `
|
|
3417
|
+
SELECT
|
|
3418
|
+
COUNT(DISTINCT index1) as active_visitors,
|
|
3419
|
+
SUM(_sample_interval) as recent_pageviews,
|
|
3420
|
+
MAX(_sample_interval) as max_interval
|
|
3421
|
+
FROM ${DATASET}
|
|
3422
|
+
${where}
|
|
3423
|
+
`;
|
|
3424
|
+
const result = await queryAE(c.env, sql);
|
|
3425
|
+
const { rows, sampled } = extractSampled(result.data);
|
|
3426
|
+
const row = rows[0] || { active_visitors: 0, recent_pageviews: 0 };
|
|
3427
|
+
return c.json(envelope({ ...row, window_minutes: windowMinutes }, site, `${windowMinutes}m`, sampled));
|
|
3428
|
+
} catch (e) {
|
|
3429
|
+
const msg = e instanceof Error && !e.message.includes("Analytics Engine") ? e.message : "Internal query error";
|
|
3430
|
+
return c.json({ error: msg }, 400);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
// worker/src/api/crawlers.ts
|
|
3435
|
+
var COLUMNS = {
|
|
3436
|
+
name: { column: "blob13", alias: "name" },
|
|
3437
|
+
operator: { column: "blob14", alias: "operator" },
|
|
3438
|
+
class: { column: "blob15", alias: "class" }
|
|
3439
|
+
};
|
|
3440
|
+
async function crawlersHandler(c) {
|
|
3441
|
+
try {
|
|
3442
|
+
const { site, period, limit, opts } = extractParams(c, c.env);
|
|
3443
|
+
const requested = c.req.query("type") || "name";
|
|
3444
|
+
const selected = COLUMNS[requested] || COLUMNS.name;
|
|
3445
|
+
const where = buildWhereClause(opts, { trafficClass: "ai" });
|
|
3446
|
+
const sql = `
|
|
3447
|
+
SELECT
|
|
3448
|
+
${selected.column} as ${selected.alias},
|
|
3449
|
+
SUM(_sample_interval) as hits,
|
|
3450
|
+
COUNT(DISTINCT index1) as approx_instances,
|
|
3451
|
+
MAX(_sample_interval) as max_interval
|
|
3452
|
+
FROM ${DATASET}
|
|
3453
|
+
${where}
|
|
3454
|
+
AND ${selected.column} != ''
|
|
3455
|
+
GROUP BY ${selected.column}
|
|
3456
|
+
ORDER BY hits DESC
|
|
3457
|
+
LIMIT ${limit}
|
|
3458
|
+
`;
|
|
3459
|
+
const result = await queryAE(c.env, sql);
|
|
3460
|
+
const { rows, sampled } = extractSampled(result.data);
|
|
3461
|
+
return c.json(envelope(rows, site, period, sampled));
|
|
3462
|
+
} catch (e) {
|
|
3463
|
+
const msg = e instanceof Error && !e.message.includes("Analytics Engine") ? e.message : "Internal query error";
|
|
3464
|
+
return c.json({ error: msg }, 400);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
// worker/src/api/channels.ts
|
|
3469
|
+
async function channelsHandler(c) {
|
|
3470
|
+
try {
|
|
3471
|
+
const { site, period, opts } = extractParams(c, c.env);
|
|
3472
|
+
const where = buildWhereClause(opts);
|
|
3473
|
+
const sql = `
|
|
3474
|
+
SELECT
|
|
3475
|
+
blob11 as channel,
|
|
3476
|
+
SUM(_sample_interval) as pageviews,
|
|
3477
|
+
COUNT(DISTINCT index1) as approx_visitors,
|
|
3478
|
+
MAX(_sample_interval) as max_interval
|
|
3479
|
+
FROM ${DATASET}
|
|
3480
|
+
${where}
|
|
3481
|
+
GROUP BY blob11
|
|
3482
|
+
ORDER BY pageviews DESC
|
|
3483
|
+
LIMIT 50
|
|
3484
|
+
`;
|
|
3485
|
+
const result = await queryAE(c.env, sql);
|
|
3486
|
+
const { rows, sampled } = extractSampled(result.data);
|
|
3487
|
+
const data = rows.map((row) => ({
|
|
3488
|
+
...row,
|
|
3489
|
+
channel: row.channel === "" || row.channel == null ? "unknown" : row.channel
|
|
3490
|
+
}));
|
|
3491
|
+
return c.json(envelope(data, site, period, sampled));
|
|
3492
|
+
} catch (e) {
|
|
3493
|
+
const msg = e instanceof Error && !e.message.includes("Analytics Engine") ? e.message : "Internal query error";
|
|
3494
|
+
return c.json({ error: msg }, 400);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
// worker/src/api/bounce.ts
|
|
3499
|
+
async function bounceHandler(c) {
|
|
3500
|
+
try {
|
|
3501
|
+
const { site, period, opts } = extractParams(c, c.env);
|
|
3502
|
+
const where = buildWhereClause(opts);
|
|
3503
|
+
const sql = `
|
|
3504
|
+
SELECT
|
|
3505
|
+
blob12 as session_key,
|
|
3506
|
+
SUM(_sample_interval) as pv_weight,
|
|
3507
|
+
SUM(double1) as pv_rows,
|
|
3508
|
+
MAX(_sample_interval) as max_interval
|
|
3509
|
+
FROM ${DATASET}
|
|
3510
|
+
${where}
|
|
3511
|
+
AND blob12 != ''
|
|
3512
|
+
GROUP BY blob12
|
|
3513
|
+
ORDER BY pv_weight DESC
|
|
3514
|
+
LIMIT 100000
|
|
3515
|
+
`;
|
|
3516
|
+
const result = await queryAE(c.env, sql);
|
|
3517
|
+
const sampled = result.data.some((row) => Number(row.max_interval) > 1);
|
|
3518
|
+
const approxSessions = result.data.length;
|
|
3519
|
+
const bouncedSessions = result.data.filter((row) => Number(row.pv_rows) === 1).length;
|
|
3520
|
+
const data = {
|
|
3521
|
+
bounce_rate: approxSessions > 0 && !sampled ? bouncedSessions / approxSessions : null,
|
|
3522
|
+
approx_sessions: approxSessions,
|
|
3523
|
+
bounced_sessions: bouncedSessions
|
|
3524
|
+
};
|
|
3525
|
+
if (sampled) data.warning = "bounce rate unreliable when sampled";
|
|
3526
|
+
return c.json(envelope(data, site, period, sampled));
|
|
3527
|
+
} catch (e) {
|
|
3528
|
+
const msg = e instanceof Error && !e.message.includes("Analytics Engine") ? e.message : "Internal query error";
|
|
3529
|
+
return c.json({ error: msg }, 400);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
// worker/src/api/duration.ts
|
|
3534
|
+
async function durationHandler(c) {
|
|
3535
|
+
try {
|
|
3536
|
+
const { site, period, opts } = extractParams(c, c.env);
|
|
3537
|
+
const where = buildWhereClause(opts, { eventType: "all" });
|
|
3538
|
+
const sql = `
|
|
3539
|
+
SELECT
|
|
3540
|
+
blob12 as session_key,
|
|
3541
|
+
toUInt32(MAX(timestamp)) - toUInt32(MIN(timestamp)) as span_s,
|
|
3542
|
+
MAX(double4) as dwell_ms,
|
|
3543
|
+
MAX(_sample_interval) as w,
|
|
3544
|
+
MAX(_sample_interval) as max_interval
|
|
3545
|
+
FROM ${DATASET}
|
|
3546
|
+
${where}
|
|
3547
|
+
AND blob12 != ''
|
|
3548
|
+
GROUP BY blob12
|
|
3549
|
+
ORDER BY w DESC
|
|
3550
|
+
LIMIT 100000
|
|
3551
|
+
`;
|
|
3552
|
+
const result = await queryAE(c.env, sql);
|
|
3553
|
+
const sampled = result.data.some((row) => Number(row.max_interval) > 1);
|
|
3554
|
+
let weightedSeconds = 0;
|
|
3555
|
+
let totalWeight = 0;
|
|
3556
|
+
for (const row of result.data) {
|
|
3557
|
+
const weight = Number(row.w) || 1;
|
|
3558
|
+
const spanSeconds = Number(row.span_s) || 0;
|
|
3559
|
+
const dwellSeconds = (Number(row.dwell_ms) || 0) / 1e3;
|
|
3560
|
+
weightedSeconds += Math.max(spanSeconds, dwellSeconds) * weight;
|
|
3561
|
+
totalWeight += weight;
|
|
3562
|
+
}
|
|
3563
|
+
const data = {
|
|
3564
|
+
avg_session_seconds: totalWeight > 0 ? weightedSeconds / totalWeight : 0,
|
|
3565
|
+
approx_sessions: result.data.length
|
|
3566
|
+
};
|
|
3567
|
+
return c.json(envelope(data, site, period, sampled));
|
|
3568
|
+
} catch (e) {
|
|
3569
|
+
const msg = e instanceof Error && !e.message.includes("Analytics Engine") ? e.message : "Internal query error";
|
|
3570
|
+
return c.json({ error: msg }, 400);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
// worker/src/lib/rollup.ts
|
|
3575
|
+
var DIMENSION_COLUMNS = {
|
|
3576
|
+
pages: "blob2",
|
|
3577
|
+
referrers: "blob3",
|
|
3578
|
+
geo: "blob4",
|
|
3579
|
+
browsers: "blob5",
|
|
3580
|
+
os: "blob6",
|
|
3581
|
+
device: "blob7"
|
|
3582
|
+
};
|
|
3583
|
+
function asNumber(value) {
|
|
3584
|
+
return Number(value) || 0;
|
|
3585
|
+
}
|
|
3586
|
+
function capRows(rows, cap = 100) {
|
|
3587
|
+
if (rows.length <= cap) return rows;
|
|
3588
|
+
const head = rows.slice(0, cap);
|
|
3589
|
+
const tail = rows.slice(cap);
|
|
3590
|
+
head.push({
|
|
3591
|
+
name: "other",
|
|
3592
|
+
views: tail.reduce((sum, row) => sum + row.views, 0),
|
|
3593
|
+
approx_visitors: tail.reduce((sum, row) => sum + row.approx_visitors, 0)
|
|
3594
|
+
});
|
|
3595
|
+
return head;
|
|
3596
|
+
}
|
|
3597
|
+
async function rollupDimension(env, site, day, dimension) {
|
|
3598
|
+
const { startAt, endAt } = utcDayBounds(day);
|
|
3599
|
+
const where = buildWhereClause({ site, startAt, endAt });
|
|
3600
|
+
const column = DIMENSION_COLUMNS[dimension];
|
|
3601
|
+
const sql = `
|
|
3602
|
+
SELECT
|
|
3603
|
+
${column} as name,
|
|
3604
|
+
SUM(_sample_interval) as views,
|
|
3605
|
+
COUNT(DISTINCT index1) as approx_visitors,
|
|
3606
|
+
MAX(_sample_interval) as max_interval
|
|
3607
|
+
FROM ${DATASET}
|
|
3608
|
+
${where}
|
|
3609
|
+
AND ${column} != ''
|
|
3610
|
+
GROUP BY ${column}
|
|
3611
|
+
ORDER BY views DESC
|
|
3612
|
+
LIMIT 1000
|
|
3613
|
+
`;
|
|
3614
|
+
const result = await queryAE(env, sql);
|
|
3615
|
+
const { rows, sampled } = extractSampled(result.data);
|
|
3616
|
+
return {
|
|
3617
|
+
rows: capRows(rows.map((row) => ({
|
|
3618
|
+
name: String(row.name || ""),
|
|
3619
|
+
views: asNumber(row.views),
|
|
3620
|
+
approx_visitors: asNumber(row.approx_visitors)
|
|
3621
|
+
}))),
|
|
3622
|
+
sampled
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
async function buildDailyRollup(env, site, day) {
|
|
3626
|
+
const { startAt, endAt } = utcDayBounds(day);
|
|
3627
|
+
const where = buildWhereClause({ site, startAt, endAt });
|
|
3628
|
+
const totalsSql = `
|
|
3629
|
+
SELECT
|
|
3630
|
+
SUM(_sample_interval) as views,
|
|
3631
|
+
COUNT(DISTINCT index1) as approx_daily_visitors,
|
|
3632
|
+
SUM(_sample_interval * double2) / (SUM(_sample_interval) + 0.0001) as avg_screen_width,
|
|
3633
|
+
MAX(_sample_interval) as max_interval
|
|
3634
|
+
FROM ${DATASET}
|
|
3635
|
+
${where}
|
|
3636
|
+
`;
|
|
3637
|
+
const totalsResult = await queryAE(env, totalsSql);
|
|
3638
|
+
const totalsSampled = extractSampled(totalsResult.data);
|
|
3639
|
+
const totalsRow = totalsSampled.rows[0] || {};
|
|
3640
|
+
const dimensions = await Promise.all(
|
|
3641
|
+
Object.keys(DIMENSION_COLUMNS).map(async (dimension) => [
|
|
3642
|
+
dimension,
|
|
3643
|
+
await rollupDimension(env, site, day, dimension)
|
|
3644
|
+
])
|
|
3645
|
+
);
|
|
3646
|
+
const rollup = {
|
|
3647
|
+
date: day,
|
|
3648
|
+
site,
|
|
3649
|
+
sampled: totalsSampled.sampled || dimensions.some(([, value]) => value.sampled),
|
|
3650
|
+
totals: {
|
|
3651
|
+
views: asNumber(totalsRow.views),
|
|
3652
|
+
approx_daily_visitors: asNumber(totalsRow.approx_daily_visitors),
|
|
3653
|
+
avg_screen_width: asNumber(totalsRow.avg_screen_width)
|
|
3654
|
+
},
|
|
3655
|
+
pages: [],
|
|
3656
|
+
referrers: [],
|
|
3657
|
+
geo: [],
|
|
3658
|
+
browsers: [],
|
|
3659
|
+
os: [],
|
|
3660
|
+
device: []
|
|
3661
|
+
};
|
|
3662
|
+
for (const [dimension, value] of dimensions) {
|
|
3663
|
+
rollup[dimension] = value.rows;
|
|
3664
|
+
}
|
|
3665
|
+
return rollup;
|
|
3666
|
+
}
|
|
3667
|
+
var ROLLUP_BACKFILL_WINDOW_DAYS = 5;
|
|
3668
|
+
var MAX_BUILDS_PER_RUN = 4;
|
|
3669
|
+
async function rollupYesterday(env, now = /* @__PURE__ */ new Date()) {
|
|
3670
|
+
if (!env.ARCHIVE || !env.CF_ACCOUNT_ID || !env.CF_API_TOKEN) return;
|
|
3671
|
+
const sites = parseAllowedSites(env.ALLOWED_SITES);
|
|
3672
|
+
const yesterday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 1));
|
|
3673
|
+
const oldest = new Date(yesterday.getTime() - (ROLLUP_BACKFILL_WINDOW_DAYS - 1) * 24 * 60 * 60 * 1e3);
|
|
3674
|
+
let builds = 0;
|
|
3675
|
+
for (const site of sites) {
|
|
3676
|
+
const days = dayKeys(oldest, yesterday).reverse();
|
|
3677
|
+
for (const day of days) {
|
|
3678
|
+
if (builds >= MAX_BUILDS_PER_RUN) return;
|
|
3679
|
+
const key = `rollups/${site}/${day}.json`;
|
|
3680
|
+
try {
|
|
3681
|
+
const existing = await env.ARCHIVE.head(key);
|
|
3682
|
+
if (existing) continue;
|
|
3683
|
+
const rollup = await buildDailyRollup(env, site, day);
|
|
3684
|
+
await env.ARCHIVE.put(key, JSON.stringify(rollup));
|
|
3685
|
+
builds += 1;
|
|
3686
|
+
} catch {
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
function isLiveRetentionDay(day, now = /* @__PURE__ */ new Date()) {
|
|
3692
|
+
const { startAt } = utcDayBounds(day);
|
|
3693
|
+
const cutoff = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 90));
|
|
3694
|
+
return startAt >= cutoff && day <= utcDayKey(now);
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
// worker/src/api/history.ts
|
|
3698
|
+
var LIVE_DIMENSIONS = {
|
|
3699
|
+
pages: { column: "blob2", alias: "page" },
|
|
3700
|
+
referrers: { column: "blob3", alias: "referrer" },
|
|
3701
|
+
geo: { column: "blob4", alias: "country" },
|
|
3702
|
+
browsers: { column: "blob5", alias: "browser" }
|
|
3703
|
+
};
|
|
3704
|
+
function asNumber2(value) {
|
|
3705
|
+
return Number(value) || 0;
|
|
3706
|
+
}
|
|
3707
|
+
async function readRollup(env, site, day) {
|
|
3708
|
+
if (!env.ARCHIVE) return null;
|
|
3709
|
+
const object = await env.ARCHIVE.get(`rollups/${site}/${day}.json`);
|
|
3710
|
+
if (!object) return null;
|
|
3711
|
+
return object.json();
|
|
3712
|
+
}
|
|
3713
|
+
async function readRollups(env, site, days) {
|
|
3714
|
+
const found = /* @__PURE__ */ new Map();
|
|
3715
|
+
const chunkSize = 40;
|
|
3716
|
+
for (let i = 0; i < days.length; i += chunkSize) {
|
|
3717
|
+
const chunk = days.slice(i, i + chunkSize);
|
|
3718
|
+
const results = await Promise.all(chunk.map(async (day) => [day, await readRollup(env, site, day)]));
|
|
3719
|
+
for (const [day, rollup] of results) {
|
|
3720
|
+
if (rollup) found.set(day, rollup);
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
return found;
|
|
3724
|
+
}
|
|
3725
|
+
async function fetchLiveRange(env, site, startAt, endAt, dimension) {
|
|
3726
|
+
const where = buildWhereClause({ site, startAt, endAt });
|
|
3727
|
+
if (dimension === "totals") {
|
|
3728
|
+
const sql2 = `
|
|
3729
|
+
SELECT
|
|
3730
|
+
SUM(_sample_interval) as views,
|
|
3731
|
+
COUNT(DISTINCT index1) as approx_daily_visitors,
|
|
3732
|
+
SUM(_sample_interval * double2) / (SUM(_sample_interval) + 0.0001) as avg_screen_width,
|
|
3733
|
+
MAX(_sample_interval) as max_interval
|
|
3734
|
+
FROM ${DATASET}
|
|
3735
|
+
${where}
|
|
3736
|
+
`;
|
|
3737
|
+
const result2 = await queryAE(env, sql2);
|
|
3738
|
+
const { rows: rows2, sampled: sampled2 } = extractSampled(result2.data);
|
|
3739
|
+
const row = rows2[0] || {};
|
|
3740
|
+
return {
|
|
3741
|
+
sampled: sampled2,
|
|
3742
|
+
totals: {
|
|
3743
|
+
views: asNumber2(row.views),
|
|
3744
|
+
approx_daily_visitors: asNumber2(row.approx_daily_visitors),
|
|
3745
|
+
avg_screen_width: asNumber2(row.avg_screen_width)
|
|
3746
|
+
},
|
|
3747
|
+
rows: []
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3750
|
+
const selected = LIVE_DIMENSIONS[dimension];
|
|
3751
|
+
const sql = `
|
|
3752
|
+
SELECT
|
|
3753
|
+
${selected.column} as name,
|
|
3754
|
+
SUM(_sample_interval) as views,
|
|
3755
|
+
COUNT(DISTINCT index1) as approx_visitors,
|
|
3756
|
+
MAX(_sample_interval) as max_interval
|
|
3757
|
+
FROM ${DATASET}
|
|
3758
|
+
${where}
|
|
3759
|
+
AND ${selected.column} != ''
|
|
3760
|
+
GROUP BY ${selected.column}
|
|
3761
|
+
ORDER BY views DESC
|
|
3762
|
+
LIMIT 1000
|
|
3763
|
+
`;
|
|
3764
|
+
const result = await queryAE(env, sql);
|
|
3765
|
+
const { rows, sampled } = extractSampled(result.data);
|
|
3766
|
+
return {
|
|
3767
|
+
sampled,
|
|
3768
|
+
totals: null,
|
|
3769
|
+
rows: rows.map((row) => ({
|
|
3770
|
+
name: String(row.name || ""),
|
|
3771
|
+
views: asNumber2(row.views),
|
|
3772
|
+
approx_visitors: asNumber2(row.approx_visitors)
|
|
3773
|
+
}))
|
|
3774
|
+
};
|
|
3775
|
+
}
|
|
3776
|
+
function mergeRows(target, rows) {
|
|
3777
|
+
for (const row of rows) {
|
|
3778
|
+
const key = row.name || "unknown";
|
|
3779
|
+
const existing = target.get(key) || { name: key, views: 0, approx_visitors: 0 };
|
|
3780
|
+
existing.views += row.views;
|
|
3781
|
+
existing.approx_visitors += row.approx_visitors;
|
|
3782
|
+
target.set(key, existing);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
async function historyHandler(c) {
|
|
3786
|
+
try {
|
|
3787
|
+
const site = c.req.query("site");
|
|
3788
|
+
if (!site) throw new Error("Missing required parameter: site");
|
|
3789
|
+
if (!validateSite(site, c.env.ALLOWED_SITES)) throw new Error(`Unknown site: ${site}`);
|
|
3790
|
+
const dimension = c.req.query("dimension") || "totals";
|
|
3791
|
+
if (!["totals", "pages", "referrers", "geo", "browsers"].includes(dimension)) {
|
|
3792
|
+
throw new Error("Invalid dimension. Supported: totals, pages, referrers, geo, browsers");
|
|
3793
|
+
}
|
|
3794
|
+
const { startAt, endAt, period } = parseLongPeriod({
|
|
3795
|
+
days: c.req.query("days"),
|
|
3796
|
+
from: c.req.query("from"),
|
|
3797
|
+
to: c.req.query("to")
|
|
3798
|
+
});
|
|
3799
|
+
const days = dayKeys(startAt, endAt);
|
|
3800
|
+
const liveDays = days.filter((day) => isLiveRetentionDay(day));
|
|
3801
|
+
const archiveDayList = days.filter((day) => !isLiveRetentionDay(day));
|
|
3802
|
+
const rollups = await readRollups(c.env, site, archiveDayList);
|
|
3803
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3804
|
+
const totals = { views: 0, approx_daily_visitors: 0, avg_screen_width: 0 };
|
|
3805
|
+
let screenWeight = 0;
|
|
3806
|
+
let sampled = false;
|
|
3807
|
+
let archivedDaysMissing = 0;
|
|
3808
|
+
let archiveDays = 0;
|
|
3809
|
+
for (const day of archiveDayList) {
|
|
3810
|
+
const rollup = rollups.get(day) || null;
|
|
3811
|
+
if (!rollup) {
|
|
3812
|
+
archivedDaysMissing += 1;
|
|
3813
|
+
continue;
|
|
3814
|
+
}
|
|
3815
|
+
archiveDays += 1;
|
|
3816
|
+
sampled ||= !!rollup.sampled;
|
|
3817
|
+
if (dimension === "totals") {
|
|
3818
|
+
totals.views += rollup.totals.views;
|
|
3819
|
+
totals.approx_daily_visitors += rollup.totals.approx_daily_visitors;
|
|
3820
|
+
screenWeight += rollup.totals.avg_screen_width * rollup.totals.views;
|
|
3821
|
+
} else {
|
|
3822
|
+
mergeRows(rows, rollup[dimension]);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
if (liveDays.length > 0) {
|
|
3826
|
+
const liveStart = utcDayBounds(liveDays[0]).startAt;
|
|
3827
|
+
const liveEnd = utcDayBounds(liveDays[liveDays.length - 1]).endAt;
|
|
3828
|
+
const live = await fetchLiveRange(c.env, site, liveStart, liveEnd, dimension);
|
|
3829
|
+
sampled ||= live.sampled;
|
|
3830
|
+
if (dimension === "totals" && live.totals) {
|
|
3831
|
+
totals.views += live.totals.views;
|
|
3832
|
+
totals.approx_daily_visitors += live.totals.approx_daily_visitors;
|
|
3833
|
+
screenWeight += live.totals.avg_screen_width * live.totals.views;
|
|
3834
|
+
} else {
|
|
3835
|
+
mergeRows(rows, live.rows);
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
const source = liveDays.length > 0 && archiveDays > 0 ? "blended" : archiveDays > 0 ? "archive" : "live";
|
|
3839
|
+
const data = dimension === "totals" ? {
|
|
3840
|
+
views: totals.views,
|
|
3841
|
+
approx_daily_visitors: totals.approx_daily_visitors,
|
|
3842
|
+
avg_screen_width: totals.views > 0 ? screenWeight / totals.views : 0
|
|
3843
|
+
} : Array.from(rows.values()).sort((a, b) => b.views - a.views).slice(0, 100);
|
|
3844
|
+
return c.json(envelope(data, site, period, sampled, {
|
|
3845
|
+
source,
|
|
3846
|
+
archived_days_missing: archivedDaysMissing
|
|
3847
|
+
}));
|
|
3848
|
+
} catch (e) {
|
|
3849
|
+
const msg = e instanceof Error && !e.message.includes("Analytics Engine") ? e.message : "Internal query error";
|
|
3850
|
+
return c.json({ error: msg }, 400);
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
// worker/src/scheduled.ts
|
|
3855
|
+
async function scheduled(_event, env, ctx) {
|
|
3856
|
+
ctx.waitUntil(rollupYesterday(env));
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3199
3859
|
// worker/src/version.ts
|
|
3200
|
-
var VERSION = true ? "0.
|
|
3860
|
+
var VERSION = true ? "0.3.0" : "0.3.0";
|
|
3201
3861
|
|
|
3202
3862
|
// worker/src/index.ts
|
|
3203
3863
|
var app = new Hono2();
|
|
@@ -3214,8 +3874,20 @@ app.get("/api/geo", geoHandler);
|
|
|
3214
3874
|
app.get("/api/browsers", browsersHandler);
|
|
3215
3875
|
app.get("/api/timeseries", timeseriesHandler);
|
|
3216
3876
|
app.get("/api/sites", sitesHandler);
|
|
3877
|
+
app.get("/api/active", activeHandler);
|
|
3878
|
+
app.get("/api/crawlers", crawlersHandler);
|
|
3879
|
+
app.get("/api/channels", channelsHandler);
|
|
3880
|
+
app.get("/api/bounce", bounceHandler);
|
|
3881
|
+
app.get("/api/duration", durationHandler);
|
|
3882
|
+
app.get("/api/history", historyHandler);
|
|
3217
3883
|
app.get("/health", (c) => c.json({ status: "ok", version: VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
3218
|
-
var
|
|
3884
|
+
var worker = {
|
|
3885
|
+
fetch: app.fetch,
|
|
3886
|
+
scheduled,
|
|
3887
|
+
request: app.request.bind(app)
|
|
3888
|
+
};
|
|
3889
|
+
var index_default = worker;
|
|
3219
3890
|
export {
|
|
3891
|
+
app,
|
|
3220
3892
|
index_default as default
|
|
3221
3893
|
};
|