@edge-base/server 0.1.1
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/admin-build/.gitkeep +0 -0
- package/admin-build/_app/env.js +1 -0
- package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
- package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
- package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
- package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
- package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
- package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
- package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
- package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
- package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
- package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
- package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
- package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
- package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
- package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
- package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
- package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
- package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
- package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
- package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
- package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
- package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
- package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
- package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
- package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
- package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
- package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
- package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
- package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
- package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
- package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
- package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
- package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
- package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
- package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
- package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
- package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
- package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
- package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
- package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
- package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
- package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
- package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
- package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
- package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
- package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
- package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
- package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
- package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
- package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
- package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
- package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
- package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
- package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
- package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
- package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
- package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
- package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
- package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
- package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
- package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
- package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
- package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
- package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
- package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
- package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
- package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
- package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
- package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
- package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
- package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
- package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
- package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
- package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
- package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
- package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
- package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
- package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
- package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
- package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
- package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
- package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
- package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
- package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
- package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
- package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
- package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
- package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
- package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
- package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
- package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
- package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
- package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
- package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
- package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
- package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
- package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
- package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
- package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
- package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
- package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
- package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
- package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
- package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
- package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
- package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
- package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
- package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
- package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
- package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
- package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
- package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
- package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
- package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
- package/admin-build/_app/version.json +1 -0
- package/admin-build/favicon.svg +26 -0
- package/admin-build/index.html +45 -0
- package/openapi.json +19543 -0
- package/package.json +66 -0
- package/src/__tests__/admin-assets.test.ts +55 -0
- package/src/__tests__/admin-data-routes.test.ts +488 -0
- package/src/__tests__/admin-db-target.test.ts +103 -0
- package/src/__tests__/admin-routing.test.ts +31 -0
- package/src/__tests__/admin-user-management.test.ts +311 -0
- package/src/__tests__/analytics-query.test.ts +75 -0
- package/src/__tests__/auth-d1.test.ts +749 -0
- package/src/__tests__/auth-db-adapter.test.ts +73 -0
- package/src/__tests__/auth-jwt.test.ts +440 -0
- package/src/__tests__/auth-oauth.test.ts +389 -0
- package/src/__tests__/auth-password.test.ts +367 -0
- package/src/__tests__/auth-redirect.test.ts +87 -0
- package/src/__tests__/backup-restore.test.ts +711 -0
- package/src/__tests__/broadcast.test.ts +128 -0
- package/src/__tests__/cli.test.ts +178 -0
- package/src/__tests__/cloudflare-realtime.test.ts +113 -0
- package/src/__tests__/config.test.ts +469 -0
- package/src/__tests__/cors.test.ts +154 -0
- package/src/__tests__/cron.test.ts +302 -0
- package/src/__tests__/d1-handler.test.ts +402 -0
- package/src/__tests__/d1-sql.test.ts +120 -0
- package/src/__tests__/database-live-config.test.ts +42 -0
- package/src/__tests__/database-live-emitter.test.ts +56 -0
- package/src/__tests__/database-live-filters.test.ts +63 -0
- package/src/__tests__/database-live-route.test.ts +113 -0
- package/src/__tests__/db-sql.test.ts +163 -0
- package/src/__tests__/do-lifecycle.test.ts +263 -0
- package/src/__tests__/do-router.test.ts +729 -0
- package/src/__tests__/email-provider.test.ts +128 -0
- package/src/__tests__/email-templates.test.ts +528 -0
- package/src/__tests__/error-format.test.ts +250 -0
- package/src/__tests__/field-ops.test.ts +242 -0
- package/src/__tests__/functions-context.test.ts +334 -0
- package/src/__tests__/functions-d1-proxy.test.ts +229 -0
- package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
- package/src/__tests__/functions-route.test.ts +139 -0
- package/src/__tests__/internal-request.test.ts +77 -0
- package/src/__tests__/log-writer.test.ts +44 -0
- package/src/__tests__/logger.test.ts +58 -0
- package/src/__tests__/meta-admin-proxy.test.ts +48 -0
- package/src/__tests__/meta-export-coverage.test.ts +191 -0
- package/src/__tests__/meta-route-registration.test.ts +47 -0
- package/src/__tests__/namespace-dump.test.ts +28 -0
- package/src/__tests__/oauth-providers.test.ts +337 -0
- package/src/__tests__/openapi-coverage.test.ts +144 -0
- package/src/__tests__/pagination.test.ts +59 -0
- package/src/__tests__/password-policy.test.ts +191 -0
- package/src/__tests__/plugin-migrations.test.ts +379 -0
- package/src/__tests__/postgres-batch-compat.test.ts +133 -0
- package/src/__tests__/postgres-dialect.test.ts +328 -0
- package/src/__tests__/postgres-executor.test.ts +79 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
- package/src/__tests__/postgres-schema-init.test.ts +105 -0
- package/src/__tests__/postgres-table-utils.test.ts +107 -0
- package/src/__tests__/presence.test.ts +199 -0
- package/src/__tests__/provider.test.ts +550 -0
- package/src/__tests__/public-user-profile.test.ts +339 -0
- package/src/__tests__/push-handlers.test.ts +179 -0
- package/src/__tests__/push-provider.test.ts +80 -0
- package/src/__tests__/push-token.test.ts +418 -0
- package/src/__tests__/query.test.ts +771 -0
- package/src/__tests__/rate-limit.test.ts +260 -0
- package/src/__tests__/room-access-policy.test.ts +101 -0
- package/src/__tests__/room-handler-context.test.ts +130 -0
- package/src/__tests__/room-monitoring.test.ts +138 -0
- package/src/__tests__/room-runtime-routing.test.ts +222 -0
- package/src/__tests__/room.test.ts +254 -0
- package/src/__tests__/route-parser.test.ts +490 -0
- package/src/__tests__/rules.test.ts +234 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
- package/src/__tests__/scheduled.test.ts +80 -0
- package/src/__tests__/schema.test.ts +1273 -0
- package/src/__tests__/security-hardening.test.ts +312 -0
- package/src/__tests__/server.unit.test.ts +333 -0
- package/src/__tests__/service-key-db-proxy.test.ts +650 -0
- package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
- package/src/__tests__/service-key.test.ts +757 -0
- package/src/__tests__/smoke-skip-report.test.ts +72 -0
- package/src/__tests__/sms-provider.test.ts +39 -0
- package/src/__tests__/sql-route.test.ts +218 -0
- package/src/__tests__/storage-hook-context.test.ts +115 -0
- package/src/__tests__/totp.test.ts +200 -0
- package/src/__tests__/uuid.test.ts +144 -0
- package/src/__tests__/validation.test.ts +773 -0
- package/src/__tests__/websocket-pending.test.ts +163 -0
- package/src/_functions-registry.ts +51 -0
- package/src/bench-entry.ts +9 -0
- package/src/cloudflare-test.d.ts +1 -0
- package/src/durable-objects/auth-do.ts +49 -0
- package/src/durable-objects/database-do.ts +2240 -0
- package/src/durable-objects/database-live-do.ts +949 -0
- package/src/durable-objects/logs-do.ts +1200 -0
- package/src/durable-objects/room-runtime-base.ts +1604 -0
- package/src/durable-objects/rooms-do.ts +2191 -0
- package/src/generated-config.ts +6 -0
- package/src/index.ts +382 -0
- package/src/lib/admin-assets.ts +54 -0
- package/src/lib/admin-db-target.ts +301 -0
- package/src/lib/admin-routing.ts +35 -0
- package/src/lib/admin-user-management.ts +464 -0
- package/src/lib/analytics-adapter.ts +103 -0
- package/src/lib/analytics-query.ts +579 -0
- package/src/lib/auth-d1-service.ts +1193 -0
- package/src/lib/auth-d1.ts +1056 -0
- package/src/lib/auth-db-adapter.ts +289 -0
- package/src/lib/auth-redirect.ts +116 -0
- package/src/lib/cidr.ts +115 -0
- package/src/lib/client-ip.ts +51 -0
- package/src/lib/cloudflare-realtime.ts +251 -0
- package/src/lib/control-db.ts +36 -0
- package/src/lib/cron.ts +163 -0
- package/src/lib/d1-handler.ts +1425 -0
- package/src/lib/d1-schema-init.ts +255 -0
- package/src/lib/d1-sql.ts +33 -0
- package/src/lib/database-live-config.ts +24 -0
- package/src/lib/database-live-emitter.ts +111 -0
- package/src/lib/db-sql.ts +66 -0
- package/src/lib/do-retry.ts +36 -0
- package/src/lib/do-router.ts +270 -0
- package/src/lib/do-sql.ts +73 -0
- package/src/lib/email-provider.ts +379 -0
- package/src/lib/email-templates.ts +285 -0
- package/src/lib/email-translations.ts +422 -0
- package/src/lib/errors.ts +151 -0
- package/src/lib/functions.ts +2091 -0
- package/src/lib/hono.ts +56 -0
- package/src/lib/internal-request.ts +56 -0
- package/src/lib/jwt.ts +354 -0
- package/src/lib/log-writer.ts +272 -0
- package/src/lib/namespace-dump.ts +125 -0
- package/src/lib/oauth-providers.ts +1225 -0
- package/src/lib/op-parser.ts +99 -0
- package/src/lib/openapi.ts +146 -0
- package/src/lib/pagination.ts +19 -0
- package/src/lib/password-policy.ts +102 -0
- package/src/lib/password.ts +145 -0
- package/src/lib/plugin-migrations.ts +612 -0
- package/src/lib/postgres-executor.ts +203 -0
- package/src/lib/postgres-handler.ts +1102 -0
- package/src/lib/postgres-schema-init.ts +341 -0
- package/src/lib/postgres-table-utils.ts +87 -0
- package/src/lib/public-user-profile.ts +187 -0
- package/src/lib/push-provider.ts +409 -0
- package/src/lib/push-token.ts +294 -0
- package/src/lib/query-engine.ts +768 -0
- package/src/lib/room-monitoring.ts +97 -0
- package/src/lib/room-runtime.ts +14 -0
- package/src/lib/route-parser.ts +434 -0
- package/src/lib/schema.ts +538 -0
- package/src/lib/schemas.ts +152 -0
- package/src/lib/service-key.ts +419 -0
- package/src/lib/sms-provider.ts +230 -0
- package/src/lib/startup-config.ts +99 -0
- package/src/lib/totp.ts +242 -0
- package/src/lib/uuid.ts +87 -0
- package/src/lib/validation.ts +205 -0
- package/src/lib/version.ts +2 -0
- package/src/lib/websocket-pending.ts +40 -0
- package/src/middleware/auth.ts +169 -0
- package/src/middleware/captcha-verify.ts +217 -0
- package/src/middleware/cors.ts +159 -0
- package/src/middleware/error-handler.ts +54 -0
- package/src/middleware/internal-guard.ts +26 -0
- package/src/middleware/logger.ts +126 -0
- package/src/middleware/rate-limit.ts +283 -0
- package/src/middleware/rules.ts +475 -0
- package/src/routes/admin-auth.ts +447 -0
- package/src/routes/admin.ts +3501 -0
- package/src/routes/analytics-api.ts +290 -0
- package/src/routes/auth.ts +4222 -0
- package/src/routes/backup.ts +1466 -0
- package/src/routes/config.ts +53 -0
- package/src/routes/d1.ts +109 -0
- package/src/routes/database-live.ts +281 -0
- package/src/routes/functions.ts +155 -0
- package/src/routes/health.ts +32 -0
- package/src/routes/kv.ts +167 -0
- package/src/routes/oauth.ts +1055 -0
- package/src/routes/push.ts +1465 -0
- package/src/routes/room.ts +639 -0
- package/src/routes/schema-endpoint.ts +76 -0
- package/src/routes/sql.ts +176 -0
- package/src/routes/storage.ts +1674 -0
- package/src/routes/tables.ts +699 -0
- package/src/routes/users.ts +21 -0
- package/src/routes/vectorize.ts +372 -0
- package/src/types.ts +99 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics query builder.
|
|
3
|
+
*
|
|
4
|
+
* Generates SQL for Cloudflare Analytics Engine (cloud) and normalizes
|
|
5
|
+
* responses into a common format consumed by the admin dashboard.
|
|
6
|
+
*
|
|
7
|
+
* Data Point Layout (Analytics Engine):
|
|
8
|
+
* index1: userId ('anonymous' if not authenticated)
|
|
9
|
+
* blob1: method blob6: subcategory
|
|
10
|
+
* blob2: path blob7: target1
|
|
11
|
+
* blob3: status (str) blob8: target2
|
|
12
|
+
* blob4: error blob9: operation
|
|
13
|
+
* blob5: category blob10: region
|
|
14
|
+
* double1: status double4: requestSize
|
|
15
|
+
* double2: duration double5: responseSize
|
|
16
|
+
* double3: timestamp double6: resultCount
|
|
17
|
+
*
|
|
18
|
+
* LogsDO SQLite queries are handled directly in logs-do.ts.
|
|
19
|
+
* This module focuses on Analytics Engine SQL generation + response transform.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ─── Types ───
|
|
23
|
+
|
|
24
|
+
export interface QueryParams {
|
|
25
|
+
range: string; // '1h'|'6h'|'24h'|'7d'|'30d'|'90d'
|
|
26
|
+
category?: string; // filter by category
|
|
27
|
+
metric: string; // 'overview'|'timeSeries'|'breakdown'|'topEndpoints'
|
|
28
|
+
groupBy?: string; // 'minute'|'tenMinute'|'hour'|'day'
|
|
29
|
+
excludeCategory?: string; // exclude a category (e.g. 'admin' to hide dashboard traffic)
|
|
30
|
+
start?: string; // ISO timestamp for custom start range
|
|
31
|
+
end?: string; // ISO timestamp for custom end range
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type AnalyticsGroupBy = 'minute' | 'tenMinute' | 'hour' | 'day';
|
|
35
|
+
export type OverviewAutoRange = '1h' | '6h' | '24h';
|
|
36
|
+
|
|
37
|
+
export interface AnalyticsSummary {
|
|
38
|
+
totalRequests: number;
|
|
39
|
+
totalErrors: number;
|
|
40
|
+
avgLatency: number;
|
|
41
|
+
uniqueUsers: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TimeSeriesPoint {
|
|
45
|
+
timestamp: number;
|
|
46
|
+
requests: number;
|
|
47
|
+
errors: number;
|
|
48
|
+
avgLatency: number;
|
|
49
|
+
uniqueUsers: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BreakdownItem {
|
|
53
|
+
label: string;
|
|
54
|
+
count: number;
|
|
55
|
+
percentage: number;
|
|
56
|
+
avgLatency?: number;
|
|
57
|
+
errorRate?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TopItem {
|
|
61
|
+
label: string;
|
|
62
|
+
count: number;
|
|
63
|
+
avgLatency: number;
|
|
64
|
+
errorRate: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface AnalyticsResponse {
|
|
68
|
+
timeSeries: TimeSeriesPoint[];
|
|
69
|
+
summary: AnalyticsSummary;
|
|
70
|
+
breakdown: BreakdownItem[];
|
|
71
|
+
topItems: TopItem[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Time Range ───
|
|
75
|
+
|
|
76
|
+
export function parseTimeRange(range: string, start?: string, end?: string): { startTs: number; endTs: number } {
|
|
77
|
+
if (start && end) {
|
|
78
|
+
const startTs = new Date(start).getTime();
|
|
79
|
+
const endTs = new Date(end).getTime();
|
|
80
|
+
if (Number.isFinite(startTs) && Number.isFinite(endTs) && endTs >= startTs) {
|
|
81
|
+
return { startTs, endTs };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
let startTs: number;
|
|
87
|
+
|
|
88
|
+
switch (range) {
|
|
89
|
+
case '1h': startTs = now - 3600_000; break;
|
|
90
|
+
case '6h': startTs = now - 6 * 3600_000; break;
|
|
91
|
+
case '24h': startTs = now - 86400_000; break;
|
|
92
|
+
case '7d': startTs = now - 7 * 86400_000; break;
|
|
93
|
+
case '30d': startTs = now - 30 * 86400_000; break;
|
|
94
|
+
case '90d': startTs = now - 90 * 86400_000; break;
|
|
95
|
+
default: startTs = now - 86400_000; break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { startTs, endTs: now };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveAnalyticsGroupBy(
|
|
102
|
+
range: string,
|
|
103
|
+
start?: string,
|
|
104
|
+
end?: string,
|
|
105
|
+
requestedGroupBy?: string,
|
|
106
|
+
): AnalyticsGroupBy {
|
|
107
|
+
if (
|
|
108
|
+
requestedGroupBy === 'minute' ||
|
|
109
|
+
requestedGroupBy === 'tenMinute' ||
|
|
110
|
+
requestedGroupBy === 'hour' ||
|
|
111
|
+
requestedGroupBy === 'day'
|
|
112
|
+
) {
|
|
113
|
+
return requestedGroupBy;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (start && end) {
|
|
117
|
+
const startMs = new Date(start).getTime();
|
|
118
|
+
const endMs = new Date(end).getTime();
|
|
119
|
+
if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
|
|
120
|
+
const diffHours = (endMs - startMs) / 3_600_000;
|
|
121
|
+
if (diffHours <= 1) return 'minute';
|
|
122
|
+
if (diffHours <= 6) return 'tenMinute';
|
|
123
|
+
if (diffHours <= 48) return 'hour';
|
|
124
|
+
return 'day';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
switch (range) {
|
|
129
|
+
case '1h':
|
|
130
|
+
return 'minute';
|
|
131
|
+
case '6h':
|
|
132
|
+
return 'tenMinute';
|
|
133
|
+
case '7d':
|
|
134
|
+
case '30d':
|
|
135
|
+
case '90d':
|
|
136
|
+
return 'day';
|
|
137
|
+
case '24h':
|
|
138
|
+
default:
|
|
139
|
+
return 'hour';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function chooseOverviewAutoRange(oldestTimestamp: number | null, now = Date.now()): OverviewAutoRange {
|
|
144
|
+
if (oldestTimestamp == null || !Number.isFinite(oldestTimestamp)) return '1h';
|
|
145
|
+
|
|
146
|
+
const historyMs = Math.max(0, now - oldestTimestamp);
|
|
147
|
+
if (historyMs <= 3 * 3_600_000) return '1h';
|
|
148
|
+
if (historyMs <= 12 * 3_600_000) return '6h';
|
|
149
|
+
return '24h';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Get the Analytics Engine timestamp format for a given timestamp (seconds) */
|
|
153
|
+
function toAETimestamp(ts: number): string {
|
|
154
|
+
return new Date(ts).toISOString().replace('T', ' ').replace('Z', '');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Get group-by interval SQL expression for Analytics Engine */
|
|
158
|
+
function aeGroupByInterval(groupBy: string): string {
|
|
159
|
+
switch (groupBy) {
|
|
160
|
+
case 'minute': return "toStartOfInterval(timestamp, INTERVAL '1' MINUTE)";
|
|
161
|
+
case 'tenMinute': return "toStartOfInterval(timestamp, INTERVAL '10' MINUTE)";
|
|
162
|
+
case 'hour': return "toStartOfInterval(timestamp, INTERVAL '1' HOUR)";
|
|
163
|
+
case 'day': return "toStartOfInterval(timestamp, INTERVAL '1' DAY)";
|
|
164
|
+
default: return "toStartOfInterval(timestamp, INTERVAL '1' HOUR)";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Escaping ───
|
|
169
|
+
|
|
170
|
+
/** Basic SQL string escaping to prevent injection in interpolated values */
|
|
171
|
+
function escapeSql(str: string): string {
|
|
172
|
+
return str.replace(/'/g, "''");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Analytics Engine SQL Builders ───
|
|
176
|
+
|
|
177
|
+
const AE_DATASET = 'ANALYTICS';
|
|
178
|
+
const SERVER_ERROR_STATUS = 500;
|
|
179
|
+
|
|
180
|
+
/** Build Analytics Engine SQL for overview query (summary + timeSeries + breakdown + topItems) */
|
|
181
|
+
export function buildOverviewSQL(params: QueryParams): string[] {
|
|
182
|
+
const { startTs, endTs } = parseTimeRange(params.range, params.start, params.end);
|
|
183
|
+
const start = toAETimestamp(startTs);
|
|
184
|
+
const end = toAETimestamp(endTs);
|
|
185
|
+
const catFilter = params.category ? ` AND blob5 = '${escapeSql(params.category)}'` : '';
|
|
186
|
+
const excludeFilter = params.excludeCategory ? ` AND blob5 != '${escapeSql(params.excludeCategory)}'` : '';
|
|
187
|
+
const interval = aeGroupByInterval(params.groupBy || 'hour');
|
|
188
|
+
|
|
189
|
+
// 1. Summary
|
|
190
|
+
const summarySQL = `
|
|
191
|
+
SELECT
|
|
192
|
+
SUM(_sample_interval) as totalRequests,
|
|
193
|
+
SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) as totalErrors,
|
|
194
|
+
AVG(double2) as avgLatency,
|
|
195
|
+
COUNT(DISTINCT index1) as uniqueUsers
|
|
196
|
+
FROM ${AE_DATASET}
|
|
197
|
+
WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}${excludeFilter}
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
// 2. Time series
|
|
201
|
+
const tsSQL = `
|
|
202
|
+
SELECT
|
|
203
|
+
${interval} as ts,
|
|
204
|
+
SUM(_sample_interval) as requests,
|
|
205
|
+
SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) as errors,
|
|
206
|
+
AVG(double2) as avgLatency,
|
|
207
|
+
COUNT(DISTINCT index1) as uniqueUsers
|
|
208
|
+
FROM ${AE_DATASET}
|
|
209
|
+
WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}${excludeFilter}
|
|
210
|
+
GROUP BY ts
|
|
211
|
+
ORDER BY ts
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
// 3. Category breakdown
|
|
215
|
+
const breakdownSQL = `
|
|
216
|
+
SELECT
|
|
217
|
+
blob5 as label,
|
|
218
|
+
SUM(_sample_interval) as count
|
|
219
|
+
FROM ${AE_DATASET}
|
|
220
|
+
WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}${excludeFilter}
|
|
221
|
+
GROUP BY blob5
|
|
222
|
+
ORDER BY count DESC
|
|
223
|
+
LIMIT 20
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
// 4. Top endpoints
|
|
227
|
+
const topSQL = `
|
|
228
|
+
SELECT
|
|
229
|
+
blob2 as label,
|
|
230
|
+
SUM(_sample_interval) as count,
|
|
231
|
+
AVG(double2) as avgLatency,
|
|
232
|
+
SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) * 100.0 / SUM(_sample_interval) as errorRate
|
|
233
|
+
FROM ${AE_DATASET}
|
|
234
|
+
WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}${excludeFilter}
|
|
235
|
+
GROUP BY blob2
|
|
236
|
+
ORDER BY count DESC
|
|
237
|
+
LIMIT 10
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
return [summarySQL, tsSQL, breakdownSQL, topSQL];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Build Analytics Engine SQL for time series only */
|
|
244
|
+
export function buildTimeSeriesSQL(params: QueryParams): string {
|
|
245
|
+
const { startTs, endTs } = parseTimeRange(params.range, params.start, params.end);
|
|
246
|
+
const start = toAETimestamp(startTs);
|
|
247
|
+
const end = toAETimestamp(endTs);
|
|
248
|
+
const catFilter = params.category ? ` AND blob5 = '${escapeSql(params.category)}'` : '';
|
|
249
|
+
const interval = aeGroupByInterval(params.groupBy || 'hour');
|
|
250
|
+
|
|
251
|
+
return `
|
|
252
|
+
SELECT
|
|
253
|
+
${interval} as ts,
|
|
254
|
+
SUM(_sample_interval) as requests,
|
|
255
|
+
SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) as errors,
|
|
256
|
+
AVG(double2) as avgLatency,
|
|
257
|
+
COUNT(DISTINCT index1) as uniqueUsers
|
|
258
|
+
FROM ${AE_DATASET}
|
|
259
|
+
WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}
|
|
260
|
+
GROUP BY ts
|
|
261
|
+
ORDER BY ts
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Build Analytics Engine SQL for breakdown */
|
|
266
|
+
export function buildBreakdownSQL(params: QueryParams): string {
|
|
267
|
+
const { startTs, endTs } = parseTimeRange(params.range, params.start, params.end);
|
|
268
|
+
const start = toAETimestamp(startTs);
|
|
269
|
+
const end = toAETimestamp(endTs);
|
|
270
|
+
const catFilter = params.category ? ` AND blob5 = '${escapeSql(params.category)}'` : '';
|
|
271
|
+
|
|
272
|
+
// If filtering by category, break down by subcategory; otherwise by category
|
|
273
|
+
const groupCol = params.category ? 'blob6' : 'blob5';
|
|
274
|
+
|
|
275
|
+
return `
|
|
276
|
+
SELECT
|
|
277
|
+
${groupCol} as label,
|
|
278
|
+
SUM(_sample_interval) as count,
|
|
279
|
+
AVG(double2) as avgLatency,
|
|
280
|
+
SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) * 100.0 / SUM(_sample_interval) as errorRate
|
|
281
|
+
FROM ${AE_DATASET}
|
|
282
|
+
WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}
|
|
283
|
+
GROUP BY ${groupCol}
|
|
284
|
+
ORDER BY count DESC
|
|
285
|
+
LIMIT 20
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Build Analytics Engine SQL for top endpoints */
|
|
290
|
+
export function buildTopEndpointsSQL(params: QueryParams): string {
|
|
291
|
+
const { startTs, endTs } = parseTimeRange(params.range, params.start, params.end);
|
|
292
|
+
const start = toAETimestamp(startTs);
|
|
293
|
+
const end = toAETimestamp(endTs);
|
|
294
|
+
const catFilter = params.category ? ` AND blob5 = '${escapeSql(params.category)}'` : '';
|
|
295
|
+
|
|
296
|
+
return `
|
|
297
|
+
SELECT
|
|
298
|
+
blob2 as label,
|
|
299
|
+
SUM(_sample_interval) as count,
|
|
300
|
+
AVG(double2) as avgLatency,
|
|
301
|
+
SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) * 100.0 / SUM(_sample_interval) as errorRate
|
|
302
|
+
FROM ${AE_DATASET}
|
|
303
|
+
WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}
|
|
304
|
+
GROUP BY blob2
|
|
305
|
+
ORDER BY count DESC
|
|
306
|
+
LIMIT 20
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Response Transformers ───
|
|
311
|
+
|
|
312
|
+
/** Transform Analytics Engine API response to standard format */
|
|
313
|
+
export function transformAEResponse(
|
|
314
|
+
summaryData: AEQueryResult,
|
|
315
|
+
timeSeriesData: AEQueryResult,
|
|
316
|
+
breakdownData: AEQueryResult,
|
|
317
|
+
topData: AEQueryResult,
|
|
318
|
+
): AnalyticsResponse {
|
|
319
|
+
// Summary
|
|
320
|
+
const summaryRow = summaryData.data?.[0] || {};
|
|
321
|
+
const summary: AnalyticsSummary = {
|
|
322
|
+
totalRequests: Number(summaryRow.totalRequests) || 0,
|
|
323
|
+
totalErrors: Number(summaryRow.totalErrors) || 0,
|
|
324
|
+
avgLatency: Number(summaryRow.avgLatency) || 0,
|
|
325
|
+
uniqueUsers: Number(summaryRow.uniqueUsers) || 0,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Time series
|
|
329
|
+
const timeSeries: TimeSeriesPoint[] = (timeSeriesData.data || []).map(row => ({
|
|
330
|
+
timestamp: new Date(row.ts as string).getTime(),
|
|
331
|
+
requests: Number(row.requests) || 0,
|
|
332
|
+
errors: Number(row.errors) || 0,
|
|
333
|
+
avgLatency: Number(row.avgLatency) || 0,
|
|
334
|
+
uniqueUsers: Number(row.uniqueUsers) || 0,
|
|
335
|
+
}));
|
|
336
|
+
|
|
337
|
+
// Breakdown
|
|
338
|
+
const rawBreakdown = breakdownData.data || [];
|
|
339
|
+
const totalBd = rawBreakdown.reduce((sum, r) => sum + (Number(r.count) || 0), 0);
|
|
340
|
+
const breakdown: BreakdownItem[] = rawBreakdown.map(row => ({
|
|
341
|
+
label: String(row.label || 'other'),
|
|
342
|
+
count: Number(row.count) || 0,
|
|
343
|
+
percentage: totalBd > 0 ? Math.round((Number(row.count) / totalBd) * 1000) / 10 : 0,
|
|
344
|
+
avgLatency: Number(row.avgLatency) || 0,
|
|
345
|
+
errorRate: Number(row.errorRate) || 0,
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
// Top items
|
|
349
|
+
const topItems: TopItem[] = (topData.data || []).map(row => ({
|
|
350
|
+
label: String(row.label || ''),
|
|
351
|
+
count: Number(row.count) || 0,
|
|
352
|
+
avgLatency: Number(row.avgLatency) || 0,
|
|
353
|
+
errorRate: Number(row.errorRate) || 0,
|
|
354
|
+
}));
|
|
355
|
+
|
|
356
|
+
return { timeSeries, summary, breakdown, topItems };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Empty analytics response (used when no backend is available) */
|
|
360
|
+
export function emptyResponse(): AnalyticsResponse {
|
|
361
|
+
return {
|
|
362
|
+
timeSeries: [],
|
|
363
|
+
summary: { totalRequests: 0, totalErrors: 0, avgLatency: 0, uniqueUsers: 0 },
|
|
364
|
+
breakdown: [],
|
|
365
|
+
topItems: [],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── Analytics Engine API Types ───
|
|
370
|
+
|
|
371
|
+
export interface AEQueryResult {
|
|
372
|
+
data: Record<string, unknown>[];
|
|
373
|
+
meta?: Record<string, unknown>[];
|
|
374
|
+
rows?: number;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Execute a SQL query against the Analytics Engine SQL API.
|
|
379
|
+
* Requires CF_ACCOUNT_ID and CF_API_TOKEN environment variables.
|
|
380
|
+
*/
|
|
381
|
+
export async function queryAnalyticsEngine(
|
|
382
|
+
sql: string,
|
|
383
|
+
accountId: string,
|
|
384
|
+
apiToken: string,
|
|
385
|
+
): Promise<AEQueryResult> {
|
|
386
|
+
const resp = await fetch(
|
|
387
|
+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/analytics_engine/sql`,
|
|
388
|
+
{
|
|
389
|
+
method: 'POST',
|
|
390
|
+
body: sql,
|
|
391
|
+
headers: {
|
|
392
|
+
Authorization: `Bearer ${apiToken}`,
|
|
393
|
+
'Content-Type': 'text/plain',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
if (!resp.ok) {
|
|
399
|
+
const text = await resp.text();
|
|
400
|
+
throw new Error(`Analytics Engine query failed (${resp.status}): ${text}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return (await resp.json()) as AEQueryResult;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function queryAnalyticsHistoryStart(
|
|
407
|
+
accountId: string,
|
|
408
|
+
apiToken: string,
|
|
409
|
+
excludeCategory?: string,
|
|
410
|
+
): Promise<number | null> {
|
|
411
|
+
const excludeFilter = excludeCategory ? ` WHERE blob5 != '${escapeSql(excludeCategory)}'` : '';
|
|
412
|
+
const result = await queryAnalyticsEngine(
|
|
413
|
+
`SELECT MIN(timestamp) as oldestTs FROM ${AE_DATASET}${excludeFilter}`,
|
|
414
|
+
accountId,
|
|
415
|
+
apiToken,
|
|
416
|
+
);
|
|
417
|
+
const raw = result.data?.[0]?.oldestTs;
|
|
418
|
+
if (!raw) return null;
|
|
419
|
+
const parsed = typeof raw === 'number' ? raw : new Date(String(raw)).getTime();
|
|
420
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function resolveOverviewAutoRange(
|
|
424
|
+
env: { ANALYTICS?: AnalyticsEngineDataset; CF_ACCOUNT_ID?: string; CF_API_TOKEN?: string; LOGS?: DurableObjectNamespace },
|
|
425
|
+
excludeCategory?: string,
|
|
426
|
+
): Promise<OverviewAutoRange> {
|
|
427
|
+
if (env.ANALYTICS && env.CF_ACCOUNT_ID && env.CF_API_TOKEN) {
|
|
428
|
+
try {
|
|
429
|
+
const oldestTimestamp = await queryAnalyticsHistoryStart(
|
|
430
|
+
env.CF_ACCOUNT_ID,
|
|
431
|
+
env.CF_API_TOKEN,
|
|
432
|
+
excludeCategory,
|
|
433
|
+
);
|
|
434
|
+
return chooseOverviewAutoRange(oldestTimestamp);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error('[Analytics] Failed to resolve history start from AE:', err);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (env.LOGS) {
|
|
441
|
+
try {
|
|
442
|
+
const logsDO = env.LOGS.get(env.LOGS.idFromName('logs:main'));
|
|
443
|
+
const params = new URLSearchParams();
|
|
444
|
+
if (excludeCategory) params.set('excludeCategory', excludeCategory);
|
|
445
|
+
const resp = await logsDO.fetch(
|
|
446
|
+
new Request(`http://internal/internal/logs/history?${params.toString()}`),
|
|
447
|
+
);
|
|
448
|
+
if (resp.ok) {
|
|
449
|
+
const body = (await resp.json()) as { oldestTimestamp?: number | null };
|
|
450
|
+
return chooseOverviewAutoRange(body.oldestTimestamp ?? null);
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.error('[Analytics] Failed to resolve history start from LogsDO:', err);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return '1h';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ─── Shared Query Executor ───
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Execute analytics query against the appropriate backend (AE or LogsDO).
|
|
464
|
+
* Used by both /admin/api/data/analytics and /api/analytics/query.
|
|
465
|
+
*/
|
|
466
|
+
export async function executeAnalyticsQuery(
|
|
467
|
+
env: { ANALYTICS?: AnalyticsEngineDataset; CF_ACCOUNT_ID?: string; CF_API_TOKEN?: string; LOGS?: DurableObjectNamespace },
|
|
468
|
+
params: QueryParams,
|
|
469
|
+
): Promise<AnalyticsResponse> {
|
|
470
|
+
// Cloud: Analytics Engine SQL API
|
|
471
|
+
if (env.ANALYTICS && env.CF_ACCOUNT_ID && env.CF_API_TOKEN) {
|
|
472
|
+
try {
|
|
473
|
+
if (params.metric === 'overview') {
|
|
474
|
+
const sqls = buildOverviewSQL(params);
|
|
475
|
+
const [summary, timeSeries, breakdown, top] = await Promise.all(
|
|
476
|
+
sqls.map(sql => queryAnalyticsEngine(sql, env.CF_ACCOUNT_ID!, env.CF_API_TOKEN!)),
|
|
477
|
+
);
|
|
478
|
+
return transformAEResponse(summary, timeSeries, breakdown, top);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (params.metric === 'timeSeries') {
|
|
482
|
+
const sql = buildTimeSeriesSQL(params);
|
|
483
|
+
const result = await queryAnalyticsEngine(sql, env.CF_ACCOUNT_ID!, env.CF_API_TOKEN!);
|
|
484
|
+
return {
|
|
485
|
+
...emptyResponse(),
|
|
486
|
+
timeSeries: (result.data || []).map(row => ({
|
|
487
|
+
timestamp: new Date(row.ts as string).getTime(),
|
|
488
|
+
requests: Number(row.requests) || 0,
|
|
489
|
+
errors: Number(row.errors) || 0,
|
|
490
|
+
avgLatency: Number(row.avgLatency) || 0,
|
|
491
|
+
uniqueUsers: Number(row.uniqueUsers) || 0,
|
|
492
|
+
})),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (params.metric === 'breakdown') {
|
|
497
|
+
const sql = buildBreakdownSQL(params);
|
|
498
|
+
const result = await queryAnalyticsEngine(sql, env.CF_ACCOUNT_ID!, env.CF_API_TOKEN!);
|
|
499
|
+
const rows = result.data || [];
|
|
500
|
+
const total = rows.reduce((sum, r) => sum + (Number(r.count) || 0), 0);
|
|
501
|
+
return {
|
|
502
|
+
...emptyResponse(),
|
|
503
|
+
breakdown: rows.map(r => ({
|
|
504
|
+
label: String(r.label || 'other'),
|
|
505
|
+
count: Number(r.count) || 0,
|
|
506
|
+
percentage: total > 0 ? Math.round((Number(r.count) / total) * 1000) / 10 : 0,
|
|
507
|
+
avgLatency: Number(r.avgLatency) || 0,
|
|
508
|
+
errorRate: Number(r.errorRate) || 0,
|
|
509
|
+
})),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (params.metric === 'topEndpoints') {
|
|
514
|
+
const sql = buildTopEndpointsSQL(params);
|
|
515
|
+
const result = await queryAnalyticsEngine(sql, env.CF_ACCOUNT_ID!, env.CF_API_TOKEN!);
|
|
516
|
+
return {
|
|
517
|
+
...emptyResponse(),
|
|
518
|
+
topItems: (result.data || []).map(r => ({
|
|
519
|
+
label: String(r.label || ''),
|
|
520
|
+
count: Number(r.count) || 0,
|
|
521
|
+
avgLatency: Number(r.avgLatency) || 0,
|
|
522
|
+
errorRate: Number(r.errorRate) || 0,
|
|
523
|
+
})),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
console.error('[Analytics] AE query failed:', err);
|
|
528
|
+
// Fall through to LogsDO
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Docker/Self-hosted: LogsDO SQLite query proxy
|
|
533
|
+
if (env.LOGS) {
|
|
534
|
+
try {
|
|
535
|
+
const logsDO = env.LOGS.get(env.LOGS.idFromName('logs:main'));
|
|
536
|
+
const queryParams = new URLSearchParams({
|
|
537
|
+
range: params.range,
|
|
538
|
+
category: params.category || '',
|
|
539
|
+
metric: params.metric,
|
|
540
|
+
groupBy: params.groupBy || 'hour',
|
|
541
|
+
excludeCategory: params.excludeCategory || '',
|
|
542
|
+
});
|
|
543
|
+
if (params.start) queryParams.set('start', params.start);
|
|
544
|
+
if (params.end) queryParams.set('end', params.end);
|
|
545
|
+
const resp = await logsDO.fetch(
|
|
546
|
+
new Request(`http://internal/internal/logs/query?${queryParams}`),
|
|
547
|
+
);
|
|
548
|
+
if (!resp.ok) {
|
|
549
|
+
console.error('[Analytics] LogsDO query returned', resp.status);
|
|
550
|
+
return emptyResponse();
|
|
551
|
+
}
|
|
552
|
+
return (await resp.json()) as AnalyticsResponse;
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.error('[Analytics] LogsDO query failed:', err);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// No analytics backend available
|
|
559
|
+
return emptyResponse();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ─── Type Helpers for executeAnalyticsQuery ───
|
|
563
|
+
|
|
564
|
+
interface AnalyticsEngineDataset {
|
|
565
|
+
writeDataPoint(event: { indexes?: string[]; blobs?: string[]; doubles?: number[] }): void;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
interface DurableObjectNamespace {
|
|
569
|
+
idFromName(name: string): DurableObjectId;
|
|
570
|
+
get(id: DurableObjectId): DurableObjectStub;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
interface DurableObjectId {
|
|
574
|
+
toString(): string;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
interface DurableObjectStub {
|
|
578
|
+
fetch(request: Request): Promise<Response>;
|
|
579
|
+
}
|