@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,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics API route — /api/analytics
|
|
3
|
+
*
|
|
4
|
+
* Endpoints:
|
|
5
|
+
* GET /api/analytics/query — Query request log metrics (Service Key required)
|
|
6
|
+
* POST /api/analytics/track — Track custom events (JWT / Service Key / anonymous)
|
|
7
|
+
* GET /api/analytics/events — Query custom events (Service Key required)
|
|
8
|
+
*
|
|
9
|
+
* This route is consumed by @edge-base/admin and @edge-base/web SDKs.
|
|
10
|
+
* The admin dashboard uses /admin/api/data/analytics instead (separate auth).
|
|
11
|
+
*/
|
|
12
|
+
import { OpenAPIHono, createRoute, z, type HonoEnv } from '../lib/hono.js';
|
|
13
|
+
import type { Env } from '../types.js';
|
|
14
|
+
import { parseConfig } from '../lib/do-router.js';
|
|
15
|
+
import { validateKey, buildConstraintCtx, resolveServiceKeyCandidate } from '../lib/service-key.js';
|
|
16
|
+
import { EdgeBaseError } from '@edge-base/shared';
|
|
17
|
+
import { zodDefaultHook, trackEventsBodySchema, jsonResponseSchema, errorResponseSchema } from '../lib/schemas.js';
|
|
18
|
+
import { executeAnalyticsQuery, resolveAnalyticsGroupBy } from '../lib/analytics-query.js';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export const analyticsApi = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
|
|
22
|
+
|
|
23
|
+
// ─── Helpers ───
|
|
24
|
+
|
|
25
|
+
/** Require valid Service Key for admin-level endpoints */
|
|
26
|
+
function requireServiceKey(c: { env: Env; req: { header: (name: string) => string | undefined; raw: Request } }): void {
|
|
27
|
+
const config = parseConfig(c.env);
|
|
28
|
+
const provided = resolveServiceKeyCandidate(c.req);
|
|
29
|
+
const constraintCtx = buildConstraintCtx(c.env as never, c.req);
|
|
30
|
+
const { result } = validateKey(provided, 'analytics:*:*:*', config, c.env as never, undefined, constraintCtx);
|
|
31
|
+
if (result === 'missing') {
|
|
32
|
+
throw new EdgeBaseError(403, 'Service Key required for analytics queries.');
|
|
33
|
+
}
|
|
34
|
+
if (result === 'invalid') {
|
|
35
|
+
throw new EdgeBaseError(401, 'Invalid Service Key.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Max custom events per single POST request */
|
|
40
|
+
const MAX_EVENTS_PER_REQUEST = 100;
|
|
41
|
+
/** Max properties per event (keys) */
|
|
42
|
+
const MAX_PROPERTIES = 50;
|
|
43
|
+
/** Max total properties JSON size in bytes */
|
|
44
|
+
const MAX_PROPERTIES_BYTES = 4096;
|
|
45
|
+
|
|
46
|
+
// ─── GET /query — Request log metrics (Service Key required) ───
|
|
47
|
+
|
|
48
|
+
const queryAnalytics = createRoute({
|
|
49
|
+
operationId: 'queryAnalytics',
|
|
50
|
+
method: 'get',
|
|
51
|
+
path: '/query',
|
|
52
|
+
tags: ['admin'],
|
|
53
|
+
summary: 'Query request log metrics',
|
|
54
|
+
request: {
|
|
55
|
+
query: z.object({
|
|
56
|
+
range: z.string().optional().openapi({ description: 'Time range (e.g. 24h, 7d, 30d)', example: '24h' }),
|
|
57
|
+
category: z.string().optional().openapi({ description: 'Filter by category' }),
|
|
58
|
+
metric: z.string().optional().openapi({ description: 'Metric type (overview, detailed)', example: 'overview' }),
|
|
59
|
+
groupBy: z.string().optional().openapi({ description: 'Group by interval (minute, tenMinute, hour, day)', example: 'hour' }),
|
|
60
|
+
start: z.string().optional().openapi({ description: 'Custom ISO start time' }),
|
|
61
|
+
end: z.string().optional().openapi({ description: 'Custom ISO end time' }),
|
|
62
|
+
excludeCategory: z.string().optional().openapi({ description: 'Exclude a category from the result set', example: 'admin' }),
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
responses: {
|
|
66
|
+
200: { description: 'Analytics data', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
67
|
+
401: { description: 'Unauthorized', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
68
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
analyticsApi.openapi(queryAnalytics, async (c) => {
|
|
73
|
+
requireServiceKey(c);
|
|
74
|
+
|
|
75
|
+
const range = c.req.query('range') || '24h';
|
|
76
|
+
const category = c.req.query('category') || '';
|
|
77
|
+
const metric = c.req.query('metric') || 'overview';
|
|
78
|
+
const start = c.req.query('start') || undefined;
|
|
79
|
+
const end = c.req.query('end') || undefined;
|
|
80
|
+
const excludeCategory = c.req.query('excludeCategory') || undefined;
|
|
81
|
+
const groupBy = resolveAnalyticsGroupBy(range, start, end, c.req.query('groupBy') || undefined);
|
|
82
|
+
|
|
83
|
+
const result = await executeAnalyticsQuery(c.env, { range, category, metric, groupBy, start, end, excludeCategory });
|
|
84
|
+
return c.json(result);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── POST /track — Custom event ingestion (JWT / Service Key / anonymous) ───
|
|
88
|
+
|
|
89
|
+
const trackEvents = createRoute({
|
|
90
|
+
operationId: 'trackEvents',
|
|
91
|
+
method: 'post',
|
|
92
|
+
path: '/track',
|
|
93
|
+
tags: ['client'],
|
|
94
|
+
summary: 'Track custom events',
|
|
95
|
+
request: {
|
|
96
|
+
body: { content: { 'application/json': { schema: trackEventsBodySchema } }, required: true },
|
|
97
|
+
},
|
|
98
|
+
responses: {
|
|
99
|
+
200: { description: 'Events tracked', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
100
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
analyticsApi.openapi(trackEvents, async (c) => {
|
|
105
|
+
// Parse body
|
|
106
|
+
let body: { events: Array<Record<string, unknown>> };
|
|
107
|
+
try {
|
|
108
|
+
body = await c.req.json();
|
|
109
|
+
} catch {
|
|
110
|
+
throw new EdgeBaseError(400, 'Invalid JSON body.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const events = body.events;
|
|
114
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
115
|
+
throw new EdgeBaseError(400, 'Request body must contain a non-empty "events" array.');
|
|
116
|
+
}
|
|
117
|
+
if (events.length > MAX_EVENTS_PER_REQUEST) {
|
|
118
|
+
throw new EdgeBaseError(400, `Maximum ${MAX_EVENTS_PER_REQUEST} events per request.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Determine userId from auth context
|
|
122
|
+
const auth = c.get('auth' as never) as { id: string } | null | undefined;
|
|
123
|
+
// Check if caller has Service Key (for userId override)
|
|
124
|
+
let isServiceKey = false;
|
|
125
|
+
try {
|
|
126
|
+
const config = parseConfig(c.env);
|
|
127
|
+
const provided = resolveServiceKeyCandidate(c.req);
|
|
128
|
+
if (provided !== undefined) {
|
|
129
|
+
const constraintCtx = buildConstraintCtx(c.env as never, c.req);
|
|
130
|
+
const { result } = validateKey(provided, 'analytics:*:*:*', config, c.env as never, undefined, constraintCtx);
|
|
131
|
+
isServiceKey = result === 'valid';
|
|
132
|
+
}
|
|
133
|
+
} catch { /* not a service key call */ }
|
|
134
|
+
|
|
135
|
+
// Extract region from cf object or CF-Ray header
|
|
136
|
+
let region = '';
|
|
137
|
+
try {
|
|
138
|
+
const cf = (c.req.raw as unknown as { cf?: { colo?: string } }).cf;
|
|
139
|
+
if (cf?.colo) region = cf.colo;
|
|
140
|
+
} catch { /* ignore */ }
|
|
141
|
+
if (!region) {
|
|
142
|
+
const cfRay = c.req.header('cf-ray');
|
|
143
|
+
if (cfRay) {
|
|
144
|
+
const parts = cfRay.split('-');
|
|
145
|
+
if (parts.length >= 2) region = parts[parts.length - 1];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Validate and normalize events
|
|
150
|
+
const validatedEvents: Array<{
|
|
151
|
+
timestamp: number;
|
|
152
|
+
userId: string | null;
|
|
153
|
+
eventName: string;
|
|
154
|
+
properties: Record<string, unknown> | null;
|
|
155
|
+
region: string;
|
|
156
|
+
}> = [];
|
|
157
|
+
|
|
158
|
+
for (const e of events) {
|
|
159
|
+
const name = e.name ?? e.eventName;
|
|
160
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
161
|
+
throw new EdgeBaseError(400, 'Each event must have a non-empty "name" string.');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Properties validation
|
|
165
|
+
let properties: Record<string, unknown> | null = null;
|
|
166
|
+
if (e.properties != null) {
|
|
167
|
+
if (typeof e.properties !== 'object' || Array.isArray(e.properties)) {
|
|
168
|
+
throw new EdgeBaseError(400, 'Event "properties" must be a plain object.');
|
|
169
|
+
}
|
|
170
|
+
const propKeys = Object.keys(e.properties as Record<string, unknown>);
|
|
171
|
+
if (propKeys.length > MAX_PROPERTIES) {
|
|
172
|
+
throw new EdgeBaseError(400, `Event properties limited to ${MAX_PROPERTIES} keys.`);
|
|
173
|
+
}
|
|
174
|
+
const jsonStr = JSON.stringify(e.properties);
|
|
175
|
+
if (new TextEncoder().encode(jsonStr).length > MAX_PROPERTIES_BYTES) {
|
|
176
|
+
throw new EdgeBaseError(400, `Event properties limited to ${MAX_PROPERTIES_BYTES} bytes.`);
|
|
177
|
+
}
|
|
178
|
+
properties = e.properties as Record<string, unknown>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// userId: JWT → auth.id, Service Key → body.userId, else null
|
|
182
|
+
let userId: string | null = null;
|
|
183
|
+
if (auth?.id) {
|
|
184
|
+
userId = auth.id;
|
|
185
|
+
} else if (isServiceKey && typeof e.userId === 'string') {
|
|
186
|
+
userId = e.userId;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
validatedEvents.push({
|
|
190
|
+
timestamp: typeof e.timestamp === 'number' ? e.timestamp : Date.now(),
|
|
191
|
+
userId,
|
|
192
|
+
eventName: name.trim(),
|
|
193
|
+
properties,
|
|
194
|
+
region,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Write to LogsDO (always, both Cloud and Docker)
|
|
199
|
+
if (!c.env.LOGS) {
|
|
200
|
+
// No LOGS binding — log and return success (best-effort)
|
|
201
|
+
console.warn('[Analytics] LOGS binding not available, custom events dropped.');
|
|
202
|
+
return c.json({ ok: true, count: 0 });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const logsDO = c.env.LOGS.get(c.env.LOGS.idFromName('logs:main'));
|
|
206
|
+
|
|
207
|
+
// Non-blocking write using waitUntil if available
|
|
208
|
+
const writePromise = logsDO.fetch(
|
|
209
|
+
new Request('http://internal/internal/events/write', {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'Content-Type': 'application/json' },
|
|
212
|
+
body: JSON.stringify({ events: validatedEvents }),
|
|
213
|
+
}),
|
|
214
|
+
).then(resp => {
|
|
215
|
+
if (!resp.ok) {
|
|
216
|
+
console.error('[Analytics] Events write failed:', resp.status);
|
|
217
|
+
}
|
|
218
|
+
}).catch(err => {
|
|
219
|
+
console.error('[Analytics] Events write error:', err);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const ctx = c.executionCtx;
|
|
224
|
+
ctx.waitUntil(writePromise);
|
|
225
|
+
} catch {
|
|
226
|
+
// No execution context (unit tests) — wait inline
|
|
227
|
+
await writePromise;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return c.json({ ok: true, count: validatedEvents.length });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ─── GET /events — Query custom events (Service Key required) ───
|
|
234
|
+
|
|
235
|
+
const queryCustomEvents = createRoute({
|
|
236
|
+
operationId: 'queryCustomEvents',
|
|
237
|
+
method: 'get',
|
|
238
|
+
path: '/events',
|
|
239
|
+
tags: ['admin'],
|
|
240
|
+
summary: 'Query custom events',
|
|
241
|
+
request: {
|
|
242
|
+
query: z.object({
|
|
243
|
+
range: z.string().optional().openapi({ description: 'Time range (e.g. 24h, 7d, 30d)', example: '24h' }),
|
|
244
|
+
event: z.string().optional().openapi({ description: 'Filter by event name' }),
|
|
245
|
+
userId: z.string().optional().openapi({ description: 'Filter by user ID' }),
|
|
246
|
+
metric: z.string().optional().openapi({ description: 'Metric type (list, count, timeSeries, topEvents)', example: 'list' }),
|
|
247
|
+
groupBy: z.string().optional().openapi({ description: 'Group by interval (minute, tenMinute, hour, day)', example: 'hour' }),
|
|
248
|
+
limit: z.string().optional().openapi({ description: 'Max items to return', example: '50' }),
|
|
249
|
+
cursor: z.string().optional().openapi({ description: 'Pagination cursor' }),
|
|
250
|
+
}),
|
|
251
|
+
},
|
|
252
|
+
responses: {
|
|
253
|
+
200: { description: 'Custom events', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
254
|
+
401: { description: 'Unauthorized', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
255
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
analyticsApi.openapi(queryCustomEvents, async (c) => {
|
|
260
|
+
requireServiceKey(c);
|
|
261
|
+
|
|
262
|
+
const range = c.req.query('range') || '24h';
|
|
263
|
+
const event = c.req.query('event') || '';
|
|
264
|
+
const userId = c.req.query('userId') || '';
|
|
265
|
+
const metric = c.req.query('metric') || 'list';
|
|
266
|
+
const groupBy = c.req.query('groupBy') || 'hour';
|
|
267
|
+
const limit = c.req.query('limit') || '50';
|
|
268
|
+
const cursor = c.req.query('cursor') || '';
|
|
269
|
+
|
|
270
|
+
if (!c.env.LOGS) {
|
|
271
|
+
// No LOGS binding — return empty
|
|
272
|
+
if (metric === 'list') return c.json({ events: [], cursor: undefined, hasMore: false });
|
|
273
|
+
if (metric === 'count') return c.json({ totalEvents: 0, uniqueUsers: 0 });
|
|
274
|
+
if (metric === 'timeSeries') return c.json({ timeSeries: [] });
|
|
275
|
+
if (metric === 'topEvents') return c.json({ topEvents: [] });
|
|
276
|
+
return c.json({ events: [] });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const logsDO = c.env.LOGS.get(c.env.LOGS.idFromName('logs:main'));
|
|
280
|
+
const params = new URLSearchParams({ range, metric, groupBy, limit });
|
|
281
|
+
if (event) params.set('event', event);
|
|
282
|
+
if (userId) params.set('userId', userId);
|
|
283
|
+
if (cursor) params.set('cursor', cursor);
|
|
284
|
+
|
|
285
|
+
const resp = await logsDO.fetch(
|
|
286
|
+
new Request(`http://internal/internal/events/query?${params}`),
|
|
287
|
+
);
|
|
288
|
+
const data = await resp.json();
|
|
289
|
+
return c.json(data);
|
|
290
|
+
});
|