@rudderhq/server 0.6.6-canary.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/register-api-routes.d.ts.map +1 -1
- package/dist/bootstrap/register-api-routes.js +6 -0
- package/dist/bootstrap/register-api-routes.js.map +1 -1
- package/dist/bundled-plugins/plugin-linear/dist/worker.js +152 -9
- package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +3 -3
- package/dist/routes/access.d.ts.map +1 -1
- package/dist/routes/access.js +6 -0
- package/dist/routes/access.js.map +1 -1
- package/dist/routes/agents.management-routes.d.ts.map +1 -1
- package/dist/routes/agents.management-routes.js +22 -0
- package/dist/routes/agents.management-routes.js.map +1 -1
- package/dist/routes/ai-search.d.ts +3 -0
- package/dist/routes/ai-search.d.ts.map +1 -0
- package/dist/routes/ai-search.js +17 -0
- package/dist/routes/ai-search.js.map +1 -0
- package/dist/routes/app-builder.d.ts +3 -0
- package/dist/routes/app-builder.d.ts.map +1 -0
- package/dist/routes/app-builder.js +305 -0
- package/dist/routes/app-builder.js.map +1 -0
- package/dist/routes/browser.d.ts.map +1 -1
- package/dist/routes/browser.js +6 -2
- package/dist/routes/browser.js.map +1 -1
- package/dist/routes/chats.annotation-routes.d.ts.map +1 -1
- package/dist/routes/chats.annotation-routes.js +10 -1
- package/dist/routes/chats.annotation-routes.js.map +1 -1
- package/dist/routes/chats.d.ts.map +1 -1
- package/dist/routes/chats.js +1 -0
- package/dist/routes/chats.js.map +1 -1
- package/dist/routes/chats.runtime-controls.d.ts +1 -0
- package/dist/routes/chats.runtime-controls.d.ts.map +1 -1
- package/dist/routes/chats.stream-routes.d.ts.map +1 -1
- package/dist/routes/chats.stream-routes.js +52 -4
- package/dist/routes/chats.stream-routes.js.map +1 -1
- package/dist/routes/chats.stream-support.d.ts +1 -0
- package/dist/routes/chats.stream-support.d.ts.map +1 -1
- package/dist/routes/chats.stream-support.js +8 -0
- package/dist/routes/chats.stream-support.js.map +1 -1
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +1 -0
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/issues.mutations.d.ts +1 -1
- package/dist/routes/issues.mutations.d.ts.map +1 -1
- package/dist/routes/issues.mutations.js +71 -7
- package/dist/routes/issues.mutations.js.map +1 -1
- package/dist/routes/onboarding.d.ts.map +1 -1
- package/dist/routes/onboarding.js +35 -32
- package/dist/routes/onboarding.js.map +1 -1
- package/dist/routes/orgs.js +1 -1
- package/dist/routes/orgs.js.map +1 -1
- package/dist/routes/product-analytics-collector.d.ts +10 -0
- package/dist/routes/product-analytics-collector.d.ts.map +1 -0
- package/dist/routes/product-analytics-collector.js +28 -0
- package/dist/routes/product-analytics-collector.js.map +1 -0
- package/dist/routes/product-analytics.d.ts +3 -0
- package/dist/routes/product-analytics.d.ts.map +1 -0
- package/dist/routes/product-analytics.js +169 -0
- package/dist/routes/product-analytics.js.map +1 -0
- package/dist/routes/website-metadata.d.ts +2 -2
- package/dist/routes/website-metadata.d.ts.map +1 -1
- package/dist/routes/website-metadata.js +15 -2
- package/dist/routes/website-metadata.js.map +1 -1
- package/dist/services/activity.d.ts +3 -1
- package/dist/services/activity.d.ts.map +1 -1
- package/dist/services/activity.js +55 -6
- package/dist/services/activity.js.map +1 -1
- package/dist/services/agent-run-context.d.ts.map +1 -1
- package/dist/services/agent-run-context.js +8 -2
- package/dist/services/agent-run-context.js.map +1 -1
- package/dist/services/agents.d.ts +84 -13
- package/dist/services/agents.d.ts.map +1 -1
- package/dist/services/agents.js +35 -1
- package/dist/services/agents.js.map +1 -1
- package/dist/services/ai-search.d.ts +6 -0
- package/dist/services/ai-search.d.ts.map +1 -0
- package/dist/services/ai-search.js +238 -0
- package/dist/services/ai-search.js.map +1 -0
- package/dist/services/ai-search.test.d.ts +2 -0
- package/dist/services/ai-search.test.d.ts.map +1 -0
- package/dist/services/ai-search.test.js +52 -0
- package/dist/services/ai-search.test.js.map +1 -0
- package/dist/services/app-builder.d.ts +13 -0
- package/dist/services/app-builder.d.ts.map +1 -0
- package/dist/services/app-builder.js +241 -0
- package/dist/services/app-builder.js.map +1 -0
- package/dist/services/approvals.d.ts +3 -3
- package/dist/services/automations.d.ts +1 -0
- package/dist/services/automations.d.ts.map +1 -1
- package/dist/services/automations.js +33 -1
- package/dist/services/automations.js.map +1 -1
- package/dist/services/browser-broker.d.ts +1 -0
- package/dist/services/browser-broker.d.ts.map +1 -1
- package/dist/services/browser-broker.js +34 -2
- package/dist/services/browser-broker.js.map +1 -1
- package/dist/services/browser-broker.test.js +73 -0
- package/dist/services/browser-broker.test.js.map +1 -1
- package/dist/services/budgets.d.ts +1 -6
- package/dist/services/budgets.d.ts.map +1 -1
- package/dist/services/budgets.js +30 -103
- package/dist/services/budgets.js.map +1 -1
- package/dist/services/chat-annotation-copy-lineage.d.ts.map +1 -1
- package/dist/services/chat-annotation-copy-lineage.js +6 -1
- package/dist/services/chat-annotation-copy-lineage.js.map +1 -1
- package/dist/services/chat-assistant.annotations.d.ts.map +1 -1
- package/dist/services/chat-assistant.annotations.js +4 -1
- package/dist/services/chat-assistant.annotations.js.map +1 -1
- package/dist/services/chat-assistant.annotations.test.js +18 -2
- package/dist/services/chat-assistant.annotations.test.js.map +1 -1
- package/dist/services/chat-family-groups.d.ts +1 -1
- package/dist/services/chat-family-groups.d.ts.map +1 -1
- package/dist/services/chat-family-groups.js +17 -1
- package/dist/services/chat-family-groups.js.map +1 -1
- package/dist/services/chat-inline-annotation-validation.d.ts.map +1 -1
- package/dist/services/chat-inline-annotation-validation.js +301 -8
- package/dist/services/chat-inline-annotation-validation.js.map +1 -1
- package/dist/services/chat-inline-annotations.test.js +249 -1
- package/dist/services/chat-inline-annotations.test.js.map +1 -1
- package/dist/services/chat-message-branch.d.ts +5 -0
- package/dist/services/chat-message-branch.d.ts.map +1 -0
- package/dist/services/chat-message-branch.js +9 -0
- package/dist/services/chat-message-branch.js.map +1 -0
- package/dist/services/chat-work-manifest.d.ts.map +1 -1
- package/dist/services/chat-work-manifest.js +6 -0
- package/dist/services/chat-work-manifest.js.map +1 -1
- package/dist/services/chats.create.d.ts +2 -0
- package/dist/services/chats.create.d.ts.map +1 -1
- package/dist/services/chats.create.js +76 -4
- package/dist/services/chats.create.js.map +1 -1
- package/dist/services/chats.d.ts +33 -13
- package/dist/services/chats.d.ts.map +1 -1
- package/dist/services/chats.inline-visual-persistence.d.ts.map +1 -1
- package/dist/services/chats.inline-visual-persistence.js +7 -0
- package/dist/services/chats.inline-visual-persistence.js.map +1 -1
- package/dist/services/chats.js +46 -24
- package/dist/services/chats.js.map +1 -1
- package/dist/services/dashboard.d.ts +0 -1
- package/dist/services/dashboard.d.ts.map +1 -1
- package/dist/services/dashboard.js +0 -1
- package/dist/services/dashboard.js.map +1 -1
- package/dist/services/heartbeat-run-summary.d.ts.map +1 -1
- package/dist/services/heartbeat-run-summary.js +6 -0
- package/dist/services/heartbeat-run-summary.js.map +1 -1
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/instance-settings.d.ts.map +1 -1
- package/dist/services/instance-settings.js +4 -6
- package/dist/services/instance-settings.js.map +1 -1
- package/dist/services/issue-approvals.d.ts +1 -1
- package/dist/services/issues.d.ts +2 -2
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +69 -2
- package/dist/services/issues.js.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.import.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-portability.import.js +1 -0
- package/dist/services/knowledge-portability/organization-portability.import.js.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.js +6 -2
- package/dist/services/knowledge-portability/organization-skills.js.map +1 -1
- package/dist/services/legacy-operator-state.d.ts +14 -0
- package/dist/services/legacy-operator-state.d.ts.map +1 -0
- package/dist/services/legacy-operator-state.js +251 -0
- package/dist/services/legacy-operator-state.js.map +1 -0
- package/dist/services/local-account-auth.d.ts.map +1 -1
- package/dist/services/local-account-auth.js +21 -3
- package/dist/services/local-account-auth.js.map +1 -1
- package/dist/services/messenger-custom-groups.d.ts +15 -0
- package/dist/services/messenger-custom-groups.d.ts.map +1 -0
- package/dist/services/messenger-custom-groups.js +155 -0
- package/dist/services/messenger-custom-groups.js.map +1 -0
- package/dist/services/messenger-saved-views.d.ts +32 -8
- package/dist/services/messenger-saved-views.d.ts.map +1 -1
- package/dist/services/messenger-saved-views.js +67 -2
- package/dist/services/messenger-saved-views.js.map +1 -1
- package/dist/services/messenger.d.ts +3 -3
- package/dist/services/messenger.d.ts.map +1 -1
- package/dist/services/messenger.js +110 -166
- package/dist/services/messenger.js.map +1 -1
- package/dist/services/orgs.d.ts +11 -2
- package/dist/services/orgs.d.ts.map +1 -1
- package/dist/services/orgs.js +23 -1
- package/dist/services/orgs.js.map +1 -1
- package/dist/services/product-analytics-collector.d.ts +86 -0
- package/dist/services/product-analytics-collector.d.ts.map +1 -0
- package/dist/services/product-analytics-collector.js +172 -0
- package/dist/services/product-analytics-collector.js.map +1 -0
- package/dist/services/product-analytics-collector.test.d.ts +2 -0
- package/dist/services/product-analytics-collector.test.d.ts.map +1 -0
- package/dist/services/product-analytics-collector.test.js +81 -0
- package/dist/services/product-analytics-collector.test.js.map +1 -0
- package/dist/services/product-analytics.d.ts +385 -0
- package/dist/services/product-analytics.d.ts.map +1 -0
- package/dist/services/product-analytics.js +698 -0
- package/dist/services/product-analytics.js.map +1 -0
- package/dist/services/product-analytics.test.d.ts +2 -0
- package/dist/services/product-analytics.test.d.ts.map +1 -0
- package/dist/services/product-analytics.test.js +136 -0
- package/dist/services/product-analytics.test.js.map +1 -0
- package/dist/services/projects.d.ts +1 -1
- package/dist/services/runtime-kernel/heartbeat.d.ts +4 -0
- package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.js +53 -1
- package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.misc.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.misc.js +5 -10
- package/dist/services/runtime-kernel/heartbeat.misc.js.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.terminal.d.ts +3 -0
- package/dist/services/runtime-kernel/heartbeat.terminal.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.terminal.js +56 -1
- package/dist/services/runtime-kernel/heartbeat.terminal.js.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.wakeup.d.ts.map +1 -1
- package/dist/services/runtime-kernel/heartbeat.wakeup.js +9 -2
- package/dist/services/runtime-kernel/heartbeat.wakeup.js.map +1 -1
- package/dist/services/secrets.d.ts +1 -1
- package/dist/services/side-chats.d.ts +3 -0
- package/dist/services/side-chats.d.ts.map +1 -1
- package/dist/services/side-chats.js +30 -15
- package/dist/services/side-chats.js.map +1 -1
- package/dist/services/website-metadata.d.ts +3 -0
- package/dist/services/website-metadata.d.ts.map +1 -1
- package/dist/services/website-metadata.js +331 -88
- package/dist/services/website-metadata.js.map +1 -1
- package/package.json +16 -14
- package/resources/bundled-skills/app-builder/SKILL.md +104 -0
- package/resources/bundled-skills/app-builder/agents/openai.yaml +14 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/.env.example +3 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/app/api/%5F%5Frudder/health/route.ts +20 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/app/api/contacts/[id]/route.ts +39 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/app/api/contacts/route.ts +31 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/app/api/data/export/route.ts +19 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/app/api/data/import/route.ts +40 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/app/globals.css +32 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/app/layout.tsx +15 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/app/page.tsx +9 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/components/contacts-workspace.tsx +292 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/components/ui/button.tsx +42 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/components/ui/card.tsx +19 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/components/ui/input.tsx +17 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/components/ui/label.tsx +6 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/data/.gitkeep +1 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/drizzle.config.ts +18 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/instrumentation.ts +5 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/lib/data-transfer.ts +44 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/lib/db/client.ts +78 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/lib/db/schema.ts +35 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/lib/domain.ts +18 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/lib/jobs/runner.ts +56 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/lib/utils.ts +6 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/migrations/0000_app_builder_foundation.sql +27 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/migrations/meta/_journal.json +13 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/next-env.d.ts +6 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/next.config.ts +8 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/package.json +47 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/playwright.config.ts +30 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/pnpm-lock.yaml +2687 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/postcss.config.mjs +5 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/rudder.app.json +32 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/scripts/migrate.ts +21 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/scripts/seed.ts +31 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/scripts/snapshot.ts +17 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/tests/e2e/app.spec.ts +50 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/tests/unit/data-transfer.test.ts +28 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/tests/unit/domain.test.ts +23 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/tsconfig.json +41 -0
- package/resources/bundled-skills/app-builder/assets/scaffold/vitest.config.ts +14 -0
- package/resources/bundled-skills/app-builder/evals/evals.json +65 -0
- package/resources/bundled-skills/app-builder/references/data-safety.md +38 -0
- package/resources/bundled-skills/app-builder/references/design-guidelines.md +20 -0
- package/resources/bundled-skills/app-builder/references/migrations-and-promotion.md +36 -0
- package/resources/bundled-skills/app-builder/references/scaffold-contract.md +77 -0
- package/resources/bundled-skills/app-builder/references/verification.md +23 -0
- package/resources/bundled-skills/app-builder/scripts/scaffold.mjs +58 -0
- package/resources/bundled-skills/app-builder/scripts/validate-manifest.mjs +63 -0
- package/skills/app-builder/SKILL.md +104 -0
- package/skills/app-builder/agents/openai.yaml +14 -0
- package/skills/app-builder/assets/scaffold/.env.example +3 -0
- package/skills/app-builder/assets/scaffold/app/api/%5F%5Frudder/health/route.ts +20 -0
- package/skills/app-builder/assets/scaffold/app/api/contacts/[id]/route.ts +39 -0
- package/skills/app-builder/assets/scaffold/app/api/contacts/route.ts +31 -0
- package/skills/app-builder/assets/scaffold/app/api/data/export/route.ts +19 -0
- package/skills/app-builder/assets/scaffold/app/api/data/import/route.ts +40 -0
- package/skills/app-builder/assets/scaffold/app/globals.css +32 -0
- package/skills/app-builder/assets/scaffold/app/layout.tsx +15 -0
- package/skills/app-builder/assets/scaffold/app/page.tsx +9 -0
- package/skills/app-builder/assets/scaffold/components/contacts-workspace.tsx +292 -0
- package/skills/app-builder/assets/scaffold/components/ui/button.tsx +42 -0
- package/skills/app-builder/assets/scaffold/components/ui/card.tsx +19 -0
- package/skills/app-builder/assets/scaffold/components/ui/input.tsx +17 -0
- package/skills/app-builder/assets/scaffold/components/ui/label.tsx +6 -0
- package/skills/app-builder/assets/scaffold/data/.gitkeep +1 -0
- package/skills/app-builder/assets/scaffold/drizzle.config.ts +18 -0
- package/skills/app-builder/assets/scaffold/instrumentation.ts +5 -0
- package/skills/app-builder/assets/scaffold/lib/data-transfer.ts +44 -0
- package/skills/app-builder/assets/scaffold/lib/db/client.ts +78 -0
- package/skills/app-builder/assets/scaffold/lib/db/schema.ts +35 -0
- package/skills/app-builder/assets/scaffold/lib/domain.ts +18 -0
- package/skills/app-builder/assets/scaffold/lib/jobs/runner.ts +56 -0
- package/skills/app-builder/assets/scaffold/lib/utils.ts +6 -0
- package/skills/app-builder/assets/scaffold/migrations/0000_app_builder_foundation.sql +27 -0
- package/skills/app-builder/assets/scaffold/migrations/meta/_journal.json +13 -0
- package/skills/app-builder/assets/scaffold/next-env.d.ts +6 -0
- package/skills/app-builder/assets/scaffold/next.config.ts +8 -0
- package/skills/app-builder/assets/scaffold/package.json +47 -0
- package/skills/app-builder/assets/scaffold/playwright.config.ts +30 -0
- package/skills/app-builder/assets/scaffold/pnpm-lock.yaml +2687 -0
- package/skills/app-builder/assets/scaffold/postcss.config.mjs +5 -0
- package/skills/app-builder/assets/scaffold/rudder.app.json +32 -0
- package/skills/app-builder/assets/scaffold/scripts/migrate.ts +21 -0
- package/skills/app-builder/assets/scaffold/scripts/seed.ts +31 -0
- package/skills/app-builder/assets/scaffold/scripts/snapshot.ts +17 -0
- package/skills/app-builder/assets/scaffold/tests/e2e/app.spec.ts +50 -0
- package/skills/app-builder/assets/scaffold/tests/unit/data-transfer.test.ts +28 -0
- package/skills/app-builder/assets/scaffold/tests/unit/domain.test.ts +23 -0
- package/skills/app-builder/assets/scaffold/tsconfig.json +41 -0
- package/skills/app-builder/assets/scaffold/vitest.config.ts +14 -0
- package/skills/app-builder/evals/evals.json +65 -0
- package/skills/app-builder/references/data-safety.md +38 -0
- package/skills/app-builder/references/design-guidelines.md +20 -0
- package/skills/app-builder/references/migrations-and-promotion.md +36 -0
- package/skills/app-builder/references/scaffold-contract.md +77 -0
- package/skills/app-builder/references/verification.md +23 -0
- package/skills/app-builder/scripts/scaffold.mjs +58 -0
- package/skills/app-builder/scripts/validate-manifest.mjs +63 -0
- package/ui-dist/assets/LibraryLiveSurface-DeUIV5N_.js +1 -0
- package/ui-dist/assets/{abnfDiagram-VRR7QNED-BdM6grBK.js → abnfDiagram-VRR7QNED-BaoLM9Pd.js} +1 -1
- package/ui-dist/assets/{arc-B8Tukeix.js → arc-Cx0L6o2z.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-DP_lYJIS.js → architectureDiagram-ZJ3FMSHR-zVfZP7v4.js} +1 -1
- package/ui-dist/assets/{blockDiagram-677ZJIJ3-D_QKZNjP.js → blockDiagram-677ZJIJ3-BiWWFP9R.js} +1 -1
- package/ui-dist/assets/{c4Diagram-LMCZKHZV-DJwVavN4.js → c4Diagram-LMCZKHZV-CX65GSJ6.js} +1 -1
- package/ui-dist/assets/channel-CHc3ibOz.js +1 -0
- package/ui-dist/assets/{chunk-2Q5K7J3B-BjdKg2cx.js → chunk-2Q5K7J3B-CH6PlelX.js} +1 -1
- package/ui-dist/assets/{chunk-32BRIVSS-C_u4l725.js → chunk-32BRIVSS-C7ZEXYO8.js} +1 -1
- package/ui-dist/assets/{chunk-5VM5RSS4-DtzEQCc9.js → chunk-5VM5RSS4-CyInWuue.js} +1 -1
- package/ui-dist/assets/{chunk-EX3LRPZG-Ds_gCBVz.js → chunk-EX3LRPZG-D58qFIdM.js} +1 -1
- package/ui-dist/assets/{chunk-JWPE2WC7-CHX9VuQf.js → chunk-JWPE2WC7-CJ3Qx30f.js} +1 -1
- package/ui-dist/assets/{chunk-MOJQB5TN-D_pj9OWe.js → chunk-MOJQB5TN-BO4bKVLH.js} +1 -1
- package/ui-dist/assets/{chunk-RYQCIY6F-aMYmnUaJ.js → chunk-RYQCIY6F-DxBmxcYc.js} +1 -1
- package/ui-dist/assets/{chunk-V7JOEXUC-Db3LrL-P.js → chunk-V7JOEXUC-Chdnd6Lq.js} +1 -1
- package/ui-dist/assets/{chunk-VR4S4FIN-BssuUpMr.js → chunk-VR4S4FIN-GkW_x_pD.js} +1 -1
- package/ui-dist/assets/{chunk-XXDRQBXY-DVzY-_Tv.js → chunk-XXDRQBXY-BJyBGcSF.js} +1 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-9iLZpz3t.js +1 -0
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-9iLZpz3t.js +1 -0
- package/ui-dist/assets/{cose-bilkent-JH36ORCC-Ch-fM2pW.js → cose-bilkent-JH36ORCC-BWP2r8TX.js} +1 -1
- package/ui-dist/assets/{cynefin-VYW2F7L2-C7gJeesr.js → cynefin-VYW2F7L2-C08dfI3w.js} +1 -1
- package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-BwMpD6aZ.js → cynefinDiagram-TSTJHNR4-h6dXjTaO.js} +1 -1
- package/ui-dist/assets/{dagre-VKFMJZFB-8TgHu2tj.js → dagre-VKFMJZFB-CWDgoFQD.js} +1 -1
- package/ui-dist/assets/{diagram-FQU43EPY-ChODej8C.js → diagram-FQU43EPY-CK1j48d-.js} +1 -1
- package/ui-dist/assets/{diagram-G47NLZAW-CVq1GXvV.js → diagram-G47NLZAW-CoSiG57q.js} +1 -1
- package/ui-dist/assets/{diagram-NH7WQ7WH-D1IO7xe3.js → diagram-NH7WQ7WH-DRG-xXGQ.js} +1 -1
- package/ui-dist/assets/{diagram-OA4YK3LP-CKlttJto.js → diagram-OA4YK3LP-D04mMwYs.js} +1 -1
- package/ui-dist/assets/{diagram-WEI45ONY-DigjsORo.js → diagram-WEI45ONY-ZkC1ZQMq.js} +1 -1
- package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-CZwO7RGO.js → ebnfDiagram-CCIWWBDH-BxMIw9PZ.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q63AITRT-C3fFVPod.js → erDiagram-Q63AITRT-B4Zwbn84.js} +1 -1
- package/ui-dist/assets/{flowDiagram-23GEKE2U-CpccWfFo.js → flowDiagram-23GEKE2U-D5pYz8Px.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-NO4QXBWP-g3KL--4W.js → ganttDiagram-NO4QXBWP-RSramodc.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-Bn59AW1B.js → gitGraphDiagram-IHSO6WYX-BFQfCkMd.js} +1 -1
- package/ui-dist/assets/{graph-LZSMW9ku.js → graph-DuSS8poS.js} +1 -1
- package/ui-dist/assets/{index-CPIoQKcG.js → index-5tzBDABj.js} +1 -1
- package/ui-dist/assets/index-9U78M9Np.js +1 -0
- package/ui-dist/assets/index-B-NP8fOI.js +1 -0
- package/ui-dist/assets/index-BADa8mxG.js +1 -0
- package/ui-dist/assets/index-BGcgy63z.js +1 -0
- package/ui-dist/assets/index-BKXUkeeU.js +1 -0
- package/ui-dist/assets/{index-CrU5g0XU.js → index-BU0KLNTt.js} +588 -558
- package/ui-dist/assets/index-BpvytaKr.js +1 -0
- package/ui-dist/assets/index-CHbXMgL0.js +7 -0
- package/ui-dist/assets/index-Cyq94-QT.js +1 -0
- package/ui-dist/assets/{index-BI9d3zXw.js → index-DRbt_Svi.js} +2 -2
- package/ui-dist/assets/index-De3vaw_P.js +1 -0
- package/ui-dist/assets/index-DzdRgH-y.js +1 -0
- package/ui-dist/assets/index-QrRs-r-4.css +1 -0
- package/ui-dist/assets/index-olsHIcXs.js +1 -0
- package/ui-dist/assets/index-wZlfBtwh.js +1 -0
- package/ui-dist/assets/{infoDiagram-FWYZ7A6U-uxPSmmcv.js → infoDiagram-FWYZ7A6U-f5i4pO3J.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-BeKQmaaL.js → ishikawaDiagram-FXEZZL3T-CVEdv5i_.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-5HDEW3XC-Cnqh1Fyp.js → journeyDiagram-5HDEW3XC-Cvp7vWcy.js} +1 -1
- package/ui-dist/assets/{kanban-definition-HUTT4EX6-DRvoG8e7.js → kanban-definition-HUTT4EX6-C6dURjjK.js} +1 -1
- package/ui-dist/assets/{layout-By7bOuHZ.js → layout-DxdOu9MY.js} +1 -1
- package/ui-dist/assets/{linear-D-kfwm1O.js → linear-CRi8RB_7.js} +1 -1
- package/ui-dist/assets/{map-d3zaWROJ.js → map-BlpDrw_l.js} +1 -1
- package/ui-dist/assets/{mermaid.core-1JtOXpMA.js → mermaid.core-2Gz9YusH.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-LN4V7U3C-D8Jge-_m.js → mindmap-definition-LN4V7U3C-BmSjJLTI.js} +1 -1
- package/ui-dist/assets/{pegDiagram-2B236MQR-UCd9-Gfd.js → pegDiagram-2B236MQR-BTUboUw8.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ENE6RG2P-DffPYn-P.js → pieDiagram-ENE6RG2P-CVPP8SYv.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-Ds1lQ25G.js → quadrantDiagram-ABIIQ3AL-BG4FmirT.js} +1 -1
- package/ui-dist/assets/{railroadDiagram-RFXS5EU6-P_pLStbY.js → railroadDiagram-RFXS5EU6-DFRNo9hF.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-TGXJPOKE-E_1U7w_T.js → requirementDiagram-TGXJPOKE-DpnVF9qP.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-DFgwdZz7.js → sankeyDiagram-HTMAVEWB-D1ETFdSE.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-CbI2EnTH.js → sequenceDiagram-DBY2YBRQ-CNqzB_AL.js} +1 -1
- package/ui-dist/assets/{sizeCapture-X5ZJPWSS-Axl0diWc.js → sizeCapture-X5ZJPWSS-DuoMCjdH.js} +1 -1
- package/ui-dist/assets/{stateDiagram-2N3HPSRC-DxZLENhX.js → stateDiagram-2N3HPSRC-fmHeKL8l.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DajAVikh.js +1 -0
- package/ui-dist/assets/swimlanes-5IMT3BWC-hjosaIhT.js +2 -0
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-DJjG9OJ-.js +8 -0
- package/ui-dist/assets/{timeline-definition-FHXFAJF6-B0Ha5bZS.js → timeline-definition-FHXFAJF6-xoMEClc-.js} +1 -1
- package/ui-dist/assets/{vennDiagram-L72KCM5P-BskD_1N5.js → vennDiagram-L72KCM5P-sdSXQfX-.js} +1 -1
- package/ui-dist/assets/{wardleyDiagram-EHGQE667-1wE4bYQ8.js → wardleyDiagram-EHGQE667-BbQBSKlD.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-FW5EYKEG-DfcOAfb8.js → xychartDiagram-FW5EYKEG-BfAtadyK.js} +1 -1
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/LibraryLiveSurface-CUgoFYvG.js +0 -1
- package/ui-dist/assets/channel-Cd4sqN24.js +0 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-Bi3tsM-q.js +0 -1
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-Bi3tsM-q.js +0 -1
- package/ui-dist/assets/index-BG46qMXw.js +0 -1
- package/ui-dist/assets/index-BLUJslyp.js +0 -1
- package/ui-dist/assets/index-BQv2nBgV.js +0 -1
- package/ui-dist/assets/index-BkbdH2n-.js +0 -1
- package/ui-dist/assets/index-BqhkUdAT.js +0 -1
- package/ui-dist/assets/index-C-qWhcfC.css +0 -1
- package/ui-dist/assets/index-C3e6slEn.js +0 -1
- package/ui-dist/assets/index-C5hYxLM9.js +0 -1
- package/ui-dist/assets/index-CLETiubs.js +0 -7
- package/ui-dist/assets/index-CLnCobCG.js +0 -1
- package/ui-dist/assets/index-CQLLvEua.js +0 -1
- package/ui-dist/assets/index-CVwGLbgm.js +0 -1
- package/ui-dist/assets/index-ChTD9DV4.js +0 -1
- package/ui-dist/assets/index-CiyYjAsy.js +0 -1
- package/ui-dist/assets/index-EqVfudOJ.js +0 -1
- package/ui-dist/assets/index-ZFb9ZHAH.js +0 -6
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-KzYOpJ1T.js +0 -1
- package/ui-dist/assets/swimlanes-5IMT3BWC-1G26qvUm.js +0 -2
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV--Vi3kUvi.js +0 -8
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { productAnalyticsConsentLedger, productAnalyticsEvents, productAnalyticsInstallations, productAnalyticsOutbox, productAnalyticsWorkCycles, } from "@rudderhq/db";
|
|
2
|
+
import { and, asc, desc, eq, gte, inArray, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
3
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { forbidden, unauthorized, unprocessable } from "../errors.js";
|
|
5
|
+
export const PRODUCT_ANALYTICS_EVENT_NAMES = [
|
|
6
|
+
"organization_created",
|
|
7
|
+
"human_work_started",
|
|
8
|
+
"run_started",
|
|
9
|
+
"run_succeeded",
|
|
10
|
+
"run_failed",
|
|
11
|
+
"output_ready",
|
|
12
|
+
"review_decision_recorded",
|
|
13
|
+
"account_created",
|
|
14
|
+
"desktop_authorized",
|
|
15
|
+
"local_server_connected",
|
|
16
|
+
"first_agent_ready",
|
|
17
|
+
"work_loop_completed",
|
|
18
|
+
"work_loop_invalidated",
|
|
19
|
+
];
|
|
20
|
+
export const PRODUCT_ANALYTICS_PRODUCED_EVENT_NAMES = [
|
|
21
|
+
"organization_created",
|
|
22
|
+
"human_work_started",
|
|
23
|
+
"run_started",
|
|
24
|
+
"run_succeeded",
|
|
25
|
+
"run_failed",
|
|
26
|
+
"output_ready",
|
|
27
|
+
"first_agent_ready",
|
|
28
|
+
"work_loop_completed",
|
|
29
|
+
"work_loop_invalidated",
|
|
30
|
+
];
|
|
31
|
+
export const PRODUCT_ANALYTICS_DERIVED_EVENT_NAMES = ["review_decision_recorded"];
|
|
32
|
+
export const PRODUCT_ANALYTICS_DEFERRED_EVENT_NAMES = ["account_created", "desktop_authorized", "local_server_connected"];
|
|
33
|
+
export function productAnalyticsRunTerminalEventName(status) {
|
|
34
|
+
if (status === "succeeded")
|
|
35
|
+
return "run_succeeded";
|
|
36
|
+
if (status === "failed" || status === "cancelled" || status === "timed_out")
|
|
37
|
+
return "run_failed";
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const EVENT_NAME_SET = new Set(PRODUCT_ANALYTICS_EVENT_NAMES);
|
|
41
|
+
const CONFIDENCE_SET = new Set(["exact", "derived", "unknown"]);
|
|
42
|
+
const ACTOR_TYPE_SET = new Set(["human", "agent", "system", "automation"]);
|
|
43
|
+
// Unknown keys are rejected by the event-specific allowlist. Keep this scan
|
|
44
|
+
// focused on content-bearing names so safe dimensions such as creation_path do
|
|
45
|
+
// not get rejected by a substring match.
|
|
46
|
+
const SENSITIVE_PROPERTY_KEY = /(prompt|transcript|title|description|body|content|url|token|secret|password|credential|email|hostname|username)/i;
|
|
47
|
+
const EVENT_PROPERTY_ALLOWLIST = {
|
|
48
|
+
organization_created: new Set([
|
|
49
|
+
"creation_path",
|
|
50
|
+
"template_kind",
|
|
51
|
+
"is_first_organization",
|
|
52
|
+
"is_user_initiated",
|
|
53
|
+
]),
|
|
54
|
+
human_work_started: new Set(["work_surface", "origin"]),
|
|
55
|
+
run_started: new Set(["run_kind", "runtime", "attempt_kind"]),
|
|
56
|
+
run_succeeded: new Set(["run_kind", "runtime", "attempt_kind"]),
|
|
57
|
+
run_failed: new Set(["run_kind", "runtime", "attempt_kind", "terminal_status"]),
|
|
58
|
+
output_ready: new Set(["output_kind"]),
|
|
59
|
+
review_decision_recorded: new Set(["decision", "review_surface"]),
|
|
60
|
+
account_created: new Set(["auth_method", "is_migrated", "acquisition_source"]),
|
|
61
|
+
desktop_authorized: new Set(["client_kind", "release_channel", "coarse_platform", "authorization_kind"]),
|
|
62
|
+
local_server_connected: new Set(["connection_kind"]),
|
|
63
|
+
first_agent_ready: new Set(["runtime_adapter", "is_default_agent", "preflight_result_code", "is_first_for_installation"]),
|
|
64
|
+
work_loop_completed: new Set(["work_surface", "origin", "review_required", "attempt_count", "duration_bucket", "cost_bucket", "output_kind", "is_first_loop"]),
|
|
65
|
+
work_loop_invalidated: new Set(["reason_code", "duration_bucket"]),
|
|
66
|
+
};
|
|
67
|
+
async function resolveProductAnalyticsInstallationId(db, installationId) {
|
|
68
|
+
if (installationId)
|
|
69
|
+
return installationId;
|
|
70
|
+
if (typeof db.select !== "function")
|
|
71
|
+
return null;
|
|
72
|
+
const [registered] = await db.select({ installationId: productAnalyticsInstallations.installationId })
|
|
73
|
+
.from(productAnalyticsInstallations)
|
|
74
|
+
.orderBy(asc(productAnalyticsInstallations.createdAt))
|
|
75
|
+
.limit(1);
|
|
76
|
+
return registered?.installationId ?? null;
|
|
77
|
+
}
|
|
78
|
+
function assertBoundedText(value, field, maxLength) {
|
|
79
|
+
if (value.length === 0 || value.length > maxLength) {
|
|
80
|
+
throw unprocessable(`Product analytics ${field} is invalid`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function validateProperties(eventName, properties) {
|
|
84
|
+
const allowed = EVENT_PROPERTY_ALLOWLIST[eventName];
|
|
85
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
86
|
+
if (!allowed.has(key) || SENSITIVE_PROPERTY_KEY.test(key)) {
|
|
87
|
+
throw unprocessable(`Product analytics property is not allowed: ${key}`);
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "string")
|
|
90
|
+
assertBoundedText(value, `property ${key}`, 80);
|
|
91
|
+
if (typeof value === "number" && (!Number.isFinite(value) || Math.abs(value) > 1_000_000)) {
|
|
92
|
+
throw unprocessable(`Product analytics property is invalid: ${key}`);
|
|
93
|
+
}
|
|
94
|
+
if (typeof value === "object" && value !== null) {
|
|
95
|
+
throw unprocessable(`Product analytics property must be scalar: ${key}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return properties;
|
|
99
|
+
}
|
|
100
|
+
function validateEvent(input) {
|
|
101
|
+
if (!EVENT_NAME_SET.has(input.eventName)) {
|
|
102
|
+
throw unprocessable(`Unknown product analytics event: ${input.eventName}`);
|
|
103
|
+
}
|
|
104
|
+
if (input.schemaVersion !== undefined && input.schemaVersion !== 1) {
|
|
105
|
+
throw unprocessable("Unsupported product analytics schema version");
|
|
106
|
+
}
|
|
107
|
+
if (!CONFIDENCE_SET.has(input.confidence))
|
|
108
|
+
throw unprocessable("Product analytics confidence is invalid");
|
|
109
|
+
if (!ACTOR_TYPE_SET.has(input.actorType))
|
|
110
|
+
throw unprocessable("Product analytics actor type is invalid");
|
|
111
|
+
if (input.completionRevision !== undefined && input.completionRevision !== null && (!Number.isInteger(input.completionRevision) || input.completionRevision < 1)) {
|
|
112
|
+
throw unprocessable("Product analytics completion revision is invalid");
|
|
113
|
+
}
|
|
114
|
+
assertBoundedText(input.sourceTransition, "source transition", 120);
|
|
115
|
+
assertBoundedText(input.dedupeKey, "dedupe key", 256);
|
|
116
|
+
if (input.actorId !== undefined && input.actorId !== null)
|
|
117
|
+
assertBoundedText(input.actorId, "actor id", 200);
|
|
118
|
+
if (input.entityType !== undefined && input.entityType !== null)
|
|
119
|
+
assertBoundedText(input.entityType, "entity type", 80);
|
|
120
|
+
if (input.entityId !== undefined && input.entityId !== null)
|
|
121
|
+
assertBoundedText(input.entityId, "entity id", 200);
|
|
122
|
+
if (input.occurredAt && Number.isNaN(input.occurredAt.getTime())) {
|
|
123
|
+
throw unprocessable("Product analytics occurredAt is invalid");
|
|
124
|
+
}
|
|
125
|
+
validateProperties(input.eventName, input.properties ?? {});
|
|
126
|
+
}
|
|
127
|
+
export async function recordProductAnalyticsEvent(db, input) {
|
|
128
|
+
validateEvent(input);
|
|
129
|
+
const installationId = await resolveProductAnalyticsInstallationId(db, input.installationId);
|
|
130
|
+
const localUserId = input.localUserId ?? (input.actorType === "human" ? input.actorId ?? null : null);
|
|
131
|
+
const rows = await db
|
|
132
|
+
.insert(productAnalyticsEvents)
|
|
133
|
+
.values({
|
|
134
|
+
orgId: input.orgId,
|
|
135
|
+
installationId,
|
|
136
|
+
eventName: input.eventName,
|
|
137
|
+
schemaVersion: input.schemaVersion ?? 1,
|
|
138
|
+
occurredAt: input.occurredAt ?? new Date(),
|
|
139
|
+
sourceTransition: input.sourceTransition,
|
|
140
|
+
confidence: input.confidence,
|
|
141
|
+
actorType: input.actorType,
|
|
142
|
+
actorId: input.actorId ?? null,
|
|
143
|
+
localUserId,
|
|
144
|
+
environment: input.environment ?? (process.env.NODE_ENV === "production" ? "production" : "development"),
|
|
145
|
+
appVersion: input.appVersion ?? "unknown",
|
|
146
|
+
releaseChannel: input.releaseChannel ?? "stable",
|
|
147
|
+
deploymentMode: input.deploymentMode ?? "self_hosted",
|
|
148
|
+
origin: input.origin ?? (input.actorType === "human" ? "human" : input.actorType === "automation" ? "automation" : "system"),
|
|
149
|
+
workSurface: input.workSurface ?? null,
|
|
150
|
+
workId: input.workId ?? input.entityId ?? null,
|
|
151
|
+
workCycleId: input.workCycleId ?? null,
|
|
152
|
+
rootRunId: input.rootRunId ?? null,
|
|
153
|
+
runId: input.runId ?? null,
|
|
154
|
+
completionRevision: input.completionRevision ?? null,
|
|
155
|
+
entityType: input.entityType ?? null,
|
|
156
|
+
entityId: input.entityId ?? null,
|
|
157
|
+
dedupeKey: input.dedupeKey,
|
|
158
|
+
properties: input.properties ?? {},
|
|
159
|
+
isBackfill: input.isBackfill ?? false,
|
|
160
|
+
})
|
|
161
|
+
.onConflictDoNothing({ target: productAnalyticsEvents.dedupeKey })
|
|
162
|
+
.returning({ id: productAnalyticsEvents.id });
|
|
163
|
+
if (rows[0] && installationId) {
|
|
164
|
+
await enqueueProductAnalyticsEvent(db, {
|
|
165
|
+
eventId: rows[0].id,
|
|
166
|
+
installationId,
|
|
167
|
+
localUserId,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (rows[0] && input.workCycleId && typeof db.update === "function") {
|
|
171
|
+
if (input.eventName === "output_ready") {
|
|
172
|
+
await db.update(productAnalyticsWorkCycles).set({ outputReadyAt: input.occurredAt ?? new Date(), updatedAt: new Date() })
|
|
173
|
+
.where(and(eq(productAnalyticsWorkCycles.orgId, input.orgId), eq(productAnalyticsWorkCycles.workCycleId, input.workCycleId)));
|
|
174
|
+
}
|
|
175
|
+
if (input.eventName === "review_decision_recorded") {
|
|
176
|
+
const decision = input.properties?.decision;
|
|
177
|
+
await db.update(productAnalyticsWorkCycles).set({ reviewDecision: typeof decision === "string" ? decision : null, updatedAt: new Date() })
|
|
178
|
+
.where(and(eq(productAnalyticsWorkCycles.orgId, input.orgId), eq(productAnalyticsWorkCycles.workCycleId, input.workCycleId)));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return rows[0] ?? null;
|
|
182
|
+
}
|
|
183
|
+
export async function enqueueProductAnalyticsEvent(db, input) {
|
|
184
|
+
const [installation] = await db.select({ mode: productAnalyticsInstallations.mode }).from(productAnalyticsInstallations)
|
|
185
|
+
.where(eq(productAnalyticsInstallations.installationId, input.installationId)).limit(1);
|
|
186
|
+
if (!installation || installation.mode === "off")
|
|
187
|
+
return null;
|
|
188
|
+
const scope = installation.mode === "account_linked" ? "account_linked_user" : "anonymous_installation";
|
|
189
|
+
const conditions = [eq(productAnalyticsConsentLedger.installationId, input.installationId), eq(productAnalyticsConsentLedger.scope, scope), eq(productAnalyticsConsentLedger.decision, "granted")];
|
|
190
|
+
conditions.push(scope === "account_linked_user" && input.localUserId ? eq(productAnalyticsConsentLedger.localUserId, input.localUserId) : isNull(productAnalyticsConsentLedger.localUserId));
|
|
191
|
+
const [consent] = await db.select().from(productAnalyticsConsentLedger).where(and(...conditions)).orderBy(desc(productAnalyticsConsentLedger.consentEpoch)).limit(1);
|
|
192
|
+
if (!consent)
|
|
193
|
+
return null;
|
|
194
|
+
const [row] = await db.insert(productAnalyticsOutbox).values({
|
|
195
|
+
eventId: input.eventId,
|
|
196
|
+
installationId: input.installationId,
|
|
197
|
+
deliveryMode: installation.mode,
|
|
198
|
+
consentScope: scope,
|
|
199
|
+
consentedLocalUserId: consent.localUserId ?? null,
|
|
200
|
+
consentVersion: consent.policyVersion,
|
|
201
|
+
consentEpoch: consent.consentEpoch,
|
|
202
|
+
}).onConflictDoNothing().returning();
|
|
203
|
+
return row ?? null;
|
|
204
|
+
}
|
|
205
|
+
export async function ensureProductAnalyticsWorkCycle(db, input) {
|
|
206
|
+
const [row] = await db
|
|
207
|
+
.insert(productAnalyticsWorkCycles)
|
|
208
|
+
.values({
|
|
209
|
+
orgId: input.orgId,
|
|
210
|
+
workSurface: input.workSurface,
|
|
211
|
+
workId: input.workId,
|
|
212
|
+
workCycleId: input.workCycleId,
|
|
213
|
+
actorId: input.actorId ?? null,
|
|
214
|
+
origin: input.origin ?? "human",
|
|
215
|
+
startedAt: input.startedAt ?? new Date(),
|
|
216
|
+
})
|
|
217
|
+
.onConflictDoNothing({ target: [productAnalyticsWorkCycles.orgId, productAnalyticsWorkCycles.workCycleId] })
|
|
218
|
+
.returning();
|
|
219
|
+
if (row)
|
|
220
|
+
return row;
|
|
221
|
+
const [existing] = await db
|
|
222
|
+
.select()
|
|
223
|
+
.from(productAnalyticsWorkCycles)
|
|
224
|
+
.where(and(eq(productAnalyticsWorkCycles.orgId, input.orgId), eq(productAnalyticsWorkCycles.workCycleId, input.workCycleId)))
|
|
225
|
+
.limit(1);
|
|
226
|
+
return existing ?? null;
|
|
227
|
+
}
|
|
228
|
+
export async function completeProductAnalyticsWorkCycle(db, input) {
|
|
229
|
+
return db.transaction(async (tx) => {
|
|
230
|
+
const cycle = await ensureProductAnalyticsWorkCycle(tx, input);
|
|
231
|
+
if (!cycle)
|
|
232
|
+
throw unprocessable("Product analytics work cycle is unavailable");
|
|
233
|
+
const [lockedCycle] = await tx.select().from(productAnalyticsWorkCycles)
|
|
234
|
+
.where(and(eq(productAnalyticsWorkCycles.orgId, input.orgId), eq(productAnalyticsWorkCycles.workCycleId, input.workCycleId)))
|
|
235
|
+
.for("update");
|
|
236
|
+
if (!lockedCycle)
|
|
237
|
+
throw unprocessable("Product analytics work cycle is unavailable");
|
|
238
|
+
const [successfulRuns] = await tx.select({ count: sql `count(*)` }).from(productAnalyticsEvents).where(and(eq(productAnalyticsEvents.orgId, input.orgId), eq(productAnalyticsEvents.workCycleId, input.workCycleId), eq(productAnalyticsEvents.eventName, "run_succeeded")));
|
|
239
|
+
const [readyOutputs] = await tx.select({ count: sql `count(*)` }).from(productAnalyticsEvents).where(and(eq(productAnalyticsEvents.orgId, input.orgId), eq(productAnalyticsEvents.workCycleId, input.workCycleId), eq(productAnalyticsEvents.eventName, "output_ready")));
|
|
240
|
+
const reviewRequired = input.properties?.review_required === true;
|
|
241
|
+
const [approvedReviews] = reviewRequired
|
|
242
|
+
? await tx.select({ count: sql `count(*)` }).from(productAnalyticsEvents).where(and(eq(productAnalyticsEvents.orgId, input.orgId), eq(productAnalyticsEvents.workCycleId, input.workCycleId), eq(productAnalyticsEvents.eventName, "review_decision_recorded"), sql `${productAnalyticsEvents.properties}->>'decision' = 'approve'`))
|
|
243
|
+
: [{ count: 1 }];
|
|
244
|
+
if (Number(successfulRuns?.count ?? 0) < 1 || Number(readyOutputs?.count ?? 0) < 1 || Number(approvedReviews?.count ?? 0) < 1) {
|
|
245
|
+
throw unprocessable("Product analytics work cycle does not satisfy completion gates");
|
|
246
|
+
}
|
|
247
|
+
if (lockedCycle.state === "completed" && lockedCycle.invalidatedAt === null) {
|
|
248
|
+
return { event: null, revision: lockedCycle.completionRevision };
|
|
249
|
+
}
|
|
250
|
+
const revision = (lockedCycle.completionRevision ?? 0) + 1;
|
|
251
|
+
const event = await recordProductAnalyticsEvent(tx, {
|
|
252
|
+
orgId: input.orgId,
|
|
253
|
+
eventName: "work_loop_completed",
|
|
254
|
+
occurredAt: new Date(),
|
|
255
|
+
sourceTransition: input.sourceTransition,
|
|
256
|
+
confidence: "exact",
|
|
257
|
+
actorType: input.actorType,
|
|
258
|
+
actorId: input.actorId ?? null,
|
|
259
|
+
entityType: input.workSurface,
|
|
260
|
+
entityId: input.workId,
|
|
261
|
+
workSurface: input.workSurface,
|
|
262
|
+
workId: input.workId,
|
|
263
|
+
workCycleId: input.workCycleId,
|
|
264
|
+
rootRunId: input.rootRunId ?? null,
|
|
265
|
+
completionRevision: revision,
|
|
266
|
+
origin: input.origin ?? "human",
|
|
267
|
+
dedupeKey: `work_loop_completed:${input.workCycleId}:${revision}`,
|
|
268
|
+
properties: input.properties ?? { work_surface: input.workSurface, origin: input.origin ?? "human", is_first_loop: revision === 1 },
|
|
269
|
+
});
|
|
270
|
+
await tx
|
|
271
|
+
.update(productAnalyticsWorkCycles)
|
|
272
|
+
.set({ state: "completed", completionRevision: revision, completedAt: new Date(), invalidatedAt: null, updatedAt: new Date() })
|
|
273
|
+
.where(and(eq(productAnalyticsWorkCycles.orgId, input.orgId), eq(productAnalyticsWorkCycles.workCycleId, input.workCycleId)));
|
|
274
|
+
return { event, revision };
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
export async function invalidateProductAnalyticsWorkCycle(db, input) {
|
|
278
|
+
return db.transaction(async (tx) => {
|
|
279
|
+
const [cycle] = await tx.select().from(productAnalyticsWorkCycles)
|
|
280
|
+
.where(and(eq(productAnalyticsWorkCycles.orgId, input.orgId), eq(productAnalyticsWorkCycles.workCycleId, input.workCycleId)))
|
|
281
|
+
.for("update");
|
|
282
|
+
if (!cycle || cycle.completionRevision < input.completionRevision)
|
|
283
|
+
return null;
|
|
284
|
+
const event = await recordProductAnalyticsEvent(tx, {
|
|
285
|
+
orgId: input.orgId,
|
|
286
|
+
eventName: "work_loop_invalidated",
|
|
287
|
+
occurredAt: input.occurredAt ?? new Date(),
|
|
288
|
+
sourceTransition: "work_loop.invalidate",
|
|
289
|
+
confidence: "exact",
|
|
290
|
+
actorType: "system",
|
|
291
|
+
entityType: input.workSurface,
|
|
292
|
+
entityId: input.workId,
|
|
293
|
+
workSurface: input.workSurface,
|
|
294
|
+
workId: input.workId,
|
|
295
|
+
workCycleId: input.workCycleId,
|
|
296
|
+
completionRevision: input.completionRevision,
|
|
297
|
+
dedupeKey: `work_loop_invalidated:${input.workCycleId}:${input.completionRevision}:${input.reasonCode}`,
|
|
298
|
+
properties: { reason_code: input.reasonCode },
|
|
299
|
+
});
|
|
300
|
+
await tx.update(productAnalyticsWorkCycles).set({ state: "invalidated", invalidatedAt: input.occurredAt ?? new Date(), updatedAt: new Date() })
|
|
301
|
+
.where(and(eq(productAnalyticsWorkCycles.orgId, input.orgId), eq(productAnalyticsWorkCycles.workCycleId, input.workCycleId)));
|
|
302
|
+
return event;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
export async function registerProductAnalyticsInstallation(db, input) {
|
|
306
|
+
assertBoundedText(input.installationId, "installation id", 200);
|
|
307
|
+
if (input.mode && input.mode !== "off") {
|
|
308
|
+
throw unprocessable("Product analytics mode requires an explicit consent ledger decision");
|
|
309
|
+
}
|
|
310
|
+
const secret = input.installationSecret ?? randomBytes(32).toString("hex");
|
|
311
|
+
const secretHash = createHash("sha256").update(secret).digest("hex");
|
|
312
|
+
const [row] = await db.insert(productAnalyticsInstallations).values({
|
|
313
|
+
installationId: input.installationId,
|
|
314
|
+
installationSecretHash: secretHash,
|
|
315
|
+
mode: input.mode ?? "off",
|
|
316
|
+
}).onConflictDoNothing({ target: productAnalyticsInstallations.installationId }).returning();
|
|
317
|
+
return { installation: row ?? null, installationSecret: row ? secret : null };
|
|
318
|
+
}
|
|
319
|
+
export async function assertProductAnalyticsInstallationSecret(db, installationId, installationSecret) {
|
|
320
|
+
assertBoundedText(installationSecret, "installation secret", 256);
|
|
321
|
+
const [installation] = await db.select({ secretHash: productAnalyticsInstallations.installationSecretHash })
|
|
322
|
+
.from(productAnalyticsInstallations)
|
|
323
|
+
.where(eq(productAnalyticsInstallations.installationId, installationId))
|
|
324
|
+
.limit(1);
|
|
325
|
+
if (!installation)
|
|
326
|
+
throw unauthorized("Product analytics installation is not registered");
|
|
327
|
+
const expected = Buffer.from(installation.secretHash, "utf8");
|
|
328
|
+
const actual = Buffer.from(createHash("sha256").update(installationSecret).digest("hex"), "utf8");
|
|
329
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
|
330
|
+
throw forbidden("Product analytics installation credential is invalid");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export async function setProductAnalyticsInstallationMode(db, installationId, mode) {
|
|
334
|
+
const [row] = await db.update(productAnalyticsInstallations).set({ mode, updatedAt: new Date() })
|
|
335
|
+
.where(eq(productAnalyticsInstallations.installationId, installationId)).returning();
|
|
336
|
+
return row ?? null;
|
|
337
|
+
}
|
|
338
|
+
export async function reconcileProductAnalyticsInstallationMode(db, installationId) {
|
|
339
|
+
const consent = await db.select({ scope: productAnalyticsConsentLedger.scope, decision: productAnalyticsConsentLedger.decision })
|
|
340
|
+
.from(productAnalyticsConsentLedger)
|
|
341
|
+
.where(eq(productAnalyticsConsentLedger.installationId, installationId))
|
|
342
|
+
.orderBy(desc(productAnalyticsConsentLedger.consentEpoch));
|
|
343
|
+
const latestByScope = new Map();
|
|
344
|
+
for (const row of consent) {
|
|
345
|
+
if (!latestByScope.has(row.scope))
|
|
346
|
+
latestByScope.set(row.scope, row.decision);
|
|
347
|
+
}
|
|
348
|
+
const mode = latestByScope.get("account_linked_user") === "granted"
|
|
349
|
+
? "account_linked"
|
|
350
|
+
: latestByScope.get("anonymous_installation") === "granted" ? "anonymous" : "off";
|
|
351
|
+
return setProductAnalyticsInstallationMode(db, installationId, mode);
|
|
352
|
+
}
|
|
353
|
+
export async function getProductAnalyticsInstallationState(db, installationId) {
|
|
354
|
+
const [installation] = await db.select().from(productAnalyticsInstallations)
|
|
355
|
+
.where(eq(productAnalyticsInstallations.installationId, installationId)).limit(1);
|
|
356
|
+
if (!installation)
|
|
357
|
+
return null;
|
|
358
|
+
const consent = await db.select().from(productAnalyticsConsentLedger)
|
|
359
|
+
.where(eq(productAnalyticsConsentLedger.installationId, installationId))
|
|
360
|
+
.orderBy(desc(productAnalyticsConsentLedger.decidedAt)).limit(20);
|
|
361
|
+
const [pending] = await db.select({ count: sql `count(*)` }).from(productAnalyticsOutbox)
|
|
362
|
+
.where(and(eq(productAnalyticsOutbox.installationId, installationId), inArray(productAnalyticsOutbox.state, ["pending", "claimed", "retry_wait", "failed_actionable"]), eq(productAnalyticsOutbox.deliveryMode, installation.mode)));
|
|
363
|
+
return { installation, consent, pendingCount: Number(pending?.count ?? 0) };
|
|
364
|
+
}
|
|
365
|
+
export async function recordProductAnalyticsConsent(db, input) {
|
|
366
|
+
return db.transaction(async (tx) => {
|
|
367
|
+
const [installation] = await tx.select({ installationId: productAnalyticsInstallations.installationId })
|
|
368
|
+
.from(productAnalyticsInstallations)
|
|
369
|
+
.where(eq(productAnalyticsInstallations.installationId, input.installationId))
|
|
370
|
+
.limit(1)
|
|
371
|
+
.for("update");
|
|
372
|
+
if (!installation)
|
|
373
|
+
throw unprocessable("Product analytics installation is not registered");
|
|
374
|
+
assertBoundedText(input.policyVersion, "consent policy version", 80);
|
|
375
|
+
if (input.localUserId)
|
|
376
|
+
assertBoundedText(input.localUserId, "consent local user id", 200);
|
|
377
|
+
const keyCondition = and(eq(productAnalyticsConsentLedger.installationId, input.installationId), eq(productAnalyticsConsentLedger.scope, input.scope), input.localUserId ? eq(productAnalyticsConsentLedger.localUserId, input.localUserId) : isNull(productAnalyticsConsentLedger.localUserId));
|
|
378
|
+
const [previous] = await tx.select({ consentEpoch: productAnalyticsConsentLedger.consentEpoch })
|
|
379
|
+
.from(productAnalyticsConsentLedger).where(keyCondition).orderBy(desc(productAnalyticsConsentLedger.consentEpoch)).limit(1).for("update");
|
|
380
|
+
const epoch = (previous?.consentEpoch ?? 0) + 1;
|
|
381
|
+
const [row] = await tx.insert(productAnalyticsConsentLedger).values({
|
|
382
|
+
installationId: input.installationId,
|
|
383
|
+
scope: input.scope,
|
|
384
|
+
localUserId: input.localUserId ?? null,
|
|
385
|
+
decision: input.decision,
|
|
386
|
+
policyVersion: input.policyVersion,
|
|
387
|
+
consentEpoch: epoch,
|
|
388
|
+
decidedByLocalUserId: input.decidedByLocalUserId ?? null,
|
|
389
|
+
uploadFromAt: input.uploadFromAt ?? (input.decision === "granted" ? new Date() : null),
|
|
390
|
+
}).returning();
|
|
391
|
+
if (input.decision === "revoked") {
|
|
392
|
+
await tx.delete(productAnalyticsOutbox).where(and(eq(productAnalyticsOutbox.installationId, input.installationId), eq(productAnalyticsOutbox.consentScope, input.scope), lt(productAnalyticsOutbox.consentEpoch, epoch), input.localUserId ? eq(productAnalyticsOutbox.consentedLocalUserId, input.localUserId) : isNull(productAnalyticsOutbox.consentedLocalUserId), sql `${productAnalyticsOutbox.state} <> 'delivered'`));
|
|
393
|
+
}
|
|
394
|
+
return row ?? null;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
export async function claimProductAnalyticsOutbox(db, input) {
|
|
398
|
+
if (!input.installationId)
|
|
399
|
+
throw unprocessable("Product analytics installation id is required for claim");
|
|
400
|
+
if (!input.deliveryMode)
|
|
401
|
+
throw unprocessable("Product analytics delivery mode is required for claim");
|
|
402
|
+
const deliveryMode = input.deliveryMode;
|
|
403
|
+
const now = new Date();
|
|
404
|
+
const leaseExpiresAt = new Date(now.getTime() + (input.leaseSeconds ?? 300) * 1000);
|
|
405
|
+
const token = randomUUID();
|
|
406
|
+
const limit = Math.min(Math.max(input.limit ?? 20, 1), 100);
|
|
407
|
+
return db.transaction(async (tx) => {
|
|
408
|
+
const conditions = [
|
|
409
|
+
or(eq(productAnalyticsOutbox.state, "pending"), and(eq(productAnalyticsOutbox.state, "claimed"), lt(productAnalyticsOutbox.leaseExpiresAt, now)), eq(productAnalyticsOutbox.state, "retry_wait")),
|
|
410
|
+
lte(productAnalyticsOutbox.nextAttemptAt, now),
|
|
411
|
+
];
|
|
412
|
+
conditions.push(eq(productAnalyticsOutbox.deliveryMode, deliveryMode));
|
|
413
|
+
conditions.push(eq(productAnalyticsOutbox.installationId, input.installationId));
|
|
414
|
+
if (input.consentedLocalUserId !== undefined)
|
|
415
|
+
conditions.push(input.consentedLocalUserId ? eq(productAnalyticsOutbox.consentedLocalUserId, input.consentedLocalUserId) : isNull(productAnalyticsOutbox.consentedLocalUserId));
|
|
416
|
+
const [first] = await tx.select({ eventId: productAnalyticsOutbox.eventId, consentEpoch: productAnalyticsOutbox.consentEpoch, consentedLocalUserId: productAnalyticsOutbox.consentedLocalUserId }).from(productAnalyticsOutbox).where(and(...conditions)).orderBy(productAnalyticsOutbox.nextAttemptAt).limit(1).for("update", { skipLocked: true });
|
|
417
|
+
if (!first)
|
|
418
|
+
return [];
|
|
419
|
+
conditions.push(eq(productAnalyticsOutbox.consentEpoch, first.consentEpoch));
|
|
420
|
+
conditions.push(first.consentedLocalUserId ? eq(productAnalyticsOutbox.consentedLocalUserId, first.consentedLocalUserId) : isNull(productAnalyticsOutbox.consentedLocalUserId));
|
|
421
|
+
const rows = await tx.select({ eventId: productAnalyticsOutbox.eventId }).from(productAnalyticsOutbox).where(and(...conditions)).orderBy(productAnalyticsOutbox.nextAttemptAt).limit(limit).for("update", { skipLocked: true });
|
|
422
|
+
if (rows.length === 0)
|
|
423
|
+
return [];
|
|
424
|
+
const scope = deliveryMode === "account_linked" ? "account_linked_user" : "anonymous_installation";
|
|
425
|
+
const consentConditions = [
|
|
426
|
+
eq(productAnalyticsConsentLedger.installationId, input.installationId),
|
|
427
|
+
eq(productAnalyticsConsentLedger.scope, scope),
|
|
428
|
+
eq(productAnalyticsConsentLedger.decision, "granted"),
|
|
429
|
+
eq(productAnalyticsConsentLedger.consentEpoch, first.consentEpoch),
|
|
430
|
+
first.consentedLocalUserId
|
|
431
|
+
? eq(productAnalyticsConsentLedger.localUserId, first.consentedLocalUserId)
|
|
432
|
+
: isNull(productAnalyticsConsentLedger.localUserId),
|
|
433
|
+
];
|
|
434
|
+
const [currentConsent] = await tx.select({ id: productAnalyticsConsentLedger.id })
|
|
435
|
+
.from(productAnalyticsConsentLedger)
|
|
436
|
+
.where(and(...consentConditions))
|
|
437
|
+
.limit(1);
|
|
438
|
+
if (!currentConsent) {
|
|
439
|
+
await tx.delete(productAnalyticsOutbox).where(inArray(productAnalyticsOutbox.eventId, rows.map((row) => row.eventId)));
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
await tx.update(productAnalyticsOutbox).set({ state: "claimed", claimToken: token, claimedAt: now, leaseExpiresAt, updatedAt: now }).where(inArray(productAnalyticsOutbox.eventId, rows.map((row) => row.eventId)));
|
|
443
|
+
return tx.select().from(productAnalyticsOutbox).where(inArray(productAnalyticsOutbox.eventId, rows.map((row) => row.eventId)));
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
export async function claimProductAnalyticsOutboxBatch(db, input) {
|
|
447
|
+
await assertProductAnalyticsInstallationSecret(db, input.installationId, input.installationSecret);
|
|
448
|
+
const rows = await claimProductAnalyticsOutbox(db, input);
|
|
449
|
+
if (rows.length === 0)
|
|
450
|
+
return null;
|
|
451
|
+
const eventRows = await db.select().from(productAnalyticsEvents).where(inArray(productAnalyticsEvents.id, rows.map((row) => row.eventId)));
|
|
452
|
+
const eventsById = new Map(eventRows.map((event) => [event.id, event]));
|
|
453
|
+
let payloads;
|
|
454
|
+
try {
|
|
455
|
+
payloads = rows.map((row) => {
|
|
456
|
+
const event = eventsById.get(row.eventId);
|
|
457
|
+
if (!event)
|
|
458
|
+
throw unprocessable("Product analytics outbox event is missing");
|
|
459
|
+
return buildProductAnalyticsExportPayload(input.installationSecret, event);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
await acknowledgeProductAnalyticsOutboxClaim(db, {
|
|
464
|
+
installationId: input.installationId,
|
|
465
|
+
installationSecret: input.installationSecret,
|
|
466
|
+
eventIds: rows.map((row) => row.eventId),
|
|
467
|
+
claimToken: rows[0]?.claimToken ?? "",
|
|
468
|
+
delivered: false,
|
|
469
|
+
errorCode: "payload_rejected",
|
|
470
|
+
});
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
const first = rows[0];
|
|
474
|
+
if (!first.claimToken)
|
|
475
|
+
throw unprocessable("Product analytics outbox claim token is missing");
|
|
476
|
+
return {
|
|
477
|
+
claimToken: first.claimToken,
|
|
478
|
+
installationId: first.installationId,
|
|
479
|
+
deliveryMode: first.deliveryMode,
|
|
480
|
+
consentScope: first.consentScope,
|
|
481
|
+
consentEpoch: first.consentEpoch,
|
|
482
|
+
events: payloads,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
export async function acknowledgeProductAnalyticsOutbox(db, input) {
|
|
486
|
+
const now = new Date();
|
|
487
|
+
const where = and(inArray(productAnalyticsOutbox.eventId, input.eventIds), eq(productAnalyticsOutbox.installationId, input.installationId), eq(productAnalyticsOutbox.claimToken, input.claimToken), eq(productAnalyticsOutbox.deliveryMode, input.deliveryMode), eq(productAnalyticsOutbox.consentEpoch, input.consentEpoch), input.consentedLocalUserId ? eq(productAnalyticsOutbox.consentedLocalUserId, input.consentedLocalUserId) : isNull(productAnalyticsOutbox.consentedLocalUserId));
|
|
488
|
+
const delivered = input.delivered ?? true;
|
|
489
|
+
const rows = await db.select({ eventId: productAnalyticsOutbox.eventId, attemptCount: productAnalyticsOutbox.attemptCount }).from(productAnalyticsOutbox).where(where);
|
|
490
|
+
if (rows.length === 0)
|
|
491
|
+
return { updatedCount: 0, state: delivered ? "delivered" : "retry_wait" };
|
|
492
|
+
const maxAttemptCount = Math.max(...rows.map((row) => row.attemptCount));
|
|
493
|
+
const deadLettered = !delivered && maxAttemptCount + 1 >= 5;
|
|
494
|
+
await db.update(productAnalyticsOutbox).set({
|
|
495
|
+
state: delivered ? "delivered" : deadLettered ? "failed_actionable" : "retry_wait",
|
|
496
|
+
attemptCount: sql `${productAnalyticsOutbox.attemptCount} + 1`,
|
|
497
|
+
deliveredAt: delivered ? now : null,
|
|
498
|
+
deadLetteredAt: deadLettered ? now : null,
|
|
499
|
+
nextAttemptAt: delivered || deadLettered ? now : new Date(now.getTime() + Math.min(60 * 60_000, 2 ** Math.min(maxAttemptCount, 6) * 60_000)),
|
|
500
|
+
lastErrorCode: input.errorCode ?? null,
|
|
501
|
+
claimToken: null,
|
|
502
|
+
leaseExpiresAt: null,
|
|
503
|
+
updatedAt: now,
|
|
504
|
+
}).where(where);
|
|
505
|
+
return { updatedCount: rows.length, state: delivered ? "delivered" : deadLettered ? "failed_actionable" : "retry_wait" };
|
|
506
|
+
}
|
|
507
|
+
export async function acknowledgeProductAnalyticsOutboxClaim(db, input) {
|
|
508
|
+
await assertProductAnalyticsInstallationSecret(db, input.installationId, input.installationSecret);
|
|
509
|
+
const rows = await db.select({
|
|
510
|
+
eventId: productAnalyticsOutbox.eventId,
|
|
511
|
+
deliveryMode: productAnalyticsOutbox.deliveryMode,
|
|
512
|
+
consentEpoch: productAnalyticsOutbox.consentEpoch,
|
|
513
|
+
consentedLocalUserId: productAnalyticsOutbox.consentedLocalUserId,
|
|
514
|
+
}).from(productAnalyticsOutbox).where(and(eq(productAnalyticsOutbox.installationId, input.installationId), eq(productAnalyticsOutbox.claimToken, input.claimToken), inArray(productAnalyticsOutbox.eventId, input.eventIds)));
|
|
515
|
+
if (rows.length === 0)
|
|
516
|
+
return { updatedCount: 0, state: input.delivered === false ? "retry_wait" : "delivered" };
|
|
517
|
+
const first = rows[0];
|
|
518
|
+
const sameClaim = rows.length === input.eventIds.length && rows.every((row) => (row.deliveryMode === first.deliveryMode
|
|
519
|
+
&& row.consentEpoch === first.consentEpoch
|
|
520
|
+
&& row.consentedLocalUserId === first.consentedLocalUserId));
|
|
521
|
+
if (!sameClaim)
|
|
522
|
+
throw forbidden("Product analytics ACK batch is mixed or incomplete");
|
|
523
|
+
return acknowledgeProductAnalyticsOutbox(db, {
|
|
524
|
+
...input,
|
|
525
|
+
deliveryMode: first.deliveryMode,
|
|
526
|
+
consentEpoch: first.consentEpoch,
|
|
527
|
+
consentedLocalUserId: first.consentedLocalUserId,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
export function pseudonymizeProductAnalyticsId(secret, value) {
|
|
531
|
+
return createHmac("sha256", secret).update(value).digest("hex");
|
|
532
|
+
}
|
|
533
|
+
export function buildProductAnalyticsExportPayload(secret, event) {
|
|
534
|
+
const properties = (event.properties ?? {});
|
|
535
|
+
const rawActorKind = String(event.actorType ?? "system");
|
|
536
|
+
const payload = {
|
|
537
|
+
eventId: String(event.id),
|
|
538
|
+
eventName: String(event.eventName),
|
|
539
|
+
schemaVersion: Number(event.schemaVersion ?? 1),
|
|
540
|
+
occurredAt: new Date(String(event.occurredAt)).toISOString(),
|
|
541
|
+
environment: String(event.environment ?? "production"),
|
|
542
|
+
appVersion: String(event.appVersion ?? "unknown"),
|
|
543
|
+
releaseChannel: String(event.releaseChannel ?? "stable"),
|
|
544
|
+
deploymentMode: String(event.deploymentMode ?? "self_hosted"),
|
|
545
|
+
pseudonymousInstallationId: event.installationId ? pseudonymizeProductAnalyticsId(secret, String(event.installationId)) : null,
|
|
546
|
+
actorKind: rawActorKind === "human" ? "user" : rawActorKind,
|
|
547
|
+
origin: String(event.origin ?? "system"),
|
|
548
|
+
pseudonymousOrgId: event.orgId ? pseudonymizeProductAnalyticsId(secret, String(event.orgId)) : null,
|
|
549
|
+
pseudonymousWorkId: event.workId ? pseudonymizeProductAnalyticsId(secret, String(event.workId)) : null,
|
|
550
|
+
pseudonymousWorkCycleId: event.workCycleId ? pseudonymizeProductAnalyticsId(secret, String(event.workCycleId)) : null,
|
|
551
|
+
pseudonymousRootRunId: event.rootRunId ? pseudonymizeProductAnalyticsId(secret, String(event.rootRunId)) : null,
|
|
552
|
+
pseudonymousRunId: event.runId ? pseudonymizeProductAnalyticsId(secret, String(event.runId)) : null,
|
|
553
|
+
completionRevision: event.completionRevision ?? null,
|
|
554
|
+
properties,
|
|
555
|
+
confidence: String(event.confidence ?? "unknown"),
|
|
556
|
+
isBackfill: Boolean(event.isBackfill),
|
|
557
|
+
};
|
|
558
|
+
const serialized = JSON.stringify(payload);
|
|
559
|
+
if (serialized.length > 2048)
|
|
560
|
+
throw unprocessable("Product analytics export payload is too large");
|
|
561
|
+
if (/(prompt|transcript|title|description|body|content|url|email|secret|token)/i.test(serialized))
|
|
562
|
+
throw unprocessable("Product analytics export payload contains sensitive data");
|
|
563
|
+
return payload;
|
|
564
|
+
}
|
|
565
|
+
function parseWindow(input) {
|
|
566
|
+
const to = input.to ?? new Date();
|
|
567
|
+
const windowDays = input.windowDays ?? 7;
|
|
568
|
+
if (!Number.isInteger(windowDays) || windowDays < 1 || windowDays > 366) {
|
|
569
|
+
throw unprocessable("Product analytics windowDays must be between 1 and 366");
|
|
570
|
+
}
|
|
571
|
+
const from = input.from ?? new Date(to.getTime() - windowDays * 24 * 60 * 60 * 1000);
|
|
572
|
+
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || from >= to) {
|
|
573
|
+
throw unprocessable("Product analytics time window is invalid");
|
|
574
|
+
}
|
|
575
|
+
return { from, to };
|
|
576
|
+
}
|
|
577
|
+
export function productAnalyticsService(db) {
|
|
578
|
+
return {
|
|
579
|
+
record: (input) => recordProductAnalyticsEvent(db, input),
|
|
580
|
+
summary: async (orgId, input = {}) => {
|
|
581
|
+
const window = parseWindow(input);
|
|
582
|
+
const where = and(eq(productAnalyticsEvents.orgId, orgId), gte(productAnalyticsEvents.occurredAt, window.from), lt(productAnalyticsEvents.occurredAt, window.to));
|
|
583
|
+
const [eventCounts, distinctHumanRows, totalRows, completedLoopRows, invalidatedLoopRows, meaningfulInstallationRows, productiveInstallationRows] = await Promise.all([
|
|
584
|
+
db
|
|
585
|
+
.select({ eventName: productAnalyticsEvents.eventName, count: sql `count(*)` })
|
|
586
|
+
.from(productAnalyticsEvents)
|
|
587
|
+
.where(where)
|
|
588
|
+
.groupBy(productAnalyticsEvents.eventName),
|
|
589
|
+
db
|
|
590
|
+
.select({ count: sql `count(distinct ${productAnalyticsEvents.actorId})` })
|
|
591
|
+
.from(productAnalyticsEvents)
|
|
592
|
+
.where(and(where, eq(productAnalyticsEvents.actorType, "human"), sql `${productAnalyticsEvents.actorId} is not null`)),
|
|
593
|
+
db
|
|
594
|
+
.select({ count: sql `count(*)` })
|
|
595
|
+
.from(productAnalyticsEvents)
|
|
596
|
+
.where(where),
|
|
597
|
+
db
|
|
598
|
+
.select({ count: sql `count(distinct ${productAnalyticsEvents.workCycleId} || ':' || ${productAnalyticsEvents.completionRevision})` })
|
|
599
|
+
.from(productAnalyticsEvents)
|
|
600
|
+
.where(and(where, eq(productAnalyticsEvents.eventName, "work_loop_completed"), eq(productAnalyticsEvents.origin, "human"), eq(productAnalyticsEvents.environment, "production"), sql `not exists (
|
|
601
|
+
select 1 from ${productAnalyticsWorkCycles} invalidated
|
|
602
|
+
where invalidated.org_id = ${productAnalyticsEvents.orgId}
|
|
603
|
+
and invalidated.work_cycle_id = ${productAnalyticsEvents.workCycleId}
|
|
604
|
+
and invalidated.state = 'invalidated'
|
|
605
|
+
)`)),
|
|
606
|
+
db
|
|
607
|
+
.select({ count: sql `count(*)` })
|
|
608
|
+
.from(productAnalyticsEvents)
|
|
609
|
+
.where(and(where, eq(productAnalyticsEvents.eventName, "work_loop_invalidated"))),
|
|
610
|
+
db
|
|
611
|
+
.select({ count: sql `count(distinct ${productAnalyticsEvents.installationId})` })
|
|
612
|
+
.from(productAnalyticsEvents)
|
|
613
|
+
.where(and(where, inArray(productAnalyticsEvents.eventName, ["human_work_started", "review_decision_recorded", "work_loop_completed"]), eq(productAnalyticsEvents.origin, "human"), eq(productAnalyticsEvents.environment, "production"), sql `${productAnalyticsEvents.installationId} is not null`)),
|
|
614
|
+
db
|
|
615
|
+
.select({ count: sql `count(distinct ${productAnalyticsEvents.installationId})` })
|
|
616
|
+
.from(productAnalyticsEvents)
|
|
617
|
+
.where(and(where, inArray(productAnalyticsEvents.eventName, ["output_ready", "work_loop_completed"]), eq(productAnalyticsEvents.origin, "human"), eq(productAnalyticsEvents.environment, "production"), sql `${productAnalyticsEvents.installationId} is not null`)),
|
|
618
|
+
]);
|
|
619
|
+
const [approvedReviewRows] = await Promise.all([
|
|
620
|
+
db
|
|
621
|
+
.select({ count: sql `count(*)` })
|
|
622
|
+
.from(productAnalyticsEvents)
|
|
623
|
+
.where(and(where, eq(productAnalyticsEvents.eventName, "review_decision_recorded"), sql `${productAnalyticsEvents.properties}->>'decision' = 'approve'`)),
|
|
624
|
+
]);
|
|
625
|
+
const counts = Object.fromEntries(eventCounts.map((row) => [row.eventName, Number(row.count)]));
|
|
626
|
+
return {
|
|
627
|
+
window: { from: window.from.toISOString(), to: window.to.toISOString() },
|
|
628
|
+
metrics: {
|
|
629
|
+
mutating_user_dau_local: Number(distinctHumanRows[0]?.count ?? 0),
|
|
630
|
+
meaningful_active_installations: Number(meaningfulInstallationRows[0]?.count ?? 0),
|
|
631
|
+
productive_installations: Number(productiveInstallationRows[0]?.count ?? 0),
|
|
632
|
+
successful_runs: counts.run_succeeded ?? 0,
|
|
633
|
+
failed_runs: counts.run_failed ?? 0,
|
|
634
|
+
human_work_started: counts.human_work_started ?? 0,
|
|
635
|
+
output_ready: counts.output_ready ?? 0,
|
|
636
|
+
review_decisions_recorded: counts.review_decision_recorded ?? 0,
|
|
637
|
+
reviewed_issue_completions: Number(approvedReviewRows[0]?.count ?? 0),
|
|
638
|
+
weekly_completed_work_loops: Number(completedLoopRows[0]?.count ?? 0),
|
|
639
|
+
work_loop_invalidations: Number(invalidatedLoopRows[0]?.count ?? 0),
|
|
640
|
+
ledger_event_count: Number(totalRows[0]?.count ?? 0),
|
|
641
|
+
},
|
|
642
|
+
dataQuality: {
|
|
643
|
+
source: "local_product_analytics_events",
|
|
644
|
+
producedExactEventNames: PRODUCT_ANALYTICS_PRODUCED_EVENT_NAMES,
|
|
645
|
+
producedDerivedEventNames: PRODUCT_ANALYTICS_DERIVED_EVENT_NAMES,
|
|
646
|
+
deferredEventNames: PRODUCT_ANALYTICS_DEFERRED_EVENT_NAMES,
|
|
647
|
+
completeDau: false,
|
|
648
|
+
completeNorthStar: false,
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
},
|
|
652
|
+
listEvents: async (orgId, input = {}) => {
|
|
653
|
+
const window = parseWindow(input);
|
|
654
|
+
const limit = Math.min(Math.max(Math.floor(input.limit ?? 100), 1), 500);
|
|
655
|
+
const conditions = [
|
|
656
|
+
eq(productAnalyticsEvents.orgId, orgId),
|
|
657
|
+
gte(productAnalyticsEvents.occurredAt, window.from),
|
|
658
|
+
lt(productAnalyticsEvents.occurredAt, window.to),
|
|
659
|
+
];
|
|
660
|
+
if (input.eventName) {
|
|
661
|
+
if (!EVENT_NAME_SET.has(input.eventName))
|
|
662
|
+
throw unprocessable("Unknown product analytics event");
|
|
663
|
+
conditions.push(eq(productAnalyticsEvents.eventName, input.eventName));
|
|
664
|
+
}
|
|
665
|
+
return db
|
|
666
|
+
.select({
|
|
667
|
+
id: productAnalyticsEvents.id,
|
|
668
|
+
installationId: productAnalyticsEvents.installationId,
|
|
669
|
+
eventName: productAnalyticsEvents.eventName,
|
|
670
|
+
schemaVersion: productAnalyticsEvents.schemaVersion,
|
|
671
|
+
occurredAt: productAnalyticsEvents.occurredAt,
|
|
672
|
+
sourceTransition: productAnalyticsEvents.sourceTransition,
|
|
673
|
+
confidence: productAnalyticsEvents.confidence,
|
|
674
|
+
actorType: productAnalyticsEvents.actorType,
|
|
675
|
+
environment: productAnalyticsEvents.environment,
|
|
676
|
+
appVersion: productAnalyticsEvents.appVersion,
|
|
677
|
+
releaseChannel: productAnalyticsEvents.releaseChannel,
|
|
678
|
+
deploymentMode: productAnalyticsEvents.deploymentMode,
|
|
679
|
+
origin: productAnalyticsEvents.origin,
|
|
680
|
+
workSurface: productAnalyticsEvents.workSurface,
|
|
681
|
+
workId: productAnalyticsEvents.workId,
|
|
682
|
+
workCycleId: productAnalyticsEvents.workCycleId,
|
|
683
|
+
rootRunId: productAnalyticsEvents.rootRunId,
|
|
684
|
+
runId: productAnalyticsEvents.runId,
|
|
685
|
+
completionRevision: productAnalyticsEvents.completionRevision,
|
|
686
|
+
entityType: productAnalyticsEvents.entityType,
|
|
687
|
+
entityId: productAnalyticsEvents.entityId,
|
|
688
|
+
dedupeKey: productAnalyticsEvents.dedupeKey,
|
|
689
|
+
properties: productAnalyticsEvents.properties,
|
|
690
|
+
})
|
|
691
|
+
.from(productAnalyticsEvents)
|
|
692
|
+
.where(and(...conditions))
|
|
693
|
+
.orderBy(desc(productAnalyticsEvents.occurredAt), desc(productAnalyticsEvents.recordedAt))
|
|
694
|
+
.limit(limit);
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
//# sourceMappingURL=product-analytics.js.map
|