enpilink 1.0.2
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/LICENSE +21 -0
- package/README.md +289 -0
- package/bin/run.js +5 -0
- package/dist/cli/build-helpers.d.ts +8 -0
- package/dist/cli/build-helpers.js +105 -0
- package/dist/cli/build-helpers.js.map +1 -0
- package/dist/cli/build-helpers.test.d.ts +1 -0
- package/dist/cli/build-helpers.test.js +100 -0
- package/dist/cli/build-helpers.test.js.map +1 -0
- package/dist/cli/detect-port.d.ts +18 -0
- package/dist/cli/detect-port.js +50 -0
- package/dist/cli/detect-port.js.map +1 -0
- package/dist/cli/ensure-ssh-key.d.ts +17 -0
- package/dist/cli/ensure-ssh-key.js +45 -0
- package/dist/cli/ensure-ssh-key.js.map +1 -0
- package/dist/cli/ensure-ssh-key.test.d.ts +1 -0
- package/dist/cli/ensure-ssh-key.test.js +68 -0
- package/dist/cli/ensure-ssh-key.test.js.map +1 -0
- package/dist/cli/header.d.ts +4 -0
- package/dist/cli/header.js +6 -0
- package/dist/cli/header.js.map +1 -0
- package/dist/cli/resolve-views-dir.d.ts +1 -0
- package/dist/cli/resolve-views-dir.js +17 -0
- package/dist/cli/resolve-views-dir.js.map +1 -0
- package/dist/cli/run-command.d.ts +2 -0
- package/dist/cli/run-command.js +43 -0
- package/dist/cli/run-command.js.map +1 -0
- package/dist/cli/telemetry.d.ts +14 -0
- package/dist/cli/telemetry.js +24 -0
- package/dist/cli/telemetry.js.map +1 -0
- package/dist/cli/tunnel-control-server.d.ts +11 -0
- package/dist/cli/tunnel-control-server.js +35 -0
- package/dist/cli/tunnel-control-server.js.map +1 -0
- package/dist/cli/tunnel-control-server.test.d.ts +1 -0
- package/dist/cli/tunnel-control-server.test.js +39 -0
- package/dist/cli/tunnel-control-server.test.js.map +1 -0
- package/dist/cli/tunnel-handler.d.ts +3 -0
- package/dist/cli/tunnel-handler.js +48 -0
- package/dist/cli/tunnel-handler.js.map +1 -0
- package/dist/cli/tunnel-handler.test.d.ts +1 -0
- package/dist/cli/tunnel-handler.test.js +107 -0
- package/dist/cli/tunnel-handler.test.js.map +1 -0
- package/dist/cli/tunnel-providers/index.d.ts +5 -0
- package/dist/cli/tunnel-providers/index.js +5 -0
- package/dist/cli/tunnel-providers/index.js.map +1 -0
- package/dist/cli/tunnel-providers/srv-us.d.ts +18 -0
- package/dist/cli/tunnel-providers/srv-us.js +66 -0
- package/dist/cli/tunnel-providers/srv-us.js.map +1 -0
- package/dist/cli/tunnel-providers/srv-us.test.d.ts +1 -0
- package/dist/cli/tunnel-providers/srv-us.test.js +74 -0
- package/dist/cli/tunnel-providers/srv-us.test.js.map +1 -0
- package/dist/cli/tunnel-providers/types.d.ts +49 -0
- package/dist/cli/tunnel-providers/types.js +2 -0
- package/dist/cli/tunnel-providers/types.js.map +1 -0
- package/dist/cli/tunnel.d.ts +75 -0
- package/dist/cli/tunnel.js +254 -0
- package/dist/cli/tunnel.js.map +1 -0
- package/dist/cli/tunnel.test.d.ts +1 -0
- package/dist/cli/tunnel.test.js +255 -0
- package/dist/cli/tunnel.test.js.map +1 -0
- package/dist/cli/types.d.ts +5 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/use-execute-steps.d.ts +11 -0
- package/dist/cli/use-execute-steps.js +36 -0
- package/dist/cli/use-execute-steps.js.map +1 -0
- package/dist/cli/use-messages.d.ts +3 -0
- package/dist/cli/use-messages.js +11 -0
- package/dist/cli/use-messages.js.map +1 -0
- package/dist/cli/use-nodemon.d.ts +2 -0
- package/dist/cli/use-nodemon.js +73 -0
- package/dist/cli/use-nodemon.js.map +1 -0
- package/dist/cli/use-open-browser.d.ts +1 -0
- package/dist/cli/use-open-browser.js +44 -0
- package/dist/cli/use-open-browser.js.map +1 -0
- package/dist/cli/use-open-tunnel-browser.d.ts +6 -0
- package/dist/cli/use-open-tunnel-browser.js +19 -0
- package/dist/cli/use-open-tunnel-browser.js.map +1 -0
- package/dist/cli/use-tunnel.d.ts +17 -0
- package/dist/cli/use-tunnel.js +131 -0
- package/dist/cli/use-tunnel.js.map +1 -0
- package/dist/cli/use-typescript-check.d.ts +9 -0
- package/dist/cli/use-typescript-check.js +94 -0
- package/dist/cli/use-typescript-check.js.map +1 -0
- package/dist/commands/build.d.ts +8 -0
- package/dist/commands/build.js +97 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/create.d.ts +9 -0
- package/dist/commands/create.js +30 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.js +112 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/start.d.ts +10 -0
- package/dist/commands/start.js +76 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/telemetry/disable.d.ts +5 -0
- package/dist/commands/telemetry/disable.js +12 -0
- package/dist/commands/telemetry/disable.js.map +1 -0
- package/dist/commands/telemetry/enable.d.ts +5 -0
- package/dist/commands/telemetry/enable.js +12 -0
- package/dist/commands/telemetry/enable.js.map +1 -0
- package/dist/commands/telemetry/status.d.ts +5 -0
- package/dist/commands/telemetry/status.js +12 -0
- package/dist/commands/telemetry/status.js.map +1 -0
- package/dist/server/admin.d.ts +79 -0
- package/dist/server/admin.js +239 -0
- package/dist/server/admin.js.map +1 -0
- package/dist/server/admin.test.d.ts +1 -0
- package/dist/server/admin.test.js +226 -0
- package/dist/server/admin.test.js.map +1 -0
- package/dist/server/analytics.d.ts +60 -0
- package/dist/server/analytics.js +168 -0
- package/dist/server/analytics.js.map +1 -0
- package/dist/server/analytics.test.d.ts +1 -0
- package/dist/server/analytics.test.js +179 -0
- package/dist/server/analytics.test.js.map +1 -0
- package/dist/server/asset-base-url-transform-plugin.d.ts +11 -0
- package/dist/server/asset-base-url-transform-plugin.js +48 -0
- package/dist/server/asset-base-url-transform-plugin.js.map +1 -0
- package/dist/server/asset-base-url-transform-plugin.test.d.ts +1 -0
- package/dist/server/asset-base-url-transform-plugin.test.js +134 -0
- package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -0
- package/dist/server/auth.d.ts +20 -0
- package/dist/server/auth.js +28 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/build-manifest.test.d.ts +1 -0
- package/dist/server/build-manifest.test.js +27 -0
- package/dist/server/build-manifest.test.js.map +1 -0
- package/dist/server/config/config.test.d.ts +1 -0
- package/dist/server/config/config.test.js +214 -0
- package/dist/server/config/config.test.js.map +1 -0
- package/dist/server/config/index.d.ts +3 -0
- package/dist/server/config/index.js +4 -0
- package/dist/server/config/index.js.map +1 -0
- package/dist/server/config/resolve.d.ts +73 -0
- package/dist/server/config/resolve.js +167 -0
- package/dist/server/config/resolve.js.map +1 -0
- package/dist/server/config/router.d.ts +23 -0
- package/dist/server/config/router.js +119 -0
- package/dist/server/config/router.js.map +1 -0
- package/dist/server/config/schema.d.ts +78 -0
- package/dist/server/config/schema.js +158 -0
- package/dist/server/config/schema.js.map +1 -0
- package/dist/server/content-helpers.d.ts +67 -0
- package/dist/server/content-helpers.js +79 -0
- package/dist/server/content-helpers.js.map +1 -0
- package/dist/server/content-helpers.test.d.ts +1 -0
- package/dist/server/content-helpers.test.js +70 -0
- package/dist/server/content-helpers.test.js.map +1 -0
- package/dist/server/express.d.ts +11 -0
- package/dist/server/express.js +129 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/express.test.d.ts +1 -0
- package/dist/server/express.test.js +464 -0
- package/dist/server/express.test.js.map +1 -0
- package/dist/server/file-ref.d.ts +28 -0
- package/dist/server/file-ref.js +27 -0
- package/dist/server/file-ref.js.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.js +14 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/inferUtilityTypes.d.ts +64 -0
- package/dist/server/inferUtilityTypes.js +2 -0
- package/dist/server/inferUtilityTypes.js.map +1 -0
- package/dist/server/log-sink.d.ts +16 -0
- package/dist/server/log-sink.js +66 -0
- package/dist/server/log-sink.js.map +1 -0
- package/dist/server/metric.d.ts +12 -0
- package/dist/server/metric.js +13 -0
- package/dist/server/metric.js.map +1 -0
- package/dist/server/middleware.d.ts +137 -0
- package/dist/server/middleware.js +93 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/middleware.test-d.d.ts +1 -0
- package/dist/server/middleware.test-d.js +75 -0
- package/dist/server/middleware.test-d.js.map +1 -0
- package/dist/server/middleware.test.d.ts +1 -0
- package/dist/server/middleware.test.js +493 -0
- package/dist/server/middleware.test.js.map +1 -0
- package/dist/server/mock-seed.d.ts +62 -0
- package/dist/server/mock-seed.js +251 -0
- package/dist/server/mock-seed.js.map +1 -0
- package/dist/server/mock-seed.test.d.ts +1 -0
- package/dist/server/mock-seed.test.js +122 -0
- package/dist/server/mock-seed.test.js.map +1 -0
- package/dist/server/observability.d.ts +149 -0
- package/dist/server/observability.js +340 -0
- package/dist/server/observability.js.map +1 -0
- package/dist/server/observability.test.d.ts +1 -0
- package/dist/server/observability.test.js +251 -0
- package/dist/server/observability.test.js.map +1 -0
- package/dist/server/otel.d.ts +45 -0
- package/dist/server/otel.js +117 -0
- package/dist/server/otel.js.map +1 -0
- package/dist/server/otel.test.d.ts +1 -0
- package/dist/server/otel.test.js +122 -0
- package/dist/server/otel.test.js.map +1 -0
- package/dist/server/server.d.ts +422 -0
- package/dist/server/server.js +684 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/storage/index.d.ts +23 -0
- package/dist/server/storage/index.js +46 -0
- package/dist/server/storage/index.js.map +1 -0
- package/dist/server/storage/memory.d.ts +30 -0
- package/dist/server/storage/memory.js +98 -0
- package/dist/server/storage/memory.js.map +1 -0
- package/dist/server/storage/memory.test.d.ts +1 -0
- package/dist/server/storage/memory.test.js +81 -0
- package/dist/server/storage/memory.test.js.map +1 -0
- package/dist/server/storage/postgres.d.ts +65 -0
- package/dist/server/storage/postgres.js +242 -0
- package/dist/server/storage/postgres.js.map +1 -0
- package/dist/server/storage/postgres.test.d.ts +1 -0
- package/dist/server/storage/postgres.test.js +182 -0
- package/dist/server/storage/postgres.test.js.map +1 -0
- package/dist/server/storage/sqlite.d.ts +33 -0
- package/dist/server/storage/sqlite.js +250 -0
- package/dist/server/storage/sqlite.js.map +1 -0
- package/dist/server/storage/sqlite.test.d.ts +1 -0
- package/dist/server/storage/sqlite.test.js +133 -0
- package/dist/server/storage/sqlite.test.js.map +1 -0
- package/dist/server/storage/types.d.ts +119 -0
- package/dist/server/storage/types.js +11 -0
- package/dist/server/storage/types.js.map +1 -0
- package/dist/server/templateHelper.d.ts +16 -0
- package/dist/server/templateHelper.js +11 -0
- package/dist/server/templateHelper.js.map +1 -0
- package/dist/server/templates.generated.d.ts +4 -0
- package/dist/server/templates.generated.js +47 -0
- package/dist/server/templates.generated.js.map +1 -0
- package/dist/server/tunnel-proxy-router.d.ts +7 -0
- package/dist/server/tunnel-proxy-router.js +110 -0
- package/dist/server/tunnel-proxy-router.js.map +1 -0
- package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
- package/dist/server/tunnel-proxy-router.test.js +229 -0
- package/dist/server/tunnel-proxy-router.test.js.map +1 -0
- package/dist/server/viewsDevServer.d.ts +14 -0
- package/dist/server/viewsDevServer.js +45 -0
- package/dist/server/viewsDevServer.js.map +1 -0
- package/dist/test/utils.d.ts +127 -0
- package/dist/test/utils.js +247 -0
- package/dist/test/utils.js.map +1 -0
- package/dist/test/view.test.d.ts +1 -0
- package/dist/test/view.test.js +568 -0
- package/dist/test/view.test.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/web/bridges/apps-sdk/adaptor.d.ts +54 -0
- package/dist/web/bridges/apps-sdk/adaptor.js +164 -0
- package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -0
- package/dist/web/bridges/apps-sdk/bridge.d.ts +11 -0
- package/dist/web/bridges/apps-sdk/bridge.js +47 -0
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -0
- package/dist/web/bridges/apps-sdk/index.d.ts +5 -0
- package/dist/web/bridges/apps-sdk/index.js +5 -0
- package/dist/web/bridges/apps-sdk/index.js.map +1 -0
- package/dist/web/bridges/apps-sdk/types.d.ts +147 -0
- package/dist/web/bridges/apps-sdk/types.js +10 -0
- package/dist/web/bridges/apps-sdk/types.js.map +1 -0
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +13 -0
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +18 -0
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -0
- package/dist/web/bridges/get-adaptor.d.ts +9 -0
- package/dist/web/bridges/get-adaptor.js +15 -0
- package/dist/web/bridges/get-adaptor.js.map +1 -0
- package/dist/web/bridges/index.d.ts +5 -0
- package/dist/web/bridges/index.js +6 -0
- package/dist/web/bridges/index.js.map +1 -0
- package/dist/web/bridges/mcp-app/adaptor.d.ts +81 -0
- package/dist/web/bridges/mcp-app/adaptor.js +346 -0
- package/dist/web/bridges/mcp-app/adaptor.js.map +1 -0
- package/dist/web/bridges/mcp-app/bridge.d.ts +28 -0
- package/dist/web/bridges/mcp-app/bridge.js +124 -0
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -0
- package/dist/web/bridges/mcp-app/index.d.ts +4 -0
- package/dist/web/bridges/mcp-app/index.js +4 -0
- package/dist/web/bridges/mcp-app/index.js.map +1 -0
- package/dist/web/bridges/mcp-app/types.d.ts +8 -0
- package/dist/web/bridges/mcp-app/types.js +2 -0
- package/dist/web/bridges/mcp-app/types.js.map +1 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +19 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js +19 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.d.ts +1 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +26 -0
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -0
- package/dist/web/bridges/mcp-app/view-tools.test.d.ts +1 -0
- package/dist/web/bridges/mcp-app/view-tools.test.js +144 -0
- package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -0
- package/dist/web/bridges/types.d.ts +243 -0
- package/dist/web/bridges/types.js +2 -0
- package/dist/web/bridges/types.js.map +1 -0
- package/dist/web/bridges/use-host-context.d.ts +7 -0
- package/dist/web/bridges/use-host-context.js +13 -0
- package/dist/web/bridges/use-host-context.js.map +1 -0
- package/dist/web/components/modal-provider.d.ts +4 -0
- package/dist/web/components/modal-provider.js +45 -0
- package/dist/web/components/modal-provider.js.map +1 -0
- package/dist/web/create-store.d.ts +29 -0
- package/dist/web/create-store.js +64 -0
- package/dist/web/create-store.js.map +1 -0
- package/dist/web/create-store.test.d.ts +1 -0
- package/dist/web/create-store.test.js +129 -0
- package/dist/web/create-store.test.js.map +1 -0
- package/dist/web/data-llm.d.ts +47 -0
- package/dist/web/data-llm.js +100 -0
- package/dist/web/data-llm.js.map +1 -0
- package/dist/web/data-llm.test.d.ts +1 -0
- package/dist/web/data-llm.test.js +142 -0
- package/dist/web/data-llm.test.js.map +1 -0
- package/dist/web/generate-helpers.d.ts +120 -0
- package/dist/web/generate-helpers.js +115 -0
- package/dist/web/generate-helpers.js.map +1 -0
- package/dist/web/generate-helpers.test-d.d.ts +1 -0
- package/dist/web/generate-helpers.test-d.js +211 -0
- package/dist/web/generate-helpers.test-d.js.map +1 -0
- package/dist/web/generate-helpers.test.d.ts +1 -0
- package/dist/web/generate-helpers.test.js +17 -0
- package/dist/web/generate-helpers.test.js.map +1 -0
- package/dist/web/helpers/state.d.ts +7 -0
- package/dist/web/helpers/state.js +45 -0
- package/dist/web/helpers/state.js.map +1 -0
- package/dist/web/helpers/state.test.d.ts +1 -0
- package/dist/web/helpers/state.test.js +53 -0
- package/dist/web/helpers/state.test.js.map +1 -0
- package/dist/web/hooks/index.d.ts +17 -0
- package/dist/web/hooks/index.js +18 -0
- package/dist/web/hooks/index.js.map +1 -0
- package/dist/web/hooks/test/utils.d.ts +20 -0
- package/dist/web/hooks/test/utils.js +75 -0
- package/dist/web/hooks/test/utils.js.map +1 -0
- package/dist/web/hooks/use-call-tool.d.ts +146 -0
- package/dist/web/hooks/use-call-tool.js +96 -0
- package/dist/web/hooks/use-call-tool.js.map +1 -0
- package/dist/web/hooks/use-call-tool.test-d.d.ts +1 -0
- package/dist/web/hooks/use-call-tool.test-d.js +104 -0
- package/dist/web/hooks/use-call-tool.test-d.js.map +1 -0
- package/dist/web/hooks/use-call-tool.test.d.ts +1 -0
- package/dist/web/hooks/use-call-tool.test.js +211 -0
- package/dist/web/hooks/use-call-tool.test.js.map +1 -0
- package/dist/web/hooks/use-display-mode.d.ts +24 -0
- package/dist/web/hooks/use-display-mode.js +29 -0
- package/dist/web/hooks/use-display-mode.js.map +1 -0
- package/dist/web/hooks/use-display-mode.test-d.d.ts +1 -0
- package/dist/web/hooks/use-display-mode.test-d.js +8 -0
- package/dist/web/hooks/use-display-mode.test-d.js.map +1 -0
- package/dist/web/hooks/use-display-mode.test.d.ts +1 -0
- package/dist/web/hooks/use-display-mode.test.js +41 -0
- package/dist/web/hooks/use-display-mode.test.js.map +1 -0
- package/dist/web/hooks/use-download.d.ts +5 -0
- package/dist/web/hooks/use-download.js +8 -0
- package/dist/web/hooks/use-download.js.map +1 -0
- package/dist/web/hooks/use-download.test.d.ts +1 -0
- package/dist/web/hooks/use-download.test.js +95 -0
- package/dist/web/hooks/use-download.test.js.map +1 -0
- package/dist/web/hooks/use-files.d.ts +39 -0
- package/dist/web/hooks/use-files.js +42 -0
- package/dist/web/hooks/use-files.js.map +1 -0
- package/dist/web/hooks/use-files.test.d.ts +1 -0
- package/dist/web/hooks/use-files.test.js +54 -0
- package/dist/web/hooks/use-files.test.js.map +1 -0
- package/dist/web/hooks/use-intent.d.ts +30 -0
- package/dist/web/hooks/use-intent.js +34 -0
- package/dist/web/hooks/use-intent.js.map +1 -0
- package/dist/web/hooks/use-intent.test.d.ts +1 -0
- package/dist/web/hooks/use-intent.test.js +85 -0
- package/dist/web/hooks/use-intent.test.js.map +1 -0
- package/dist/web/hooks/use-layout.d.ts +24 -0
- package/dist/web/hooks/use-layout.js +25 -0
- package/dist/web/hooks/use-layout.js.map +1 -0
- package/dist/web/hooks/use-layout.test.d.ts +1 -0
- package/dist/web/hooks/use-layout.test.js +96 -0
- package/dist/web/hooks/use-layout.test.js.map +1 -0
- package/dist/web/hooks/use-notify.d.ts +29 -0
- package/dist/web/hooks/use-notify.js +33 -0
- package/dist/web/hooks/use-notify.js.map +1 -0
- package/dist/web/hooks/use-notify.test.d.ts +1 -0
- package/dist/web/hooks/use-notify.test.js +105 -0
- package/dist/web/hooks/use-notify.test.js.map +1 -0
- package/dist/web/hooks/use-open-external.d.ts +20 -0
- package/dist/web/hooks/use-open-external.js +24 -0
- package/dist/web/hooks/use-open-external.js.map +1 -0
- package/dist/web/hooks/use-open-external.test.d.ts +1 -0
- package/dist/web/hooks/use-open-external.test.js +65 -0
- package/dist/web/hooks/use-open-external.test.js.map +1 -0
- package/dist/web/hooks/use-register-view-tool.d.ts +38 -0
- package/dist/web/hooks/use-register-view-tool.js +50 -0
- package/dist/web/hooks/use-register-view-tool.js.map +1 -0
- package/dist/web/hooks/use-request-close.d.ts +16 -0
- package/dist/web/hooks/use-request-close.js +21 -0
- package/dist/web/hooks/use-request-close.js.map +1 -0
- package/dist/web/hooks/use-request-close.test.d.ts +1 -0
- package/dist/web/hooks/use-request-close.test.js +52 -0
- package/dist/web/hooks/use-request-close.test.js.map +1 -0
- package/dist/web/hooks/use-request-modal.d.ts +24 -0
- package/dist/web/hooks/use-request-modal.js +31 -0
- package/dist/web/hooks/use-request-modal.js.map +1 -0
- package/dist/web/hooks/use-request-modal.test.d.ts +1 -0
- package/dist/web/hooks/use-request-modal.test.js +61 -0
- package/dist/web/hooks/use-request-modal.test.js.map +1 -0
- package/dist/web/hooks/use-request-size.d.ts +20 -0
- package/dist/web/hooks/use-request-size.js +24 -0
- package/dist/web/hooks/use-request-size.js.map +1 -0
- package/dist/web/hooks/use-request-size.test.d.ts +1 -0
- package/dist/web/hooks/use-request-size.test.js +65 -0
- package/dist/web/hooks/use-request-size.test.js.map +1 -0
- package/dist/web/hooks/use-send-follow-up-message.d.ts +19 -0
- package/dist/web/hooks/use-send-follow-up-message.js +25 -0
- package/dist/web/hooks/use-send-follow-up-message.js.map +1 -0
- package/dist/web/hooks/use-set-open-in-app-url.d.ts +18 -0
- package/dist/web/hooks/use-set-open-in-app-url.js +25 -0
- package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -0
- package/dist/web/hooks/use-set-open-in-app-url.test.d.ts +1 -0
- package/dist/web/hooks/use-set-open-in-app-url.test.js +43 -0
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -0
- package/dist/web/hooks/use-tool-info.d.ts +87 -0
- package/dist/web/hooks/use-tool-info.js +49 -0
- package/dist/web/hooks/use-tool-info.js.map +1 -0
- package/dist/web/hooks/use-tool-info.test-d.d.ts +1 -0
- package/dist/web/hooks/use-tool-info.test-d.js +91 -0
- package/dist/web/hooks/use-tool-info.test-d.js.map +1 -0
- package/dist/web/hooks/use-tool-info.test.d.ts +1 -0
- package/dist/web/hooks/use-tool-info.test.js +130 -0
- package/dist/web/hooks/use-tool-info.test.js.map +1 -0
- package/dist/web/hooks/use-user.d.ts +20 -0
- package/dist/web/hooks/use-user.js +37 -0
- package/dist/web/hooks/use-user.js.map +1 -0
- package/dist/web/hooks/use-user.test.d.ts +1 -0
- package/dist/web/hooks/use-user.test.js +122 -0
- package/dist/web/hooks/use-user.test.js.map +1 -0
- package/dist/web/hooks/use-view-state.d.ts +25 -0
- package/dist/web/hooks/use-view-state.js +32 -0
- package/dist/web/hooks/use-view-state.js.map +1 -0
- package/dist/web/hooks/use-view-state.test.d.ts +1 -0
- package/dist/web/hooks/use-view-state.test.js +177 -0
- package/dist/web/hooks/use-view-state.test.js.map +1 -0
- package/dist/web/index.d.ts +7 -0
- package/dist/web/index.js +8 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/mount-view.d.ts +20 -0
- package/dist/web/mount-view.js +46 -0
- package/dist/web/mount-view.js.map +1 -0
- package/dist/web/plugin/data-llm.test.d.ts +1 -0
- package/dist/web/plugin/data-llm.test.js +81 -0
- package/dist/web/plugin/data-llm.test.js.map +1 -0
- package/dist/web/plugin/plugin.d.ts +33 -0
- package/dist/web/plugin/plugin.js +189 -0
- package/dist/web/plugin/plugin.js.map +1 -0
- package/dist/web/plugin/scan-views.d.ts +16 -0
- package/dist/web/plugin/scan-views.js +88 -0
- package/dist/web/plugin/scan-views.js.map +1 -0
- package/dist/web/plugin/scan-views.test.d.ts +1 -0
- package/dist/web/plugin/scan-views.test.js +99 -0
- package/dist/web/plugin/scan-views.test.js.map +1 -0
- package/dist/web/plugin/transform-data-llm.d.ts +12 -0
- package/dist/web/plugin/transform-data-llm.js +96 -0
- package/dist/web/plugin/transform-data-llm.js.map +1 -0
- package/dist/web/plugin/transform-data-llm.test.d.ts +1 -0
- package/dist/web/plugin/transform-data-llm.test.js +81 -0
- package/dist/web/plugin/transform-data-llm.test.js.map +1 -0
- package/dist/web/plugin/validate-view.d.ts +1 -0
- package/dist/web/plugin/validate-view.js +9 -0
- package/dist/web/plugin/validate-view.js.map +1 -0
- package/dist/web/plugin/validate-view.test.d.ts +1 -0
- package/dist/web/plugin/validate-view.test.js +24 -0
- package/dist/web/plugin/validate-view.test.js.map +1 -0
- package/dist/web/proxy.d.ts +1 -0
- package/dist/web/proxy.js +52 -0
- package/dist/web/proxy.js.map +1 -0
- package/dist/web/types.d.ts +20 -0
- package/dist/web/types.js +2 -0
- package/dist/web/types.js.map +1 -0
- package/package.json +125 -0
- package/scripts/postinstall.mjs +45 -0
- package/tsconfig.base.json +36 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import express, {} from "express";
|
|
2
|
+
import { getActiveStorage } from "./log-sink.js";
|
|
3
|
+
/**
|
|
4
|
+
* Percentile of a numeric sample using nearest-rank (lower) interpolation.
|
|
5
|
+
* Returns `0` for an empty sample. `p` is in `[0, 1]`. Tolerates `ms === 0`
|
|
6
|
+
* (a zero-latency call is a real data point, not a missing one).
|
|
7
|
+
*/
|
|
8
|
+
export function percentile(sorted, p) {
|
|
9
|
+
if (sorted.length === 0) {
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
const rank = p * (sorted.length - 1);
|
|
13
|
+
const lo = Math.floor(rank);
|
|
14
|
+
const hi = Math.ceil(rank);
|
|
15
|
+
const loVal = sorted[lo] ?? 0;
|
|
16
|
+
if (lo === hi) {
|
|
17
|
+
return loVal;
|
|
18
|
+
}
|
|
19
|
+
const hiVal = sorted[hi] ?? loVal;
|
|
20
|
+
const frac = rank - lo;
|
|
21
|
+
return loVal + (hiVal - loVal) * frac;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fixed latency-histogram bucket edges (ms). Each pair `[edges[i], edges[i+1])`
|
|
25
|
+
* is a bucket; the final bucket is open-ended (`>= last edge`). Chosen to span
|
|
26
|
+
* sub-ms lookups through multi-second LLM/report calls.
|
|
27
|
+
*/
|
|
28
|
+
const HISTOGRAM_EDGES = [0, 10, 25, 50, 100, 250, 500, 1000, 2500];
|
|
29
|
+
/** Bucket a sorted/unsorted latency sample into {@link HISTOGRAM_EDGES}. */
|
|
30
|
+
function buildHistogram(latencies) {
|
|
31
|
+
const edges = [...HISTOGRAM_EDGES];
|
|
32
|
+
const counts = new Array(edges.length).fill(0);
|
|
33
|
+
for (const ms of latencies) {
|
|
34
|
+
let idx = 0;
|
|
35
|
+
for (let i = 0; i < edges.length; i++) {
|
|
36
|
+
if (ms >= edges[i]) {
|
|
37
|
+
idx = i;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
counts[idx] = (counts[idx] ?? 0) + 1;
|
|
41
|
+
}
|
|
42
|
+
return edges.map((from, i) => ({
|
|
43
|
+
from,
|
|
44
|
+
to: i + 1 < edges.length ? edges[i + 1] : null,
|
|
45
|
+
count: counts[i] ?? 0,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
/** Mean of a numeric sample, or `0` when empty. */
|
|
49
|
+
function mean(xs) {
|
|
50
|
+
if (xs.length === 0) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
return xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Aggregate raw events into a {@link ObservabilitySummary}. Pure + deterministic
|
|
57
|
+
* (no clock, no I/O) so it is unit-testable with injected events. The storage
|
|
58
|
+
* adapter intentionally does NOT aggregate — all rollups happen here.
|
|
59
|
+
*
|
|
60
|
+
* - Latency percentiles only consider events with a numeric `ms` (including
|
|
61
|
+
* `ms === 0`); events without timing are excluded from p50/p95 but still
|
|
62
|
+
* counted toward totals + error rate.
|
|
63
|
+
* - `ok === false` (or a present `error`) counts as an error.
|
|
64
|
+
* - Tools group by `tool`, falling back to `method`, then `"unknown"`.
|
|
65
|
+
*/
|
|
66
|
+
export function summarize(events, opts) {
|
|
67
|
+
const bucketMs = opts.bucketMs ?? 60_000;
|
|
68
|
+
const topLimit = opts.topLimit ?? 10;
|
|
69
|
+
const isError = (e) => e.ok === false || (e.ok === undefined && e.error !== undefined);
|
|
70
|
+
const allLatencies = [];
|
|
71
|
+
const buckets = new Map();
|
|
72
|
+
const tools = new Map();
|
|
73
|
+
const methods = new Map();
|
|
74
|
+
let minTs = Number.POSITIVE_INFINITY;
|
|
75
|
+
let maxTs = Number.NEGATIVE_INFINITY;
|
|
76
|
+
let errors = 0;
|
|
77
|
+
for (const e of events) {
|
|
78
|
+
const errored = isError(e);
|
|
79
|
+
if (errored) {
|
|
80
|
+
errors += 1;
|
|
81
|
+
}
|
|
82
|
+
if (typeof e.ms === "number" && Number.isFinite(e.ms)) {
|
|
83
|
+
allLatencies.push(e.ms);
|
|
84
|
+
}
|
|
85
|
+
if (e.ts < minTs) {
|
|
86
|
+
minTs = e.ts;
|
|
87
|
+
}
|
|
88
|
+
if (e.ts > maxTs) {
|
|
89
|
+
maxTs = e.ts;
|
|
90
|
+
}
|
|
91
|
+
const methodName = e.method ?? "unknown";
|
|
92
|
+
const mStat = methods.get(methodName) ?? { count: 0, errors: 0 };
|
|
93
|
+
mStat.count += 1;
|
|
94
|
+
if (errored) {
|
|
95
|
+
mStat.errors += 1;
|
|
96
|
+
}
|
|
97
|
+
methods.set(methodName, mStat);
|
|
98
|
+
const bucketTs = Math.floor(e.ts / bucketMs) * bucketMs;
|
|
99
|
+
const bucket = buckets.get(bucketTs) ?? {
|
|
100
|
+
ts: bucketTs,
|
|
101
|
+
count: 0,
|
|
102
|
+
errors: 0,
|
|
103
|
+
};
|
|
104
|
+
bucket.count += 1;
|
|
105
|
+
if (errored) {
|
|
106
|
+
bucket.errors += 1;
|
|
107
|
+
}
|
|
108
|
+
buckets.set(bucketTs, bucket);
|
|
109
|
+
const name = e.tool ?? e.method ?? "unknown";
|
|
110
|
+
const stat = tools.get(name) ?? { count: 0, errors: 0, latencies: [] };
|
|
111
|
+
stat.count += 1;
|
|
112
|
+
if (errored) {
|
|
113
|
+
stat.errors += 1;
|
|
114
|
+
}
|
|
115
|
+
if (typeof e.ms === "number" && Number.isFinite(e.ms)) {
|
|
116
|
+
stat.latencies.push(e.ms);
|
|
117
|
+
}
|
|
118
|
+
tools.set(name, stat);
|
|
119
|
+
}
|
|
120
|
+
allLatencies.sort((a, b) => a - b);
|
|
121
|
+
const total = events.length;
|
|
122
|
+
const toolStats = [...tools.entries()].map(([name, s]) => {
|
|
123
|
+
const sorted = s.latencies.slice().sort((a, b) => a - b);
|
|
124
|
+
return {
|
|
125
|
+
name,
|
|
126
|
+
count: s.count,
|
|
127
|
+
errors: s.errors,
|
|
128
|
+
errorRate: s.count === 0 ? 0 : s.errors / s.count,
|
|
129
|
+
p50: percentile(sorted, 0.5),
|
|
130
|
+
p95: percentile(sorted, 0.95),
|
|
131
|
+
p99: percentile(sorted, 0.99),
|
|
132
|
+
avg: mean(sorted),
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
const topTools = toolStats
|
|
136
|
+
.slice()
|
|
137
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name))
|
|
138
|
+
.slice(0, topLimit);
|
|
139
|
+
// Slowest by p95, considering only tools that actually have timing data.
|
|
140
|
+
const slowestTools = toolStats
|
|
141
|
+
.filter((t) => t.p95 > 0)
|
|
142
|
+
.sort((a, b) => b.p95 - a.p95 || a.name.localeCompare(b.name))
|
|
143
|
+
.slice(0, topLimit);
|
|
144
|
+
const byMethod = [...methods.entries()]
|
|
145
|
+
.map(([method, s]) => ({ method, count: s.count, errors: s.errors }))
|
|
146
|
+
.sort((a, b) => b.count - a.count || a.method.localeCompare(b.method));
|
|
147
|
+
const callsOverTime = [...buckets.values()].sort((a, b) => a.ts - b.ts);
|
|
148
|
+
// Throughput: calls / minute over the observed span. Use the actual event
|
|
149
|
+
// span (since → newest event) so a long default window with little data
|
|
150
|
+
// doesn't dilute the rate to ~0. Falls back to 0 for empty input.
|
|
151
|
+
const spanMs = total === 0 ? 0 : Math.max(maxTs - opts.since, maxTs - minTs);
|
|
152
|
+
const throughputPerMin = spanMs > 0 ? total / (spanMs / 60_000) : total > 0 ? total : 0;
|
|
153
|
+
return {
|
|
154
|
+
enabled: true,
|
|
155
|
+
since: opts.since,
|
|
156
|
+
total,
|
|
157
|
+
errors,
|
|
158
|
+
errorRate: total === 0 ? 0 : errors / total,
|
|
159
|
+
p50: percentile(allLatencies, 0.5),
|
|
160
|
+
p95: percentile(allLatencies, 0.95),
|
|
161
|
+
p99: percentile(allLatencies, 0.99),
|
|
162
|
+
avg: mean(allLatencies),
|
|
163
|
+
throughputPerMin,
|
|
164
|
+
bucketMs,
|
|
165
|
+
callsOverTime,
|
|
166
|
+
topTools,
|
|
167
|
+
slowestTools,
|
|
168
|
+
byMethod,
|
|
169
|
+
latencyHistogram: buildHistogram(allLatencies),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Empty 200 payload returned when there is no active storage. */
|
|
173
|
+
function disabledSummary(bucketMs) {
|
|
174
|
+
return {
|
|
175
|
+
enabled: false,
|
|
176
|
+
total: 0,
|
|
177
|
+
errors: 0,
|
|
178
|
+
errorRate: 0,
|
|
179
|
+
p50: 0,
|
|
180
|
+
p95: 0,
|
|
181
|
+
p99: 0,
|
|
182
|
+
avg: 0,
|
|
183
|
+
throughputPerMin: 0,
|
|
184
|
+
bucketMs,
|
|
185
|
+
callsOverTime: [],
|
|
186
|
+
topTools: [],
|
|
187
|
+
slowestTools: [],
|
|
188
|
+
byMethod: [],
|
|
189
|
+
latencyHistogram: [],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** Parse a non-negative integer query param, or `undefined` if absent/invalid. */
|
|
193
|
+
function intParam(raw) {
|
|
194
|
+
if (typeof raw !== "string") {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
const n = Number.parseInt(raw, 10);
|
|
198
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
199
|
+
}
|
|
200
|
+
function strParam(raw) {
|
|
201
|
+
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
|
202
|
+
}
|
|
203
|
+
const DEFAULT_SUMMARY_WINDOW_MS = 60 * 60 * 1000; // 1h
|
|
204
|
+
const DEFAULT_LIMIT = 200;
|
|
205
|
+
const STREAM_POLL_MS = 1000;
|
|
206
|
+
/**
|
|
207
|
+
* Build the observability read API router. All routes read storage
|
|
208
|
+
* per-request via {@link getActiveStorage} (overridable for tests) so the
|
|
209
|
+
* disabled / not-yet-installed path returns a 200 empty payload, never a 500.
|
|
210
|
+
*/
|
|
211
|
+
export function createObservabilityRouter(getStorage = getActiveStorage) {
|
|
212
|
+
const router = express.Router();
|
|
213
|
+
const base = "/__enpilink/observability";
|
|
214
|
+
// GET /summary — aggregate from queryEvents (counts, error rate, p50/p95,
|
|
215
|
+
// top tools, calls over time). Aggregation happens here, not in the adapter.
|
|
216
|
+
router.get(`${base}/summary`, async (req, res) => {
|
|
217
|
+
const bucketMs = intParam(req.query.bucketMs) || 60_000;
|
|
218
|
+
const storage = getStorage();
|
|
219
|
+
if (!storage) {
|
|
220
|
+
res.json(disabledSummary(bucketMs));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const sinceParam = intParam(req.query.since);
|
|
224
|
+
const since = sinceParam !== undefined
|
|
225
|
+
? sinceParam
|
|
226
|
+
: Date.now() - DEFAULT_SUMMARY_WINDOW_MS;
|
|
227
|
+
try {
|
|
228
|
+
// limit:0 from memory means "no events"; pass a large cap so the summary
|
|
229
|
+
// reflects the full window. queryEvents returns most-recent-first.
|
|
230
|
+
const events = await storage.queryEvents({
|
|
231
|
+
since,
|
|
232
|
+
type: strParam(req.query.type),
|
|
233
|
+
tool: strParam(req.query.tool),
|
|
234
|
+
limit: intParam(req.query.limit) ?? 5000,
|
|
235
|
+
});
|
|
236
|
+
res.json(summarize(events, { since, bucketMs }));
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
res.json(disabledSummary(bucketMs));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
// GET /events?since&type&tool&limit — pass-through to storage.queryEvents.
|
|
243
|
+
router.get(`${base}/events`, async (req, res) => {
|
|
244
|
+
const storage = getStorage();
|
|
245
|
+
if (!storage) {
|
|
246
|
+
res.json({ enabled: false, events: [] });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const events = await storage.queryEvents({
|
|
251
|
+
since: intParam(req.query.since),
|
|
252
|
+
type: strParam(req.query.type),
|
|
253
|
+
tool: strParam(req.query.tool),
|
|
254
|
+
limit: intParam(req.query.limit) ?? DEFAULT_LIMIT,
|
|
255
|
+
});
|
|
256
|
+
res.json({ enabled: true, events });
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
res.json({ enabled: false, events: [] });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
// GET /logs?since&level&limit — pass-through to storage.queryLogs.
|
|
263
|
+
router.get(`${base}/logs`, async (req, res) => {
|
|
264
|
+
const storage = getStorage();
|
|
265
|
+
if (!storage) {
|
|
266
|
+
res.json({ enabled: false, logs: [] });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const logs = await storage.queryLogs({
|
|
271
|
+
since: intParam(req.query.since),
|
|
272
|
+
level: strParam(req.query.level),
|
|
273
|
+
limit: intParam(req.query.limit) ?? DEFAULT_LIMIT,
|
|
274
|
+
});
|
|
275
|
+
res.json({ enabled: true, logs });
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
res.json({ enabled: false, logs: [] });
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
// GET /stream — poll-based SSE. Mirrors the tunnel SSE style: set the
|
|
282
|
+
// event-stream headers, push frames, and clean up the interval on client
|
|
283
|
+
// disconnect. We poll storage on an interval using a `since` cursor and push
|
|
284
|
+
// only events/logs newer than the cursor. Resilient to storage being absent.
|
|
285
|
+
router.get(`${base}/stream`, async (req, res) => {
|
|
286
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
287
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
288
|
+
res.setHeader("Connection", "keep-alive");
|
|
289
|
+
res.flushHeaders?.();
|
|
290
|
+
// Start the cursor at request time so we only stream NEW activity.
|
|
291
|
+
let cursor = intParam(req.query.since) ?? Date.now();
|
|
292
|
+
const send = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
293
|
+
// Immediately tell the client whether analytics is live.
|
|
294
|
+
send("status", { enabled: getStorage() !== null });
|
|
295
|
+
let stopped = false;
|
|
296
|
+
const poll = async () => {
|
|
297
|
+
if (stopped) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const storage = getStorage();
|
|
301
|
+
if (!storage) {
|
|
302
|
+
// Heartbeat keeps the connection (and proxies) alive while OFF.
|
|
303
|
+
send("status", { enabled: false });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const [events, logs] = await Promise.all([
|
|
308
|
+
storage.queryEvents({ since: cursor, limit: 200 }),
|
|
309
|
+
storage.queryLogs({ since: cursor, limit: 200 }),
|
|
310
|
+
]);
|
|
311
|
+
// queryEvents/queryLogs are most-recent-first; advance the cursor past
|
|
312
|
+
// the newest ts so the next poll doesn't re-send the same rows.
|
|
313
|
+
const newest = Math.max(events[0]?.ts ?? 0, logs[0]?.ts ?? 0, cursor);
|
|
314
|
+
// Use `> cursor` so the same-ms boundary isn't dropped on the first
|
|
315
|
+
// pass but isn't re-sent afterward (cursor advances to newest + 1).
|
|
316
|
+
if (events.length > 0) {
|
|
317
|
+
send("events", events);
|
|
318
|
+
}
|
|
319
|
+
if (logs.length > 0) {
|
|
320
|
+
send("logs", logs);
|
|
321
|
+
}
|
|
322
|
+
if (newest >= cursor) {
|
|
323
|
+
cursor = newest + 1;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Swallow — a transient storage error must not kill the stream.
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const timer = setInterval(() => void poll(), STREAM_POLL_MS);
|
|
331
|
+
const cleanup = () => {
|
|
332
|
+
stopped = true;
|
|
333
|
+
clearInterval(timer);
|
|
334
|
+
res.end();
|
|
335
|
+
};
|
|
336
|
+
req.on("close", cleanup);
|
|
337
|
+
});
|
|
338
|
+
return router;
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=observability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/server/observability.ts"],"names":[],"mappings":"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","sourcesContent":["import express, { type Router } from \"express\";\nimport { getActiveStorage } from \"./log-sink.js\";\nimport type { AnalyticsEvent, StorageAdapter } from \"./storage/types.js\";\n\n/**\n * Observability read API (M3). Pure core — reads the SAME active\n * {@link StorageAdapter} the analytics middleware + log sink write to, via\n * {@link getActiveStorage}. It does NOT depend on `@enpilink/console`.\n *\n * Mounted dev-only (under the `NODE_ENV !== \"production\"` block in\n * `express.ts`) at `/__enpilink/observability/`. The `/__enpilink/` prefix\n * avoids colliding with user-defined routes; the dev-only mount keeps prod\n * surface unchanged (prod admin is M5).\n *\n * Graceful when analytics is OFF: storage is read PER-REQUEST (it may be `null`\n * when disabled or before the server applies middleware). When there is no\n * active storage every route returns a 200 with `{ enabled: false }` / an empty\n * payload — NEVER a 500. The storage may also change between requests, so we\n * never cache it.\n */\n\n/** A single point on the calls-over-time series. */\nexport interface TimeBucket {\n /** Bucket start, epoch ms (aligned to `bucketMs`). */\n ts: number;\n /** Total calls in the bucket. */\n count: number;\n /** Errored calls in the bucket. */\n errors: number;\n}\n\n/** Per-tool (or per-method) aggregate row. */\nexport interface ToolStat {\n /** Tool name, or the method name when there is no tool (non-`tools/call`). */\n name: string;\n /** Total calls. */\n count: number;\n /** Errored calls. */\n errors: number;\n /** Error rate in `[0, 1]`. */\n errorRate: number;\n /** Median latency (ms). */\n p50: number;\n /** 95th-percentile latency (ms). */\n p95: number;\n /** 99th-percentile latency (ms). */\n p99: number;\n /** Average latency (ms) over events with timing. */\n avg: number;\n}\n\n/** Per-MCP-method aggregate row (e.g. `tools/call`, `tools/list`). */\nexport interface MethodStat {\n /** MCP method name, or `\"unknown\"`. */\n method: string;\n /** Total calls. */\n count: number;\n /** Errored calls. */\n errors: number;\n}\n\n/** A single bar of the latency histogram. */\nexport interface LatencyBucket {\n /** Inclusive lower bound of the bucket (ms). */\n from: number;\n /** Exclusive upper bound of the bucket (ms), or `null` for the open top bin. */\n to: number | null;\n /** Number of events whose latency fell in this bucket. */\n count: number;\n}\n\n/** The aggregate returned by `GET /summary` when analytics is enabled. */\nexport interface ObservabilitySummary {\n enabled: true;\n /** Window the summary was computed over (epoch ms). */\n since: number;\n /** Total events considered. */\n total: number;\n /** Errored events. */\n errors: number;\n /** Error rate in `[0, 1]`. */\n errorRate: number;\n /** Median latency across all events (ms). */\n p50: number;\n /** 95th-percentile latency across all events (ms). */\n p95: number;\n /** 99th-percentile latency across all events (ms). */\n p99: number;\n /** Average latency across all timed events (ms). */\n avg: number;\n /** Throughput: calls per minute over the window `[since, latest event]`. */\n throughputPerMin: number;\n /** Bucket width used for {@link callsOverTime} (ms). */\n bucketMs: number;\n /** Calls-over-time series, oldest bucket first. */\n callsOverTime: TimeBucket[];\n /** Top tools/methods by call count (descending). */\n topTools: ToolStat[];\n /** Slowest tools/methods by p95 latency (descending). Only timed tools. */\n slowestTools: ToolStat[];\n /** Calls grouped by MCP method (descending by count). */\n byMethod: MethodStat[];\n /** Latency distribution histogram (fixed buckets). */\n latencyHistogram: LatencyBucket[];\n}\n\n/** The disabled/no-storage shape — a valid 200 payload, never a 500. */\nexport interface ObservabilityDisabled {\n enabled: false;\n total: 0;\n errors: 0;\n errorRate: 0;\n p50: 0;\n p95: 0;\n p99: 0;\n avg: 0;\n throughputPerMin: 0;\n bucketMs: number;\n callsOverTime: [];\n topTools: [];\n slowestTools: [];\n byMethod: [];\n latencyHistogram: [];\n}\n\n/**\n * Percentile of a numeric sample using nearest-rank (lower) interpolation.\n * Returns `0` for an empty sample. `p` is in `[0, 1]`. Tolerates `ms === 0`\n * (a zero-latency call is a real data point, not a missing one).\n */\nexport function percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) {\n return 0;\n }\n const rank = p * (sorted.length - 1);\n const lo = Math.floor(rank);\n const hi = Math.ceil(rank);\n const loVal = sorted[lo] ?? 0;\n if (lo === hi) {\n return loVal;\n }\n const hiVal = sorted[hi] ?? loVal;\n const frac = rank - lo;\n return loVal + (hiVal - loVal) * frac;\n}\n\n/**\n * Fixed latency-histogram bucket edges (ms). Each pair `[edges[i], edges[i+1])`\n * is a bucket; the final bucket is open-ended (`>= last edge`). Chosen to span\n * sub-ms lookups through multi-second LLM/report calls.\n */\nconst HISTOGRAM_EDGES = [0, 10, 25, 50, 100, 250, 500, 1000, 2500] as const;\n\n/** Bucket a sorted/unsorted latency sample into {@link HISTOGRAM_EDGES}. */\nfunction buildHistogram(latencies: number[]): LatencyBucket[] {\n const edges: number[] = [...HISTOGRAM_EDGES];\n const counts = new Array<number>(edges.length).fill(0);\n for (const ms of latencies) {\n let idx = 0;\n for (let i = 0; i < edges.length; i++) {\n if (ms >= (edges[i] as number)) {\n idx = i;\n }\n }\n counts[idx] = (counts[idx] ?? 0) + 1;\n }\n return edges.map((from, i) => ({\n from,\n to: i + 1 < edges.length ? (edges[i + 1] as number) : null,\n count: counts[i] ?? 0,\n }));\n}\n\n/** Mean of a numeric sample, or `0` when empty. */\nfunction mean(xs: number[]): number {\n if (xs.length === 0) {\n return 0;\n }\n return xs.reduce((a, b) => a + b, 0) / xs.length;\n}\n\n/** Options for {@link summarize}. */\nexport interface SummarizeOptions {\n /** Lower bound used in the response (epoch ms). */\n since: number;\n /** Bucket width for the calls-over-time series (ms). Defaults to 60_000. */\n bucketMs?: number;\n /** Max tools in `topTools`. Defaults to 10. */\n topLimit?: number;\n}\n\n/**\n * Aggregate raw events into a {@link ObservabilitySummary}. Pure + deterministic\n * (no clock, no I/O) so it is unit-testable with injected events. The storage\n * adapter intentionally does NOT aggregate — all rollups happen here.\n *\n * - Latency percentiles only consider events with a numeric `ms` (including\n * `ms === 0`); events without timing are excluded from p50/p95 but still\n * counted toward totals + error rate.\n * - `ok === false` (or a present `error`) counts as an error.\n * - Tools group by `tool`, falling back to `method`, then `\"unknown\"`.\n */\nexport function summarize(\n events: AnalyticsEvent[],\n opts: SummarizeOptions,\n): ObservabilitySummary {\n const bucketMs = opts.bucketMs ?? 60_000;\n const topLimit = opts.topLimit ?? 10;\n\n const isError = (e: AnalyticsEvent): boolean =>\n e.ok === false || (e.ok === undefined && e.error !== undefined);\n\n const allLatencies: number[] = [];\n const buckets = new Map<number, TimeBucket>();\n const tools = new Map<\n string,\n { count: number; errors: number; latencies: number[] }\n >();\n const methods = new Map<string, { count: number; errors: number }>();\n let minTs = Number.POSITIVE_INFINITY;\n let maxTs = Number.NEGATIVE_INFINITY;\n\n let errors = 0;\n for (const e of events) {\n const errored = isError(e);\n if (errored) {\n errors += 1;\n }\n if (typeof e.ms === \"number\" && Number.isFinite(e.ms)) {\n allLatencies.push(e.ms);\n }\n if (e.ts < minTs) {\n minTs = e.ts;\n }\n if (e.ts > maxTs) {\n maxTs = e.ts;\n }\n\n const methodName = e.method ?? \"unknown\";\n const mStat = methods.get(methodName) ?? { count: 0, errors: 0 };\n mStat.count += 1;\n if (errored) {\n mStat.errors += 1;\n }\n methods.set(methodName, mStat);\n\n const bucketTs = Math.floor(e.ts / bucketMs) * bucketMs;\n const bucket = buckets.get(bucketTs) ?? {\n ts: bucketTs,\n count: 0,\n errors: 0,\n };\n bucket.count += 1;\n if (errored) {\n bucket.errors += 1;\n }\n buckets.set(bucketTs, bucket);\n\n const name = e.tool ?? e.method ?? \"unknown\";\n const stat = tools.get(name) ?? { count: 0, errors: 0, latencies: [] };\n stat.count += 1;\n if (errored) {\n stat.errors += 1;\n }\n if (typeof e.ms === \"number\" && Number.isFinite(e.ms)) {\n stat.latencies.push(e.ms);\n }\n tools.set(name, stat);\n }\n\n allLatencies.sort((a, b) => a - b);\n const total = events.length;\n\n const toolStats: ToolStat[] = [...tools.entries()].map(([name, s]) => {\n const sorted = s.latencies.slice().sort((a, b) => a - b);\n return {\n name,\n count: s.count,\n errors: s.errors,\n errorRate: s.count === 0 ? 0 : s.errors / s.count,\n p50: percentile(sorted, 0.5),\n p95: percentile(sorted, 0.95),\n p99: percentile(sorted, 0.99),\n avg: mean(sorted),\n };\n });\n\n const topTools = toolStats\n .slice()\n .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name))\n .slice(0, topLimit);\n\n // Slowest by p95, considering only tools that actually have timing data.\n const slowestTools = toolStats\n .filter((t) => t.p95 > 0)\n .sort((a, b) => b.p95 - a.p95 || a.name.localeCompare(b.name))\n .slice(0, topLimit);\n\n const byMethod: MethodStat[] = [...methods.entries()]\n .map(([method, s]) => ({ method, count: s.count, errors: s.errors }))\n .sort((a, b) => b.count - a.count || a.method.localeCompare(b.method));\n\n const callsOverTime = [...buckets.values()].sort((a, b) => a.ts - b.ts);\n\n // Throughput: calls / minute over the observed span. Use the actual event\n // span (since → newest event) so a long default window with little data\n // doesn't dilute the rate to ~0. Falls back to 0 for empty input.\n const spanMs = total === 0 ? 0 : Math.max(maxTs - opts.since, maxTs - minTs);\n const throughputPerMin =\n spanMs > 0 ? total / (spanMs / 60_000) : total > 0 ? total : 0;\n\n return {\n enabled: true,\n since: opts.since,\n total,\n errors,\n errorRate: total === 0 ? 0 : errors / total,\n p50: percentile(allLatencies, 0.5),\n p95: percentile(allLatencies, 0.95),\n p99: percentile(allLatencies, 0.99),\n avg: mean(allLatencies),\n throughputPerMin,\n bucketMs,\n callsOverTime,\n topTools,\n slowestTools,\n byMethod,\n latencyHistogram: buildHistogram(allLatencies),\n };\n}\n\n/** Empty 200 payload returned when there is no active storage. */\nfunction disabledSummary(bucketMs: number): ObservabilityDisabled {\n return {\n enabled: false,\n total: 0,\n errors: 0,\n errorRate: 0,\n p50: 0,\n p95: 0,\n p99: 0,\n avg: 0,\n throughputPerMin: 0,\n bucketMs,\n callsOverTime: [],\n topTools: [],\n slowestTools: [],\n byMethod: [],\n latencyHistogram: [],\n };\n}\n\n/** Parse a non-negative integer query param, or `undefined` if absent/invalid. */\nfunction intParam(raw: unknown): number | undefined {\n if (typeof raw !== \"string\") {\n return undefined;\n }\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) && n >= 0 ? n : undefined;\n}\n\nfunction strParam(raw: unknown): string | undefined {\n return typeof raw === \"string\" && raw.length > 0 ? raw : undefined;\n}\n\nconst DEFAULT_SUMMARY_WINDOW_MS = 60 * 60 * 1000; // 1h\nconst DEFAULT_LIMIT = 200;\nconst STREAM_POLL_MS = 1000;\n\n/**\n * Build the observability read API router. All routes read storage\n * per-request via {@link getActiveStorage} (overridable for tests) so the\n * disabled / not-yet-installed path returns a 200 empty payload, never a 500.\n */\nexport function createObservabilityRouter(\n getStorage: () => StorageAdapter | null = getActiveStorage,\n): Router {\n const router = express.Router();\n const base = \"/__enpilink/observability\";\n\n // GET /summary — aggregate from queryEvents (counts, error rate, p50/p95,\n // top tools, calls over time). Aggregation happens here, not in the adapter.\n router.get(`${base}/summary`, async (req, res) => {\n const bucketMs = intParam(req.query.bucketMs) || 60_000;\n const storage = getStorage();\n if (!storage) {\n res.json(disabledSummary(bucketMs));\n return;\n }\n const sinceParam = intParam(req.query.since);\n const since =\n sinceParam !== undefined\n ? sinceParam\n : Date.now() - DEFAULT_SUMMARY_WINDOW_MS;\n try {\n // limit:0 from memory means \"no events\"; pass a large cap so the summary\n // reflects the full window. queryEvents returns most-recent-first.\n const events = await storage.queryEvents({\n since,\n type: strParam(req.query.type),\n tool: strParam(req.query.tool),\n limit: intParam(req.query.limit) ?? 5000,\n });\n res.json(summarize(events, { since, bucketMs }));\n } catch {\n res.json(disabledSummary(bucketMs));\n }\n });\n\n // GET /events?since&type&tool&limit — pass-through to storage.queryEvents.\n router.get(`${base}/events`, async (req, res) => {\n const storage = getStorage();\n if (!storage) {\n res.json({ enabled: false, events: [] });\n return;\n }\n try {\n const events = await storage.queryEvents({\n since: intParam(req.query.since),\n type: strParam(req.query.type),\n tool: strParam(req.query.tool),\n limit: intParam(req.query.limit) ?? DEFAULT_LIMIT,\n });\n res.json({ enabled: true, events });\n } catch {\n res.json({ enabled: false, events: [] });\n }\n });\n\n // GET /logs?since&level&limit — pass-through to storage.queryLogs.\n router.get(`${base}/logs`, async (req, res) => {\n const storage = getStorage();\n if (!storage) {\n res.json({ enabled: false, logs: [] });\n return;\n }\n try {\n const logs = await storage.queryLogs({\n since: intParam(req.query.since),\n level: strParam(req.query.level),\n limit: intParam(req.query.limit) ?? DEFAULT_LIMIT,\n });\n res.json({ enabled: true, logs });\n } catch {\n res.json({ enabled: false, logs: [] });\n }\n });\n\n // GET /stream — poll-based SSE. Mirrors the tunnel SSE style: set the\n // event-stream headers, push frames, and clean up the interval on client\n // disconnect. We poll storage on an interval using a `since` cursor and push\n // only events/logs newer than the cursor. Resilient to storage being absent.\n router.get(`${base}/stream`, async (req, res) => {\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"Connection\", \"keep-alive\");\n res.flushHeaders?.();\n\n // Start the cursor at request time so we only stream NEW activity.\n let cursor = intParam(req.query.since) ?? Date.now();\n\n const send = (event: string, data: unknown): boolean =>\n res.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n\n // Immediately tell the client whether analytics is live.\n send(\"status\", { enabled: getStorage() !== null });\n\n let stopped = false;\n const poll = async () => {\n if (stopped) {\n return;\n }\n const storage = getStorage();\n if (!storage) {\n // Heartbeat keeps the connection (and proxies) alive while OFF.\n send(\"status\", { enabled: false });\n return;\n }\n try {\n const [events, logs] = await Promise.all([\n storage.queryEvents({ since: cursor, limit: 200 }),\n storage.queryLogs({ since: cursor, limit: 200 }),\n ]);\n // queryEvents/queryLogs are most-recent-first; advance the cursor past\n // the newest ts so the next poll doesn't re-send the same rows.\n const newest = Math.max(events[0]?.ts ?? 0, logs[0]?.ts ?? 0, cursor);\n // Use `> cursor` so the same-ms boundary isn't dropped on the first\n // pass but isn't re-sent afterward (cursor advances to newest + 1).\n if (events.length > 0) {\n send(\"events\", events);\n }\n if (logs.length > 0) {\n send(\"logs\", logs);\n }\n if (newest >= cursor) {\n cursor = newest + 1;\n }\n } catch {\n // Swallow — a transient storage error must not kill the stream.\n }\n };\n\n const timer = setInterval(() => void poll(), STREAM_POLL_MS);\n const cleanup = () => {\n stopped = true;\n clearInterval(timer);\n res.end();\n };\n req.on(\"close\", cleanup);\n });\n\n return router;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { createObservabilityRouter, percentile, summarize, } from "./observability.js";
|
|
4
|
+
import { MemoryStorageAdapter } from "./storage/memory.js";
|
|
5
|
+
function ev(p) {
|
|
6
|
+
return { ts: 0, type: "tool_call", ...p };
|
|
7
|
+
}
|
|
8
|
+
describe("percentile", () => {
|
|
9
|
+
it("returns 0 for an empty sample", () => {
|
|
10
|
+
expect(percentile([], 0.5)).toBe(0);
|
|
11
|
+
expect(percentile([], 0.95)).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
it("returns the single value for a one-element sample", () => {
|
|
14
|
+
expect(percentile([42], 0.5)).toBe(42);
|
|
15
|
+
});
|
|
16
|
+
it("computes p50/p95 by nearest-rank interpolation", () => {
|
|
17
|
+
const s = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
18
|
+
expect(percentile(s, 0.5)).toBeCloseTo(55, 5);
|
|
19
|
+
expect(percentile(s, 0.95)).toBeCloseTo(95.5, 5);
|
|
20
|
+
});
|
|
21
|
+
it("tolerates a zero-latency sample (ms === 0)", () => {
|
|
22
|
+
expect(percentile([0, 0, 0], 0.5)).toBe(0);
|
|
23
|
+
expect(percentile([0, 10], 0.5)).toBe(5);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe("summarize", () => {
|
|
27
|
+
it("aggregates totals, error rate, p50/p95, top tools, and buckets", () => {
|
|
28
|
+
const base = 1_000_000;
|
|
29
|
+
const events = [
|
|
30
|
+
ev({ ts: base + 0, tool: "echo", ms: 10, ok: true }),
|
|
31
|
+
ev({ ts: base + 1000, tool: "echo", ms: 20, ok: true }),
|
|
32
|
+
ev({ ts: base + 2000, tool: "echo", ms: 30, ok: false }),
|
|
33
|
+
ev({ ts: base + 70_000, tool: "search", ms: 100, ok: true }),
|
|
34
|
+
ev({ ts: base + 71_000, tool: "search", ms: 0, ok: true }),
|
|
35
|
+
];
|
|
36
|
+
const s = summarize(events, { since: base, bucketMs: 60_000 });
|
|
37
|
+
expect(s.enabled).toBe(true);
|
|
38
|
+
expect(s.total).toBe(5);
|
|
39
|
+
expect(s.errors).toBe(1);
|
|
40
|
+
expect(s.errorRate).toBeCloseTo(0.2, 5);
|
|
41
|
+
// overall latencies sorted: [0,10,20,30,100]
|
|
42
|
+
expect(s.p50).toBe(20);
|
|
43
|
+
// Two 60s buckets: first has 3 (1 error), second has 2.
|
|
44
|
+
expect(s.callsOverTime).toHaveLength(2);
|
|
45
|
+
const [b0, b1] = s.callsOverTime;
|
|
46
|
+
expect(b0).toMatchObject({ count: 3, errors: 1 });
|
|
47
|
+
expect(b1).toMatchObject({ count: 2, errors: 0 });
|
|
48
|
+
// Buckets oldest-first.
|
|
49
|
+
expect(b0?.ts ?? 0).toBeLessThan(b1?.ts ?? 0);
|
|
50
|
+
// Top tools: echo (3) then search (2).
|
|
51
|
+
expect(s.topTools.map((t) => t.name)).toEqual(["echo", "search"]);
|
|
52
|
+
const [t0, t1] = s.topTools;
|
|
53
|
+
expect(t0).toMatchObject({ count: 3, errors: 1 });
|
|
54
|
+
expect(t0?.errorRate ?? 0).toBeCloseTo(1 / 3, 5);
|
|
55
|
+
expect(t1).toMatchObject({ count: 2, errors: 0 });
|
|
56
|
+
});
|
|
57
|
+
it("counts a thrown error (ok undefined + error set) as an error", () => {
|
|
58
|
+
const s = summarize([ev({ ts: 1, error: "boom" }), ev({ ts: 2, ok: true, ms: 5 })], { since: 0 });
|
|
59
|
+
expect(s.errors).toBe(1);
|
|
60
|
+
expect(s.errorRate).toBe(0.5);
|
|
61
|
+
});
|
|
62
|
+
it("excludes timing-less events from percentiles but counts them in totals", () => {
|
|
63
|
+
const s = summarize([ev({ ts: 1, ok: true }), ev({ ts: 2, ok: true, ms: 50 })], { since: 0 });
|
|
64
|
+
expect(s.total).toBe(2);
|
|
65
|
+
// Only one event has ms → p50 is that value.
|
|
66
|
+
expect(s.p50).toBe(50);
|
|
67
|
+
});
|
|
68
|
+
it("returns zeroed aggregates (not NaN) for no events", () => {
|
|
69
|
+
const s = summarize([], { since: 0 });
|
|
70
|
+
expect(s).toMatchObject({
|
|
71
|
+
total: 0,
|
|
72
|
+
errors: 0,
|
|
73
|
+
errorRate: 0,
|
|
74
|
+
p50: 0,
|
|
75
|
+
p95: 0,
|
|
76
|
+
p99: 0,
|
|
77
|
+
avg: 0,
|
|
78
|
+
throughputPerMin: 0,
|
|
79
|
+
callsOverTime: [],
|
|
80
|
+
topTools: [],
|
|
81
|
+
slowestTools: [],
|
|
82
|
+
byMethod: [],
|
|
83
|
+
});
|
|
84
|
+
// The histogram always has fixed buckets; they're just all zero.
|
|
85
|
+
expect(s.latencyHistogram.every((b) => b.count === 0)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
it("computes p99 + avg overall and per-tool", () => {
|
|
88
|
+
const events = [];
|
|
89
|
+
// echo: latencies 1..100 → avg 50.5, p99 ~99
|
|
90
|
+
for (let i = 1; i <= 100; i++) {
|
|
91
|
+
events.push(ev({ ts: i, tool: "echo", ms: i, ok: true }));
|
|
92
|
+
}
|
|
93
|
+
const s = summarize(events, { since: 0 });
|
|
94
|
+
expect(s.avg).toBeCloseTo(50.5, 5);
|
|
95
|
+
// nearest-rank: p99 of 1..100 → 99 + 0.01 interp
|
|
96
|
+
expect(s.p99).toBeCloseTo(99, 0);
|
|
97
|
+
const echo = s.topTools.find((t) => t.name === "echo");
|
|
98
|
+
expect(echo?.avg).toBeCloseTo(50.5, 5);
|
|
99
|
+
expect(echo?.p99).toBeGreaterThan(echo?.p95 ?? 0);
|
|
100
|
+
});
|
|
101
|
+
it("groups calls by method and ranks slowest tools by p95", () => {
|
|
102
|
+
const s = summarize([
|
|
103
|
+
ev({ ts: 1, tool: "fast", method: "tools/call", ms: 5, ok: true }),
|
|
104
|
+
ev({ ts: 2, tool: "fast", method: "tools/call", ms: 7, ok: true }),
|
|
105
|
+
ev({ ts: 3, tool: "slow", method: "tools/call", ms: 900, ok: true }),
|
|
106
|
+
ev({ ts: 4, method: "tools/list", ms: 2, ok: true }),
|
|
107
|
+
], { since: 0 });
|
|
108
|
+
// byMethod descending by count: tools/call (3) then tools/list (1)
|
|
109
|
+
expect(s.byMethod.map((m) => m.method)).toEqual([
|
|
110
|
+
"tools/call",
|
|
111
|
+
"tools/list",
|
|
112
|
+
]);
|
|
113
|
+
expect(s.byMethod[0]?.count).toBe(3);
|
|
114
|
+
// slowest by p95: slow first
|
|
115
|
+
expect(s.slowestTools[0]?.name).toBe("slow");
|
|
116
|
+
});
|
|
117
|
+
it("buckets latencies into a fixed histogram", () => {
|
|
118
|
+
const s = summarize([
|
|
119
|
+
ev({ ts: 1, tool: "a", ms: 5, ok: true }), // [0,10)
|
|
120
|
+
ev({ ts: 2, tool: "a", ms: 5, ok: true }), // [0,10)
|
|
121
|
+
ev({ ts: 3, tool: "a", ms: 30, ok: true }), // [25,50)
|
|
122
|
+
ev({ ts: 4, tool: "a", ms: 3000, ok: true }), // >=2500
|
|
123
|
+
], { since: 0 });
|
|
124
|
+
const total = s.latencyHistogram.reduce((n, b) => n + b.count, 0);
|
|
125
|
+
expect(total).toBe(4);
|
|
126
|
+
const first = s.latencyHistogram.find((b) => b.from === 0);
|
|
127
|
+
expect(first?.count).toBe(2);
|
|
128
|
+
const top = s.latencyHistogram.find((b) => b.to === null);
|
|
129
|
+
expect(top?.count).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
it("falls back to method then 'unknown' for the tool name", () => {
|
|
132
|
+
const s = summarize([ev({ ts: 1, method: "initialize", ms: 1 }), ev({ ts: 2, ms: 1 })], { since: 0 });
|
|
133
|
+
const names = s.topTools.map((t) => t.name).sort();
|
|
134
|
+
expect(names).toEqual(["initialize", "unknown"]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe("summarize — bucket size per range (M9)", () => {
|
|
138
|
+
const MINUTE = 60_000;
|
|
139
|
+
const HOUR = 60 * MINUTE;
|
|
140
|
+
const DAY = 24 * HOUR;
|
|
141
|
+
const base = 1_700_000_000_000;
|
|
142
|
+
// One event at the start of each of 14 consecutive days.
|
|
143
|
+
const daily = Array.from({ length: 14 }, (_, i) => ev({ ts: base + i * DAY, tool: "echo", ms: 5, ok: true }));
|
|
144
|
+
it("echoes the chosen bucketMs and aligns buckets to it", () => {
|
|
145
|
+
for (const bucketMs of [MINUTE, HOUR, 6 * HOUR, DAY]) {
|
|
146
|
+
const s = summarize(daily, { since: base, bucketMs });
|
|
147
|
+
expect(s.bucketMs).toBe(bucketMs);
|
|
148
|
+
// Every bucket start is aligned to bucketMs.
|
|
149
|
+
for (const b of s.callsOverTime) {
|
|
150
|
+
expect(b.ts % bucketMs).toBe(0);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
it("daily buckets (30d range): 14 daily events → 14 buckets", () => {
|
|
155
|
+
const s = summarize(daily, { since: base, bucketMs: DAY });
|
|
156
|
+
expect(s.callsOverTime).toHaveLength(14);
|
|
157
|
+
expect(s.callsOverTime.every((b) => b.count === 1)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
it("6h buckets (7d range) keep each daily event in its own bucket", () => {
|
|
160
|
+
const s = summarize(daily, { since: base, bucketMs: 6 * HOUR });
|
|
161
|
+
// Events are a full day apart, so no two share a 6h bucket.
|
|
162
|
+
expect(s.callsOverTime).toHaveLength(14);
|
|
163
|
+
});
|
|
164
|
+
it("coarser buckets merge sub-bucket events; finer buckets split them", () => {
|
|
165
|
+
// Two events 90 minutes apart.
|
|
166
|
+
const pair = [
|
|
167
|
+
ev({ ts: base, tool: "echo", ms: 1, ok: true }),
|
|
168
|
+
ev({ ts: base + 90 * MINUTE, tool: "echo", ms: 1, ok: true }),
|
|
169
|
+
];
|
|
170
|
+
// Minute buckets (1h range): two distinct buckets.
|
|
171
|
+
expect(summarize(pair, { since: base, bucketMs: MINUTE }).callsOverTime).toHaveLength(2);
|
|
172
|
+
// Hourly buckets (24h range): still two (they straddle the hour boundary).
|
|
173
|
+
expect(summarize(pair, { since: base, bucketMs: HOUR }).callsOverTime).toHaveLength(2);
|
|
174
|
+
// Daily buckets (30d range): both fall in one day → a single bucket of 2.
|
|
175
|
+
const dayBuckets = summarize(pair, {
|
|
176
|
+
since: base,
|
|
177
|
+
bucketMs: DAY,
|
|
178
|
+
}).callsOverTime;
|
|
179
|
+
expect(dayBuckets).toHaveLength(1);
|
|
180
|
+
expect(dayBuckets[0]?.count).toBe(2);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
// --- Router behaviour (incl. the disabled/no-storage path) ---
|
|
184
|
+
const servers = [];
|
|
185
|
+
afterEach(async () => {
|
|
186
|
+
while (servers.length > 0) {
|
|
187
|
+
await servers.pop()?.close();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
async function mount(getStorage) {
|
|
191
|
+
const app = express();
|
|
192
|
+
app.use(createObservabilityRouter(getStorage));
|
|
193
|
+
const server = app.listen(0, "127.0.0.1");
|
|
194
|
+
await new Promise((resolve) => server.once("listening", resolve));
|
|
195
|
+
const port = server.address().port;
|
|
196
|
+
servers.push({
|
|
197
|
+
close: () => new Promise((resolve) => {
|
|
198
|
+
server.closeAllConnections?.();
|
|
199
|
+
server.close(() => resolve());
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
return `http://127.0.0.1:${port}`;
|
|
203
|
+
}
|
|
204
|
+
describe("createObservabilityRouter (disabled / no storage)", () => {
|
|
205
|
+
it("returns 200 { enabled: false } empty payloads, never 500", async () => {
|
|
206
|
+
const url = await mount(() => null);
|
|
207
|
+
const summary = await fetch(`${url}/__enpilink/observability/summary`);
|
|
208
|
+
expect(summary.status).toBe(200);
|
|
209
|
+
expect(await summary.json()).toMatchObject({
|
|
210
|
+
enabled: false,
|
|
211
|
+
total: 0,
|
|
212
|
+
callsOverTime: [],
|
|
213
|
+
topTools: [],
|
|
214
|
+
});
|
|
215
|
+
const events = await fetch(`${url}/__enpilink/observability/events`);
|
|
216
|
+
expect(events.status).toBe(200);
|
|
217
|
+
expect(await events.json()).toEqual({ enabled: false, events: [] });
|
|
218
|
+
const logs = await fetch(`${url}/__enpilink/observability/logs`);
|
|
219
|
+
expect(logs.status).toBe(200);
|
|
220
|
+
expect(await logs.json()).toEqual({ enabled: false, logs: [] });
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe("createObservabilityRouter (with storage)", () => {
|
|
224
|
+
it("serves a real summary + events from the active adapter", async () => {
|
|
225
|
+
const storage = new MemoryStorageAdapter();
|
|
226
|
+
await storage.init();
|
|
227
|
+
await storage.recordEvent(ev({ ts: Date.now(), tool: "echo", ms: 5, ok: true }));
|
|
228
|
+
await storage.recordEvent(ev({ ts: Date.now(), tool: "echo", ms: 15, ok: false }));
|
|
229
|
+
const url = await mount(() => storage);
|
|
230
|
+
const summary = await (await fetch(`${url}/__enpilink/observability/summary?since=0`)).json();
|
|
231
|
+
expect(summary.enabled).toBe(true);
|
|
232
|
+
expect(summary.total).toBe(2);
|
|
233
|
+
expect(summary.errors).toBe(1);
|
|
234
|
+
expect(summary.topTools[0].name).toBe("echo");
|
|
235
|
+
const events = await (await fetch(`${url}/__enpilink/observability/events?since=0`)).json();
|
|
236
|
+
expect(events.enabled).toBe(true);
|
|
237
|
+
expect(events.events).toHaveLength(2);
|
|
238
|
+
});
|
|
239
|
+
it("returns a 200 empty summary (not 500) when storage throws", async () => {
|
|
240
|
+
const broken = {
|
|
241
|
+
queryEvents: async () => {
|
|
242
|
+
throw new Error("db down");
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
const url = await mount(() => broken);
|
|
246
|
+
const res = await fetch(`${url}/__enpilink/observability/summary`);
|
|
247
|
+
expect(res.status).toBe(200);
|
|
248
|
+
expect(await res.json()).toMatchObject({ enabled: false, total: 0 });
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
//# sourceMappingURL=observability.test.js.map
|