@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/dist/worker.js CHANGED
@@ -1,4 +1,4 @@
1
- // worker/node_modules/hono/dist/compose.js
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
- // worker/node_modules/hono/dist/request/constants.js
45
+ // node_modules/hono/dist/request/constants.js
46
46
  var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
47
47
 
48
- // worker/node_modules/hono/dist/utils/body.js
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
- // worker/node_modules/hono/dist/utils/url.js
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
- // worker/node_modules/hono/dist/request.js
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
- // worker/node_modules/hono/dist/utils/html.js
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
- // worker/node_modules/hono/dist/context.js
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
- // worker/node_modules/hono/dist/router.js
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
- // worker/node_modules/hono/dist/utils/constants.js
1064
+ // node_modules/hono/dist/utils/constants.js
1050
1065
  var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
1051
1066
 
1052
- // worker/node_modules/hono/dist/hono-base.js
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 = url.pathname.slice(pathPrefixLength) || "/";
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 = { basePath: this._basePath, path, method, handler };
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
- // worker/node_modules/hono/dist/router/reg-exp-router/matcher.js
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
- // worker/node_modules/hono/dist/router/reg-exp-router/node.js
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
- // worker/node_modules/hono/dist/router/reg-exp-router/trie.js
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
- // worker/node_modules/hono/dist/router/reg-exp-router/router.js
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
- // worker/node_modules/hono/dist/router/smart-router/router.js
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
- // worker/node_modules/hono/dist/router/trie-router/node.js
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
- // worker/node_modules/hono/dist/router/trie-router/router.js
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
- // worker/node_modules/hono/dist/hono.js
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
- // worker/node_modules/hono/dist/middleware/cors/index.js
2074
+ // node_modules/hono/dist/middleware/cors/index.js
2055
2075
  var cors = (options) => {
2056
- const defaults = {
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 !== "*" || opts.credentials) {
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 !== "*" || opts.credentials) {
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/bot.ts
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
- function isBot(ua) {
2228
- if (!ua || ua.length < 10) return true;
2229
- return BOT_PATTERNS.some((pattern) => pattern.test(ua));
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
- if (isBot(ua)) {
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:stripUrl(w.location.href),
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
- function buildWhereClause(opts) {
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
- return `WHERE blob1 = '${site}' AND timestamp >= toDateTime('${start}') AND timestamp <= toDateTime('${end}')`;
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.1.0" : "0.1.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 index_default = app;
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
  };