@ucptools/validator 1.0.0 → 1.1.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.
Files changed (330) hide show
  1. package/dist/auth/config.d.ts +20 -0
  2. package/dist/auth/config.d.ts.map +1 -0
  3. package/dist/auth/config.js +114 -0
  4. package/dist/auth/config.js.map +1 -0
  5. package/dist/auth/index.d.ts +5 -0
  6. package/dist/auth/index.d.ts.map +1 -0
  7. package/dist/auth/index.js +17 -0
  8. package/dist/auth/index.js.map +1 -0
  9. package/dist/auth/middleware.d.ts +45 -0
  10. package/dist/auth/middleware.d.ts.map +1 -0
  11. package/dist/auth/middleware.js +170 -0
  12. package/dist/auth/middleware.js.map +1 -0
  13. package/dist/auth/service.d.ts +80 -0
  14. package/dist/auth/service.d.ts.map +1 -0
  15. package/dist/auth/service.js +298 -0
  16. package/dist/auth/service.js.map +1 -0
  17. package/dist/cli/index.d.ts +6 -0
  18. package/dist/cli/index.d.ts.map +1 -0
  19. package/dist/cli/index.js +375 -0
  20. package/dist/cli/index.js.map +1 -0
  21. package/dist/cli/mock-server.d.ts +20 -0
  22. package/dist/cli/mock-server.d.ts.map +1 -0
  23. package/dist/cli/mock-server.js +261 -0
  24. package/dist/cli/mock-server.js.map +1 -0
  25. package/dist/compliance/compliance-generator.d.ts +34 -0
  26. package/dist/compliance/compliance-generator.d.ts.map +1 -0
  27. package/dist/compliance/compliance-generator.js +320 -0
  28. package/dist/compliance/compliance-generator.js.map +1 -0
  29. package/dist/compliance/index.d.ts +8 -0
  30. package/dist/compliance/index.d.ts.map +1 -0
  31. package/dist/compliance/index.js +17 -0
  32. package/dist/compliance/index.js.map +1 -0
  33. package/dist/compliance/templates.d.ts +34 -0
  34. package/dist/compliance/templates.d.ts.map +1 -0
  35. package/{src/compliance/templates.ts → dist/compliance/templates.js} +117 -155
  36. package/dist/compliance/templates.js.map +1 -0
  37. package/dist/compliance/types.d.ts +64 -0
  38. package/dist/compliance/types.d.ts.map +1 -0
  39. package/dist/compliance/types.js +64 -0
  40. package/dist/compliance/types.js.map +1 -0
  41. package/dist/db/index.d.ts +17 -0
  42. package/dist/db/index.d.ts.map +1 -0
  43. package/dist/db/index.js +80 -0
  44. package/dist/db/index.js.map +1 -0
  45. package/dist/db/schema.d.ts +3886 -0
  46. package/dist/db/schema.d.ts.map +1 -0
  47. package/dist/db/schema.js +425 -0
  48. package/dist/db/schema.js.map +1 -0
  49. package/dist/db/utils.d.ts +252 -0
  50. package/dist/db/utils.d.ts.map +1 -0
  51. package/dist/db/utils.js +295 -0
  52. package/dist/db/utils.js.map +1 -0
  53. package/dist/feed-analyzer/feed-analyzer.d.ts +26 -0
  54. package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -0
  55. package/{src/feed-analyzer/feed-analyzer.ts → dist/feed-analyzer/feed-analyzer.js} +856 -726
  56. package/dist/feed-analyzer/feed-analyzer.js.map +1 -0
  57. package/dist/feed-analyzer/index.d.ts +8 -0
  58. package/dist/feed-analyzer/index.d.ts.map +1 -0
  59. package/dist/feed-analyzer/index.js +19 -0
  60. package/dist/feed-analyzer/index.js.map +1 -0
  61. package/dist/feed-analyzer/types.d.ts +285 -0
  62. package/dist/feed-analyzer/types.d.ts.map +1 -0
  63. package/dist/feed-analyzer/types.js +175 -0
  64. package/dist/feed-analyzer/types.js.map +1 -0
  65. package/{src/generator/index.ts → dist/generator/index.d.ts} +1 -1
  66. package/dist/generator/index.d.ts.map +1 -0
  67. package/dist/generator/index.js +13 -0
  68. package/dist/generator/index.js.map +1 -0
  69. package/dist/generator/key-generator.d.ts +24 -0
  70. package/dist/generator/key-generator.d.ts.map +1 -0
  71. package/dist/generator/key-generator.js +144 -0
  72. package/dist/generator/key-generator.js.map +1 -0
  73. package/dist/generator/profile-builder.d.ts +15 -0
  74. package/dist/generator/profile-builder.d.ts.map +1 -0
  75. package/dist/generator/profile-builder.js +338 -0
  76. package/dist/generator/profile-builder.js.map +1 -0
  77. package/dist/hosting/artifacts-generator.d.ts +10 -0
  78. package/dist/hosting/artifacts-generator.d.ts.map +1 -0
  79. package/{src/hosting/artifacts-generator.ts → dist/hosting/artifacts-generator.js} +191 -241
  80. package/dist/hosting/artifacts-generator.js.map +1 -0
  81. package/{src/hosting/index.ts → dist/hosting/index.d.ts} +1 -1
  82. package/dist/hosting/index.d.ts.map +1 -0
  83. package/dist/hosting/index.js +10 -0
  84. package/dist/hosting/index.js.map +1 -0
  85. package/dist/index.d.ts +18 -0
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +78 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/lib/analytics.d.ts +337 -0
  90. package/dist/lib/analytics.d.ts.map +1 -0
  91. package/dist/lib/analytics.js +188 -0
  92. package/dist/lib/analytics.js.map +1 -0
  93. package/{src/security/index.ts → dist/security/index.d.ts} +8 -15
  94. package/dist/security/index.d.ts.map +1 -0
  95. package/dist/security/index.js +12 -0
  96. package/dist/security/index.js.map +1 -0
  97. package/dist/security/security-scanner.d.ts +10 -0
  98. package/dist/security/security-scanner.d.ts.map +1 -0
  99. package/dist/security/security-scanner.js +669 -0
  100. package/dist/security/security-scanner.js.map +1 -0
  101. package/dist/security/types.d.ts +80 -0
  102. package/dist/security/types.d.ts.map +1 -0
  103. package/dist/security/types.js +21 -0
  104. package/dist/security/types.js.map +1 -0
  105. package/dist/services/analytics.d.ts +114 -0
  106. package/dist/services/analytics.d.ts.map +1 -0
  107. package/dist/services/analytics.js +862 -0
  108. package/dist/services/analytics.js.map +1 -0
  109. package/dist/services/badge.d.ts +31 -0
  110. package/dist/services/badge.d.ts.map +1 -0
  111. package/dist/services/badge.js +152 -0
  112. package/dist/services/badge.js.map +1 -0
  113. package/dist/services/cron.d.ts +125 -0
  114. package/dist/services/cron.d.ts.map +1 -0
  115. package/dist/services/cron.js +613 -0
  116. package/dist/services/cron.js.map +1 -0
  117. package/dist/services/directory.d.ts +106 -0
  118. package/dist/services/directory.d.ts.map +1 -0
  119. package/dist/services/directory.js +351 -0
  120. package/dist/services/directory.js.map +1 -0
  121. package/dist/services/email.d.ts +112 -0
  122. package/dist/services/email.d.ts.map +1 -0
  123. package/dist/services/email.js +772 -0
  124. package/dist/services/email.js.map +1 -0
  125. package/dist/services/hosted-profiles.d.ts +77 -0
  126. package/dist/services/hosted-profiles.d.ts.map +1 -0
  127. package/dist/services/hosted-profiles.js +433 -0
  128. package/dist/services/hosted-profiles.js.map +1 -0
  129. package/dist/services/latency.d.ts +67 -0
  130. package/dist/services/latency.d.ts.map +1 -0
  131. package/dist/services/latency.js +274 -0
  132. package/dist/services/latency.js.map +1 -0
  133. package/dist/services/manifest-compliance.d.ts +64 -0
  134. package/dist/services/manifest-compliance.d.ts.map +1 -0
  135. package/dist/services/manifest-compliance.js +271 -0
  136. package/dist/services/manifest-compliance.js.map +1 -0
  137. package/dist/services/monitoring-diff.d.ts +31 -0
  138. package/dist/services/monitoring-diff.d.ts.map +1 -0
  139. package/dist/services/monitoring-diff.js +189 -0
  140. package/dist/services/monitoring-diff.js.map +1 -0
  141. package/dist/services/notifications.d.ts +46 -0
  142. package/dist/services/notifications.d.ts.map +1 -0
  143. package/dist/services/notifications.js +88 -0
  144. package/dist/services/notifications.js.map +1 -0
  145. package/dist/services/stripe.d.ts +93 -0
  146. package/dist/services/stripe.d.ts.map +1 -0
  147. package/dist/services/stripe.js +490 -0
  148. package/dist/services/stripe.js.map +1 -0
  149. package/dist/services/validation-history.d.ts +99 -0
  150. package/dist/services/validation-history.d.ts.map +1 -0
  151. package/dist/services/validation-history.js +344 -0
  152. package/dist/services/validation-history.js.map +1 -0
  153. package/dist/services/validation-logging.d.ts +103 -0
  154. package/dist/services/validation-logging.d.ts.map +1 -0
  155. package/dist/services/validation-logging.js +210 -0
  156. package/dist/services/validation-logging.js.map +1 -0
  157. package/dist/services/validation.d.ts +119 -0
  158. package/dist/services/validation.d.ts.map +1 -0
  159. package/dist/services/validation.js +1185 -0
  160. package/dist/services/validation.js.map +1 -0
  161. package/dist/simulator/agent-simulator.d.ts +69 -0
  162. package/dist/simulator/agent-simulator.d.ts.map +1 -0
  163. package/dist/simulator/agent-simulator.js +870 -0
  164. package/dist/simulator/agent-simulator.js.map +1 -0
  165. package/{src/simulator/index.ts → dist/simulator/index.d.ts} +7 -7
  166. package/dist/simulator/index.d.ts.map +1 -0
  167. package/dist/simulator/index.js +23 -0
  168. package/dist/simulator/index.js.map +1 -0
  169. package/{src/simulator/types.ts → dist/simulator/types.d.ts} +171 -170
  170. package/dist/simulator/types.d.ts.map +1 -0
  171. package/dist/simulator/types.js +18 -0
  172. package/dist/simulator/types.js.map +1 -0
  173. package/dist/types/acp-validation.d.ts +87 -0
  174. package/dist/types/acp-validation.d.ts.map +1 -0
  175. package/dist/types/acp-validation.js +40 -0
  176. package/dist/types/acp-validation.js.map +1 -0
  177. package/dist/types/analytics.d.ts +182 -0
  178. package/dist/types/analytics.d.ts.map +1 -0
  179. package/dist/types/analytics.js +7 -0
  180. package/dist/types/analytics.js.map +1 -0
  181. package/dist/types/generator.d.ts +106 -0
  182. package/dist/types/generator.d.ts.map +1 -0
  183. package/dist/types/generator.js +6 -0
  184. package/dist/types/generator.js.map +1 -0
  185. package/{src/types/index.ts → dist/types/index.d.ts} +1 -1
  186. package/dist/types/index.d.ts.map +1 -0
  187. package/dist/types/index.js +23 -0
  188. package/dist/types/index.js.map +1 -0
  189. package/dist/types/ucp-profile.d.ts +111 -0
  190. package/dist/types/ucp-profile.d.ts.map +1 -0
  191. package/dist/types/ucp-profile.js +45 -0
  192. package/dist/types/ucp-profile.js.map +1 -0
  193. package/dist/types/validation.d.ts +76 -0
  194. package/dist/types/validation.d.ts.map +1 -0
  195. package/dist/types/validation.js +42 -0
  196. package/dist/types/validation.js.map +1 -0
  197. package/dist/validator/acp/index.d.ts +31 -0
  198. package/dist/validator/acp/index.d.ts.map +1 -0
  199. package/dist/validator/acp/index.js +574 -0
  200. package/dist/validator/acp/index.js.map +1 -0
  201. package/dist/validator/index.d.ts +26 -0
  202. package/dist/validator/index.d.ts.map +1 -0
  203. package/dist/validator/index.js +161 -0
  204. package/dist/validator/index.js.map +1 -0
  205. package/dist/validator/network-validator.d.ts +28 -0
  206. package/dist/validator/network-validator.d.ts.map +1 -0
  207. package/dist/validator/network-validator.js +319 -0
  208. package/dist/validator/network-validator.js.map +1 -0
  209. package/dist/validator/rules-validator.d.ts +19 -0
  210. package/dist/validator/rules-validator.d.ts.map +1 -0
  211. package/dist/validator/rules-validator.js +306 -0
  212. package/dist/validator/rules-validator.js.map +1 -0
  213. package/dist/validator/sdk-validator.d.ts +58 -0
  214. package/dist/validator/sdk-validator.d.ts.map +1 -0
  215. package/{src/validator/sdk-validator.ts → dist/validator/sdk-validator.js} +273 -330
  216. package/dist/validator/sdk-validator.js.map +1 -0
  217. package/dist/validator/structural-validator.d.ts +11 -0
  218. package/dist/validator/structural-validator.d.ts.map +1 -0
  219. package/dist/validator/structural-validator.js +549 -0
  220. package/dist/validator/structural-validator.js.map +1 -0
  221. package/dist/validator/utils.d.ts +51 -0
  222. package/dist/validator/utils.d.ts.map +1 -0
  223. package/dist/validator/utils.js +132 -0
  224. package/dist/validator/utils.js.map +1 -0
  225. package/package.json +44 -12
  226. package/CLAUDE.md +0 -109
  227. package/api/analyze-feed.js +0 -140
  228. package/api/badge.js +0 -185
  229. package/api/benchmark.js +0 -177
  230. package/api/directory-stats.ts +0 -29
  231. package/api/directory.ts +0 -73
  232. package/api/generate-compliance.js +0 -143
  233. package/api/generate-schema.js +0 -457
  234. package/api/generate.js +0 -132
  235. package/api/security-scan.js +0 -133
  236. package/api/simulate.js +0 -187
  237. package/api/tsconfig.json +0 -10
  238. package/api/validate.js +0 -1351
  239. package/apify-actor/.actor/actor.json +0 -68
  240. package/apify-actor/.actor/input_schema.json +0 -32
  241. package/apify-actor/APIFY-STORE-LISTING.md +0 -412
  242. package/apify-actor/Dockerfile +0 -8
  243. package/apify-actor/README.md +0 -166
  244. package/apify-actor/main.ts +0 -111
  245. package/apify-actor/package.json +0 -17
  246. package/apify-actor/src/main.js +0 -199
  247. package/docs/BRAND-IDENTITY.md +0 -238
  248. package/docs/BRAND-STYLE-GUIDE.md +0 -356
  249. package/drizzle/0000_black_king_cobra.sql +0 -39
  250. package/drizzle/meta/0000_snapshot.json +0 -309
  251. package/drizzle/meta/_journal.json +0 -13
  252. package/drizzle.config.ts +0 -10
  253. package/public/.well-known/ucp +0 -25
  254. package/public/android-chrome-192x192.png +0 -0
  255. package/public/android-chrome-512x512.png +0 -0
  256. package/public/apple-touch-icon.png +0 -0
  257. package/public/brand.css +0 -321
  258. package/public/directory.html +0 -701
  259. package/public/favicon-16x16.png +0 -0
  260. package/public/favicon-32x32.png +0 -0
  261. package/public/favicon.ico +0 -0
  262. package/public/guides/bigcommerce.html +0 -743
  263. package/public/guides/fastucp.html +0 -838
  264. package/public/guides/magento.html +0 -779
  265. package/public/guides/shopify.html +0 -726
  266. package/public/guides/squarespace.html +0 -749
  267. package/public/guides/wix.html +0 -747
  268. package/public/guides/woocommerce.html +0 -733
  269. package/public/index.html +0 -3835
  270. package/public/learn.html +0 -396
  271. package/public/logo.jpeg +0 -0
  272. package/public/og-image-icon.png +0 -0
  273. package/public/og-image.png +0 -0
  274. package/public/robots.txt +0 -6
  275. package/public/site.webmanifest +0 -31
  276. package/public/sitemap.xml +0 -69
  277. package/public/social/linkedin-banner-1128x191.png +0 -0
  278. package/public/social/temp.PNG +0 -0
  279. package/public/social/x-header-1500x500.png +0 -0
  280. package/public/verify.html +0 -410
  281. package/scripts/generate-favicons.js +0 -44
  282. package/scripts/generate-ico.js +0 -23
  283. package/scripts/generate-og-image.js +0 -45
  284. package/scripts/reset-db.ts +0 -77
  285. package/scripts/seed-db.ts +0 -71
  286. package/scripts/setup-benchmark-db.js +0 -70
  287. package/src/api/server.ts +0 -266
  288. package/src/cli/index.ts +0 -302
  289. package/src/compliance/compliance-generator.ts +0 -452
  290. package/src/compliance/index.ts +0 -28
  291. package/src/compliance/types.ts +0 -170
  292. package/src/db/index.ts +0 -28
  293. package/src/db/schema.ts +0 -84
  294. package/src/feed-analyzer/index.ts +0 -34
  295. package/src/feed-analyzer/types.ts +0 -354
  296. package/src/generator/key-generator.ts +0 -124
  297. package/src/generator/profile-builder.ts +0 -402
  298. package/src/index.ts +0 -105
  299. package/src/security/security-scanner.ts +0 -604
  300. package/src/security/types.ts +0 -55
  301. package/src/services/directory.ts +0 -434
  302. package/src/simulator/agent-simulator.ts +0 -941
  303. package/src/types/generator.ts +0 -140
  304. package/src/types/ucp-profile.ts +0 -140
  305. package/src/types/validation.ts +0 -89
  306. package/src/validator/index.ts +0 -194
  307. package/src/validator/network-validator.ts +0 -417
  308. package/src/validator/rules-validator.ts +0 -297
  309. package/src/validator/structural-validator.ts +0 -476
  310. package/tests/fixtures/non-compliant-profile.json +0 -25
  311. package/tests/fixtures/official-sample-profile.json +0 -75
  312. package/tests/integration/benchmark.test.ts +0 -207
  313. package/tests/integration/database.test.ts +0 -163
  314. package/tests/integration/directory-api.test.ts +0 -268
  315. package/tests/integration/simulate-api.test.ts +0 -230
  316. package/tests/integration/validate-api.test.ts +0 -269
  317. package/tests/setup.ts +0 -15
  318. package/tests/unit/agent-simulator.test.ts +0 -575
  319. package/tests/unit/compliance-generator.test.ts +0 -374
  320. package/tests/unit/directory-service.test.ts +0 -272
  321. package/tests/unit/feed-analyzer.test.ts +0 -517
  322. package/tests/unit/lint-suggestions.test.ts +0 -423
  323. package/tests/unit/official-samples.test.ts +0 -211
  324. package/tests/unit/pdf-report.test.ts +0 -390
  325. package/tests/unit/sdk-validator.test.ts +0 -531
  326. package/tests/unit/security-scanner.test.ts +0 -410
  327. package/tests/unit/validation.test.ts +0 -390
  328. package/tsconfig.json +0 -20
  329. package/vercel.json +0 -34
  330. package/vitest.config.ts +0 -22
@@ -0,0 +1,862 @@
1
+ "use strict";
2
+ /**
3
+ * Analytics Service
4
+ * Business logic for AI Agent Traffic Analytics (V1 + V2)
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.detectAgentType = detectAgentType;
8
+ exports.hashIpAddress = hashIpAddress;
9
+ exports.ingestEvents = ingestEvents;
10
+ exports.getAnalyticsSummary = getAnalyticsSummary;
11
+ exports.getTimeseries = getTimeseries;
12
+ exports.getEventLog = getEventLog;
13
+ exports.getFunnelData = getFunnelData;
14
+ exports.getCommerceKPIs = getCommerceKPIs;
15
+ exports.getAgentComparison = getAgentComparison;
16
+ exports.getErrorAnalysis = getErrorAnalysis;
17
+ exports.getCapabilityGaps = getCapabilityGaps;
18
+ exports.createAnalyticsApiKey = createAnalyticsApiKey;
19
+ exports.validateAnalyticsApiKey = validateAnalyticsApiKey;
20
+ exports.getAnalyticsApiKeys = getAnalyticsApiKeys;
21
+ exports.deleteAnalyticsApiKey = deleteAnalyticsApiKey;
22
+ exports.aggregateEventsForDate = aggregateEventsForDate;
23
+ exports.purgeOldEvents = purgeOldEvents;
24
+ exports.getDomainFromApiKey = getDomainFromApiKey;
25
+ exports.userOwnsDomain = userOwnsDomain;
26
+ const drizzle_orm_1 = require("drizzle-orm");
27
+ const index_js_1 = require("../db/index.js");
28
+ const schema_js_1 = require("../db/schema.js");
29
+ const bcryptjs_1 = require("bcryptjs");
30
+ const nanoid_1 = require("nanoid");
31
+ const crypto_1 = require("crypto");
32
+ // Agent detection patterns
33
+ const AGENT_PATTERNS = {
34
+ gemini: /googlebot|google-extended|gemini/i,
35
+ chatgpt: /chatgpt|openai|gptbot/i,
36
+ perplexity: /perplexitybot/i,
37
+ claude: /anthropic|claude/i,
38
+ copilot: /copilot|bingbot/i,
39
+ unknown: /./, // Fallback
40
+ };
41
+ // Funnel stage definitions: maps event types to funnel stages
42
+ const FUNNEL_STAGES = [
43
+ { stage: 'Discovery', types: ['discovery'] },
44
+ { stage: 'Browse', types: ['capability', 'capability_negotiation', 'product_browse', 'product_detail'] },
45
+ { stage: 'Checkout', types: ['checkout', 'checkout_create', 'checkout_update', 'checkout_escalation', 'acp_checkout_create', 'acp_checkout_update'] },
46
+ { stage: 'Payment', types: ['payment_attempt', 'payment_success', 'acp_payment'] },
47
+ { stage: 'Order', types: ['order', 'order_created', 'acp_order'] },
48
+ ];
49
+ // Checkout-related event types that trigger session tracking
50
+ const CHECKOUT_EVENT_TYPES = new Set([
51
+ 'checkout', 'checkout_create', 'checkout_update', 'checkout_escalation',
52
+ 'payment_attempt', 'payment_success', 'payment_failure',
53
+ 'order', 'order_created',
54
+ 'acp_checkout_create', 'acp_checkout_update', 'acp_payment', 'acp_order',
55
+ ]);
56
+ // Error fix suggestions by HTTP status
57
+ const ERROR_SUGGESTIONS = {
58
+ 400: 'Check request payload format — agents may be sending malformed data',
59
+ 401: 'Verify authentication configuration — agents cannot access protected endpoints',
60
+ 403: 'Check CORS and permission settings — agents are being blocked',
61
+ 404: 'Verify endpoint URLs in your UCP profile — agents are hitting missing routes',
62
+ 422: 'Validate your product feed — line items or checkout data may be invalid',
63
+ 429: 'Increase rate limits — AI agents are being throttled',
64
+ 500: 'Check server logs for internal errors on UCP endpoints',
65
+ 502: 'Check upstream service health — your backend may be down',
66
+ 503: 'Service unavailable — check capacity and health checks',
67
+ };
68
+ /**
69
+ * Detect agent type and version from User-Agent string
70
+ */
71
+ function detectAgentType(userAgent) {
72
+ if (!userAgent) {
73
+ return { type: 'unknown' };
74
+ }
75
+ for (const [agentType, pattern] of Object.entries(AGENT_PATTERNS)) {
76
+ if (agentType === 'unknown')
77
+ continue;
78
+ if (pattern.test(userAgent)) {
79
+ // Try to extract version
80
+ const versionMatch = userAgent.match(/\/(\d+(?:\.\d+)*)/);
81
+ return {
82
+ type: agentType,
83
+ version: versionMatch?.[1],
84
+ };
85
+ }
86
+ }
87
+ return { type: 'unknown' };
88
+ }
89
+ /**
90
+ * Hash an IP address for privacy
91
+ */
92
+ function hashIpAddress(ip) {
93
+ return (0, crypto_1.createHash)('sha256').update(ip).digest('hex').substring(0, 64);
94
+ }
95
+ /**
96
+ * Upsert checkout session tracking
97
+ */
98
+ async function upsertCheckoutSession(domainId, event) {
99
+ if (!event.checkoutSessionId)
100
+ return;
101
+ const db = (0, index_js_1.getDb)();
102
+ const detection = detectAgentType(event.userAgent);
103
+ try {
104
+ const existing = await db
105
+ .select()
106
+ .from(schema_js_1.analyticsCheckoutSessions)
107
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsCheckoutSessions.domainId, domainId), (0, drizzle_orm_1.eq)(schema_js_1.analyticsCheckoutSessions.checkoutSessionId, event.checkoutSessionId)))
108
+ .limit(1);
109
+ if (existing.length > 0) {
110
+ const session = existing[0];
111
+ const updates = {
112
+ updatedAt: new Date(),
113
+ };
114
+ if (event.checkoutState)
115
+ updates.currentState = event.checkoutState;
116
+ if (event.lineItemsCount)
117
+ updates.lineItemsCount = event.lineItemsCount;
118
+ if (event.orderTotal)
119
+ updates.orderTotal = String(event.orderTotal);
120
+ if (event.currency)
121
+ updates.currency = event.currency;
122
+ if (event.paymentHandler)
123
+ updates.paymentHandler = event.paymentHandler;
124
+ // Track escalation
125
+ if (event.checkoutState === 'requires_escalation' || event.eventType === 'checkout_escalation') {
126
+ updates.hadEscalation = true;
127
+ if (event.escalationReason)
128
+ updates.escalationReason = event.escalationReason;
129
+ }
130
+ // Track timing milestones
131
+ const now = new Date();
132
+ if (['payment_attempt', 'acp_payment'].includes(event.eventType) && !session.paymentAttemptedAt) {
133
+ updates.paymentAttemptedAt = now;
134
+ }
135
+ if (['order_created', 'order', 'acp_order'].includes(event.eventType) || event.checkoutState === 'completed') {
136
+ updates.completedAt = now;
137
+ if (session.checkoutCreatedAt) {
138
+ updates.timeToPurchaseMs = now.getTime() - session.checkoutCreatedAt.getTime();
139
+ }
140
+ }
141
+ await db
142
+ .update(schema_js_1.analyticsCheckoutSessions)
143
+ .set(updates)
144
+ .where((0, drizzle_orm_1.eq)(schema_js_1.analyticsCheckoutSessions.id, session.id));
145
+ }
146
+ else {
147
+ await db.insert(schema_js_1.analyticsCheckoutSessions).values({
148
+ domainId,
149
+ checkoutSessionId: event.checkoutSessionId,
150
+ agentType: detection.type,
151
+ agentProfileUrl: event.agentProfileUrl,
152
+ protocol: event.protocol || 'unknown',
153
+ currentState: event.checkoutState || 'incomplete',
154
+ lineItemsCount: event.lineItemsCount,
155
+ orderTotal: event.orderTotal ? String(event.orderTotal) : undefined,
156
+ currency: event.currency,
157
+ paymentHandler: event.paymentHandler,
158
+ checkoutCreatedAt: new Date(),
159
+ });
160
+ }
161
+ }
162
+ catch (error) {
163
+ // Don't fail event ingestion if session tracking fails
164
+ console.error('[Analytics] Checkout session upsert error:', error);
165
+ }
166
+ }
167
+ /**
168
+ * Ingest analytics events (V1 + V2)
169
+ */
170
+ async function ingestEvents(domainId, events) {
171
+ const db = (0, index_js_1.getDb)();
172
+ const errors = [];
173
+ let received = 0;
174
+ for (const event of events) {
175
+ try {
176
+ const detection = detectAgentType(event.userAgent);
177
+ const newEvent = {
178
+ domainId,
179
+ eventType: event.eventType,
180
+ agentType: detection.type,
181
+ agentVersion: detection.version,
182
+ userAgent: event.userAgent,
183
+ endpoint: event.endpoint,
184
+ capability: event.capability,
185
+ httpStatus: event.httpStatus,
186
+ responseTimeMs: event.responseTimeMs,
187
+ sessionId: event.sessionId,
188
+ ipHash: event.ipAddress ? hashIpAddress(event.ipAddress) : undefined,
189
+ countryCode: event.countryCode?.toUpperCase().substring(0, 2),
190
+ metadata: event.metadata ? JSON.stringify(event.metadata) : undefined,
191
+ // V2 fields
192
+ protocol: event.protocol,
193
+ checkoutSessionId: event.checkoutSessionId,
194
+ checkoutState: event.checkoutState,
195
+ lineItemsCount: event.lineItemsCount,
196
+ orderTotal: event.orderTotal ? String(event.orderTotal) : undefined,
197
+ currency: event.currency,
198
+ paymentHandler: event.paymentHandler,
199
+ escalationReason: event.escalationReason,
200
+ agentProfileUrl: event.agentProfileUrl,
201
+ };
202
+ await db.insert(schema_js_1.analyticsEvents).values(newEvent);
203
+ received++;
204
+ // Track checkout sessions for V2 funnel correlation
205
+ if (event.checkoutSessionId && CHECKOUT_EVENT_TYPES.has(event.eventType)) {
206
+ await upsertCheckoutSession(domainId, event);
207
+ }
208
+ }
209
+ catch (error) {
210
+ errors.push(`Failed to insert event: ${error instanceof Error ? error.message : 'Unknown error'}`);
211
+ }
212
+ }
213
+ return { received, errors: errors.length > 0 ? errors : [] };
214
+ }
215
+ /**
216
+ * Get analytics summary for a domain
217
+ */
218
+ async function getAnalyticsSummary(domainId, startDate, endDate) {
219
+ const db = (0, index_js_1.getDb)();
220
+ // Get total events and basic stats
221
+ const statsResult = await db
222
+ .select({
223
+ totalEvents: (0, drizzle_orm_1.count)(),
224
+ avgResponseTimeMs: (0, drizzle_orm_1.sql) `COALESCE(AVG(${schema_js_1.analyticsEvents.responseTimeMs}), 0)`,
225
+ errorCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsEvents.httpStatus} >= 400)`,
226
+ })
227
+ .from(schema_js_1.analyticsEvents)
228
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate)));
229
+ const stats = statsResult[0];
230
+ const totalEvents = Number(stats?.totalEvents) || 0;
231
+ const avgResponseTimeMs = Math.round(Number(stats?.avgResponseTimeMs) || 0);
232
+ const errorCount = Number(stats?.errorCount) || 0;
233
+ const errorRate = totalEvents > 0 ? (errorCount / totalEvents) * 100 : 0;
234
+ // Get unique sessions
235
+ const sessionsResult = await db
236
+ .select({
237
+ count: (0, drizzle_orm_1.sql) `COUNT(DISTINCT ${schema_js_1.analyticsEvents.sessionId})`,
238
+ })
239
+ .from(schema_js_1.analyticsEvents)
240
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate), (0, drizzle_orm_1.sql) `${schema_js_1.analyticsEvents.sessionId} IS NOT NULL`));
241
+ const uniqueSessions = Number(sessionsResult[0]?.count) || 0;
242
+ // Get unique agent types
243
+ const agentsResult = await db
244
+ .select({
245
+ count: (0, drizzle_orm_1.sql) `COUNT(DISTINCT ${schema_js_1.analyticsEvents.agentType})`,
246
+ })
247
+ .from(schema_js_1.analyticsEvents)
248
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate)));
249
+ const uniqueAgents = Number(agentsResult[0]?.count) || 0;
250
+ // Events by type
251
+ const eventsByTypeResult = await db
252
+ .select({
253
+ eventType: schema_js_1.analyticsEvents.eventType,
254
+ count: (0, drizzle_orm_1.count)(),
255
+ })
256
+ .from(schema_js_1.analyticsEvents)
257
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate)))
258
+ .groupBy(schema_js_1.analyticsEvents.eventType);
259
+ const eventsByType = {};
260
+ for (const row of eventsByTypeResult) {
261
+ eventsByType[row.eventType] = Number(row.count);
262
+ }
263
+ // Events by agent
264
+ const eventsByAgentResult = await db
265
+ .select({
266
+ agentType: schema_js_1.analyticsEvents.agentType,
267
+ count: (0, drizzle_orm_1.count)(),
268
+ })
269
+ .from(schema_js_1.analyticsEvents)
270
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate)))
271
+ .groupBy(schema_js_1.analyticsEvents.agentType);
272
+ const eventsByAgent = {};
273
+ for (const row of eventsByAgentResult) {
274
+ eventsByAgent[row.agentType || 'unknown'] = Number(row.count);
275
+ }
276
+ // Top capabilities
277
+ const topCapabilitiesResult = await db
278
+ .select({
279
+ capability: schema_js_1.analyticsEvents.capability,
280
+ count: (0, drizzle_orm_1.count)(),
281
+ })
282
+ .from(schema_js_1.analyticsEvents)
283
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate), (0, drizzle_orm_1.sql) `${schema_js_1.analyticsEvents.capability} IS NOT NULL`))
284
+ .groupBy(schema_js_1.analyticsEvents.capability)
285
+ .orderBy((0, drizzle_orm_1.desc)((0, drizzle_orm_1.count)()))
286
+ .limit(10);
287
+ const topCapabilities = topCapabilitiesResult.map(row => ({
288
+ capability: row.capability,
289
+ count: Number(row.count),
290
+ }));
291
+ // Top endpoints
292
+ const topEndpointsResult = await db
293
+ .select({
294
+ endpoint: schema_js_1.analyticsEvents.endpoint,
295
+ count: (0, drizzle_orm_1.count)(),
296
+ })
297
+ .from(schema_js_1.analyticsEvents)
298
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate), (0, drizzle_orm_1.sql) `${schema_js_1.analyticsEvents.endpoint} IS NOT NULL`))
299
+ .groupBy(schema_js_1.analyticsEvents.endpoint)
300
+ .orderBy((0, drizzle_orm_1.desc)((0, drizzle_orm_1.count)()))
301
+ .limit(10);
302
+ const topEndpoints = topEndpointsResult.map(row => ({
303
+ endpoint: row.endpoint,
304
+ count: Number(row.count),
305
+ }));
306
+ return {
307
+ totalEvents,
308
+ uniqueSessions,
309
+ uniqueAgents,
310
+ avgResponseTimeMs,
311
+ errorRate: Math.round(errorRate * 100) / 100,
312
+ eventsByType,
313
+ eventsByAgent,
314
+ topCapabilities,
315
+ topEndpoints,
316
+ };
317
+ }
318
+ /**
319
+ * Get timeseries data for a domain
320
+ */
321
+ async function getTimeseries(domainId, startDate, endDate, granularity = 'day') {
322
+ const db = (0, index_js_1.getDb)();
323
+ // Determine date truncation based on granularity
324
+ const truncate = granularity === 'hour' ? 'hour' : granularity === 'week' ? 'week' : 'day';
325
+ const result = await db
326
+ .select({
327
+ timestamp: (0, drizzle_orm_1.sql) `date_trunc('${drizzle_orm_1.sql.raw(truncate)}', ${schema_js_1.analyticsEvents.createdAt})::text`,
328
+ eventCount: (0, drizzle_orm_1.count)(),
329
+ errorCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsEvents.httpStatus} >= 400)`,
330
+ avgResponseTimeMs: (0, drizzle_orm_1.sql) `COALESCE(AVG(${schema_js_1.analyticsEvents.responseTimeMs}), 0)`,
331
+ uniqueSessions: (0, drizzle_orm_1.sql) `COUNT(DISTINCT ${schema_js_1.analyticsEvents.sessionId})`,
332
+ })
333
+ .from(schema_js_1.analyticsEvents)
334
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate)))
335
+ .groupBy((0, drizzle_orm_1.sql) `date_trunc('${drizzle_orm_1.sql.raw(truncate)}', ${schema_js_1.analyticsEvents.createdAt})`)
336
+ .orderBy((0, drizzle_orm_1.sql) `date_trunc('${drizzle_orm_1.sql.raw(truncate)}', ${schema_js_1.analyticsEvents.createdAt})`);
337
+ const data = result.map(row => ({
338
+ timestamp: row.timestamp,
339
+ eventCount: Number(row.eventCount),
340
+ uniqueSessions: Number(row.uniqueSessions),
341
+ errorCount: Number(row.errorCount),
342
+ avgResponseTimeMs: Math.round(Number(row.avgResponseTimeMs)),
343
+ }));
344
+ return {
345
+ granularity,
346
+ startDate: startDate.toISOString(),
347
+ endDate: endDate.toISOString(),
348
+ data,
349
+ };
350
+ }
351
+ /**
352
+ * Get event log with pagination
353
+ */
354
+ async function getEventLog(domainId, filters, pagination) {
355
+ const db = (0, index_js_1.getDb)();
356
+ // Build conditions
357
+ const conditions = [(0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId)];
358
+ if (filters.eventType) {
359
+ conditions.push((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.eventType, filters.eventType));
360
+ }
361
+ if (filters.agentType) {
362
+ conditions.push((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.agentType, filters.agentType));
363
+ }
364
+ if (filters.startDate) {
365
+ conditions.push((0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, filters.startDate));
366
+ }
367
+ if (filters.endDate) {
368
+ conditions.push((0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, filters.endDate));
369
+ }
370
+ if (filters.capability) {
371
+ conditions.push((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.capability, filters.capability));
372
+ }
373
+ if (filters.hasError === true) {
374
+ conditions.push((0, drizzle_orm_1.sql) `${schema_js_1.analyticsEvents.httpStatus} >= 400`);
375
+ }
376
+ else if (filters.hasError === false) {
377
+ conditions.push((0, drizzle_orm_1.sql) `${schema_js_1.analyticsEvents.httpStatus} < 400 OR ${schema_js_1.analyticsEvents.httpStatus} IS NULL`);
378
+ }
379
+ const whereClause = (0, drizzle_orm_1.and)(...conditions);
380
+ // Get total count
381
+ const countResult = await db
382
+ .select({ total: (0, drizzle_orm_1.count)() })
383
+ .from(schema_js_1.analyticsEvents)
384
+ .where(whereClause);
385
+ const total = Number(countResult[0]?.total) || 0;
386
+ // Get events (including V2 fields)
387
+ const events = await db
388
+ .select({
389
+ id: schema_js_1.analyticsEvents.id,
390
+ eventType: schema_js_1.analyticsEvents.eventType,
391
+ agentType: schema_js_1.analyticsEvents.agentType,
392
+ agentVersion: schema_js_1.analyticsEvents.agentVersion,
393
+ endpoint: schema_js_1.analyticsEvents.endpoint,
394
+ capability: schema_js_1.analyticsEvents.capability,
395
+ httpStatus: schema_js_1.analyticsEvents.httpStatus,
396
+ responseTimeMs: schema_js_1.analyticsEvents.responseTimeMs,
397
+ countryCode: schema_js_1.analyticsEvents.countryCode,
398
+ createdAt: schema_js_1.analyticsEvents.createdAt,
399
+ protocol: schema_js_1.analyticsEvents.protocol,
400
+ checkoutSessionId: schema_js_1.analyticsEvents.checkoutSessionId,
401
+ checkoutState: schema_js_1.analyticsEvents.checkoutState,
402
+ orderTotal: schema_js_1.analyticsEvents.orderTotal,
403
+ currency: schema_js_1.analyticsEvents.currency,
404
+ })
405
+ .from(schema_js_1.analyticsEvents)
406
+ .where(whereClause)
407
+ .orderBy((0, drizzle_orm_1.desc)(schema_js_1.analyticsEvents.createdAt))
408
+ .limit(pagination.limit)
409
+ .offset(pagination.offset);
410
+ return {
411
+ events: events,
412
+ total,
413
+ limit: pagination.limit,
414
+ offset: pagination.offset,
415
+ };
416
+ }
417
+ // ============================================================================
418
+ // V2: Funnel, Commerce KPIs, Agent Comparison, Error Analysis, Capability Gaps
419
+ // ============================================================================
420
+ /**
421
+ * Get conversion funnel data
422
+ */
423
+ async function getFunnelData(domainId, startDate, endDate) {
424
+ const db = (0, index_js_1.getDb)();
425
+ const stages = [];
426
+ for (const funnelStage of FUNNEL_STAGES) {
427
+ // Build IN clause for event types
428
+ const typePlaceholders = funnelStage.types.map(t => `'${t}'`).join(',');
429
+ const result = await db
430
+ .select({
431
+ count: (0, drizzle_orm_1.sql) `COUNT(DISTINCT COALESCE(${schema_js_1.analyticsEvents.checkoutSessionId}, ${schema_js_1.analyticsEvents.sessionId}, ${schema_js_1.analyticsEvents.ipHash}))`,
432
+ })
433
+ .from(schema_js_1.analyticsEvents)
434
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate), (0, drizzle_orm_1.sql) `${schema_js_1.analyticsEvents.eventType} IN (${drizzle_orm_1.sql.raw(typePlaceholders)})`));
435
+ stages.push({
436
+ stage: funnelStage.stage,
437
+ count: Number(result[0]?.count) || 0,
438
+ conversionRate: 0,
439
+ dropOffRate: 0,
440
+ });
441
+ }
442
+ // Calculate conversion rates
443
+ for (let i = 0; i < stages.length; i++) {
444
+ if (i === 0) {
445
+ stages[i].conversionRate = 100;
446
+ stages[i].dropOffRate = 0;
447
+ }
448
+ else {
449
+ const prevCount = stages[i - 1].count;
450
+ stages[i].conversionRate = prevCount > 0
451
+ ? Math.round((stages[i].count / prevCount) * 10000) / 100
452
+ : 0;
453
+ stages[i].dropOffRate = prevCount > 0
454
+ ? Math.round(((prevCount - stages[i].count) / prevCount) * 10000) / 100
455
+ : 0;
456
+ }
457
+ }
458
+ const overallConversionRate = stages[0].count > 0
459
+ ? Math.round((stages[stages.length - 1].count / stages[0].count) * 10000) / 100
460
+ : 0;
461
+ return {
462
+ stages,
463
+ overallConversionRate,
464
+ timeRange: { startDate: startDate.toISOString(), endDate: endDate.toISOString() },
465
+ };
466
+ }
467
+ /**
468
+ * Get commerce KPIs from checkout sessions
469
+ */
470
+ async function getCommerceKPIs(domainId, startDate, endDate) {
471
+ const db = (0, index_js_1.getDb)();
472
+ const orderStats = await db
473
+ .select({
474
+ totalRevenue: (0, drizzle_orm_1.sql) `COALESCE(SUM(${schema_js_1.analyticsCheckoutSessions.orderTotal}::numeric), 0)`,
475
+ orderCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsCheckoutSessions.currentState} = 'completed')`,
476
+ checkoutCount: (0, drizzle_orm_1.count)(),
477
+ escalationCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsCheckoutSessions.hadEscalation} = true)`,
478
+ avgTimeToPurchase: (0, drizzle_orm_1.sql) `AVG(${schema_js_1.analyticsCheckoutSessions.timeToPurchaseMs})`,
479
+ ucpCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsCheckoutSessions.protocol} = 'ucp')`,
480
+ acpCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsCheckoutSessions.protocol} = 'acp')`,
481
+ unknownCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsCheckoutSessions.protocol} NOT IN ('ucp', 'acp'))`,
482
+ })
483
+ .from(schema_js_1.analyticsCheckoutSessions)
484
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsCheckoutSessions.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsCheckoutSessions.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsCheckoutSessions.createdAt, endDate)));
485
+ const stats = orderStats[0];
486
+ const totalRevenue = Number(stats?.totalRevenue) || 0;
487
+ const orderCount = Number(stats?.orderCount) || 0;
488
+ const checkoutCount = Number(stats?.checkoutCount) || 0;
489
+ const escalationCount = Number(stats?.escalationCount) || 0;
490
+ // Payment handler distribution
491
+ const paymentHandlers = await db
492
+ .select({
493
+ handler: schema_js_1.analyticsCheckoutSessions.paymentHandler,
494
+ count: (0, drizzle_orm_1.count)(),
495
+ })
496
+ .from(schema_js_1.analyticsCheckoutSessions)
497
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsCheckoutSessions.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsCheckoutSessions.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsCheckoutSessions.createdAt, endDate), (0, drizzle_orm_1.sql) `${schema_js_1.analyticsCheckoutSessions.paymentHandler} IS NOT NULL`))
498
+ .groupBy(schema_js_1.analyticsCheckoutSessions.paymentHandler);
499
+ const paymentHandlerDistribution = {};
500
+ for (const row of paymentHandlers) {
501
+ paymentHandlerDistribution[row.handler || 'unknown'] = Number(row.count);
502
+ }
503
+ return {
504
+ agenticRevenue: totalRevenue,
505
+ agenticConversionRate: checkoutCount > 0
506
+ ? Math.round((orderCount / checkoutCount) * 10000) / 100
507
+ : 0,
508
+ averageOrderValue: orderCount > 0
509
+ ? Math.round((totalRevenue / orderCount) * 100) / 100
510
+ : 0,
511
+ escalationRate: checkoutCount > 0
512
+ ? Math.round((escalationCount / checkoutCount) * 10000) / 100
513
+ : 0,
514
+ checkoutAbandonmentRate: checkoutCount > 0
515
+ ? Math.round(((checkoutCount - orderCount) / checkoutCount) * 10000) / 100
516
+ : 0,
517
+ timeToPurchaseMs: Math.round(Number(stats?.avgTimeToPurchase) || 0),
518
+ protocolDistribution: {
519
+ ucp: Number(stats?.ucpCount) || 0,
520
+ acp: Number(stats?.acpCount) || 0,
521
+ unknown: Number(stats?.unknownCount) || 0,
522
+ },
523
+ paymentHandlerDistribution,
524
+ };
525
+ }
526
+ /**
527
+ * Get agent comparison data
528
+ */
529
+ async function getAgentComparison(domainId, startDate, endDate) {
530
+ const db = (0, index_js_1.getDb)();
531
+ // Per-agent event counts by type
532
+ const agentStats = await db
533
+ .select({
534
+ agentType: schema_js_1.analyticsEvents.agentType,
535
+ totalEvents: (0, drizzle_orm_1.count)(),
536
+ discoveries: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsEvents.eventType} = 'discovery')`,
537
+ checkouts: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsEvents.eventType} IN ('checkout', 'checkout_create', 'acp_checkout_create'))`,
538
+ orders: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsEvents.eventType} IN ('order', 'order_created', 'acp_order'))`,
539
+ errorCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsEvents.httpStatus} >= 400)`,
540
+ avgResponseTimeMs: (0, drizzle_orm_1.sql) `AVG(${schema_js_1.analyticsEvents.responseTimeMs})`,
541
+ })
542
+ .from(schema_js_1.analyticsEvents)
543
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate)))
544
+ .groupBy(schema_js_1.analyticsEvents.agentType);
545
+ // Per-agent revenue from checkout sessions
546
+ const agentRevenue = await db
547
+ .select({
548
+ agentType: schema_js_1.analyticsCheckoutSessions.agentType,
549
+ totalRevenue: (0, drizzle_orm_1.sql) `COALESCE(SUM(${schema_js_1.analyticsCheckoutSessions.orderTotal}::numeric), 0)`,
550
+ orderCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsCheckoutSessions.currentState} = 'completed')`,
551
+ })
552
+ .from(schema_js_1.analyticsCheckoutSessions)
553
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsCheckoutSessions.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsCheckoutSessions.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsCheckoutSessions.createdAt, endDate)))
554
+ .groupBy(schema_js_1.analyticsCheckoutSessions.agentType);
555
+ const revenueMap = new Map(agentRevenue.map(r => [r.agentType, r]));
556
+ const agents = agentStats.map(stat => {
557
+ const totalEvents = Number(stat.totalEvents);
558
+ const orders = Number(stat.orders);
559
+ const checkouts = Number(stat.checkouts);
560
+ const rev = revenueMap.get(stat.agentType);
561
+ const revenue = Number(rev?.totalRevenue) || 0;
562
+ const revOrderCount = Number(rev?.orderCount) || 0;
563
+ return {
564
+ agentType: stat.agentType || 'unknown',
565
+ totalEvents,
566
+ discoveries: Number(stat.discoveries),
567
+ checkouts,
568
+ orders,
569
+ conversionRate: checkouts > 0
570
+ ? Math.round((orders / checkouts) * 10000) / 100
571
+ : 0,
572
+ avgOrderValue: revOrderCount > 0
573
+ ? Math.round((revenue / revOrderCount) * 100) / 100
574
+ : 0,
575
+ errorRate: totalEvents > 0
576
+ ? Math.round((Number(stat.errorCount) / totalEvents) * 10000) / 100
577
+ : 0,
578
+ avgResponseTimeMs: Math.round(Number(stat.avgResponseTimeMs) || 0),
579
+ };
580
+ });
581
+ return { agents: agents.sort((a, b) => b.totalEvents - a.totalEvents) };
582
+ }
583
+ /**
584
+ * Get error analysis data
585
+ */
586
+ async function getErrorAnalysis(domainId, startDate, endDate) {
587
+ const db = (0, index_js_1.getDb)();
588
+ // Total events and errors
589
+ const totalResult = await db
590
+ .select({
591
+ totalErrors: (0, drizzle_orm_1.count)(),
592
+ })
593
+ .from(schema_js_1.analyticsEvents)
594
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate), (0, drizzle_orm_1.sql) `${schema_js_1.analyticsEvents.httpStatus} >= 400`));
595
+ const totalAllResult = await db
596
+ .select({
597
+ total: (0, drizzle_orm_1.count)(),
598
+ })
599
+ .from(schema_js_1.analyticsEvents)
600
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate)));
601
+ const totalErrors = Number(totalResult[0]?.totalErrors) || 0;
602
+ const totalAll = Number(totalAllResult[0]?.total) || 0;
603
+ // Group errors by HTTP status
604
+ const errorGroups = await db
605
+ .select({
606
+ httpStatus: schema_js_1.analyticsEvents.httpStatus,
607
+ count: (0, drizzle_orm_1.count)(),
608
+ endpoints: (0, drizzle_orm_1.sql) `array_agg(DISTINCT ${schema_js_1.analyticsEvents.endpoint})`,
609
+ agents: (0, drizzle_orm_1.sql) `array_agg(DISTINCT ${schema_js_1.analyticsEvents.agentType})`,
610
+ })
611
+ .from(schema_js_1.analyticsEvents)
612
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate), (0, drizzle_orm_1.sql) `${schema_js_1.analyticsEvents.httpStatus} >= 400`))
613
+ .groupBy(schema_js_1.analyticsEvents.httpStatus)
614
+ .orderBy((0, drizzle_orm_1.desc)((0, drizzle_orm_1.count)()))
615
+ .limit(20);
616
+ const errors = errorGroups.map(group => {
617
+ const statusCode = group.httpStatus || 0;
618
+ const groupCount = Number(group.count);
619
+ // Filter out null values from arrays
620
+ const endpoints = Array.isArray(group.endpoints)
621
+ ? group.endpoints.filter(Boolean)
622
+ : [];
623
+ const agents = Array.isArray(group.agents)
624
+ ? group.agents.filter(Boolean)
625
+ : [];
626
+ return {
627
+ errorCode: String(statusCode),
628
+ count: groupCount,
629
+ percentage: totalErrors > 0
630
+ ? Math.round((groupCount / totalErrors) * 10000) / 100
631
+ : 0,
632
+ suggestion: ERROR_SUGGESTIONS[statusCode] || 'Review server logs for this error type',
633
+ affectedEndpoints: endpoints,
634
+ affectedAgents: agents,
635
+ };
636
+ });
637
+ return {
638
+ totalErrors,
639
+ errorRate: totalAll > 0
640
+ ? Math.round((totalErrors / totalAll) * 10000) / 100
641
+ : 0,
642
+ errors,
643
+ };
644
+ }
645
+ /**
646
+ * Get capability gap analysis
647
+ */
648
+ async function getCapabilityGaps(domainId, startDate, endDate) {
649
+ const db = (0, index_js_1.getDb)();
650
+ const capStats = await db
651
+ .select({
652
+ capability: schema_js_1.analyticsEvents.capability,
653
+ totalRequests: (0, drizzle_orm_1.count)(),
654
+ errorCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsEvents.httpStatus} >= 400)`,
655
+ })
656
+ .from(schema_js_1.analyticsEvents)
657
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsEvents.domainId, domainId), (0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startDate), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endDate), (0, drizzle_orm_1.sql) `${schema_js_1.analyticsEvents.capability} IS NOT NULL`))
658
+ .groupBy(schema_js_1.analyticsEvents.capability)
659
+ .orderBy((0, drizzle_orm_1.desc)((0, drizzle_orm_1.count)()));
660
+ const gaps = capStats.map(stat => {
661
+ const total = Number(stat.totalRequests);
662
+ const errors = Number(stat.errorCount);
663
+ const errorRate = total > 0 ? errors / total : 0;
664
+ return {
665
+ requestedCapability: stat.capability,
666
+ requestCount: total,
667
+ supported: errorRate < 0.5,
668
+ suggestedAction: errorRate >= 0.5
669
+ ? `Agents requested "${stat.capability}" ${total} times but got ${Math.round(errorRate * 100)}% errors. Consider adding this capability.`
670
+ : `"${stat.capability}" is working well with ${Math.round((1 - errorRate) * 100)}% success rate.`,
671
+ };
672
+ });
673
+ const totalRequests = gaps.reduce((sum, g) => sum + g.requestCount, 0);
674
+ const supportedRequests = gaps
675
+ .filter(g => g.supported)
676
+ .reduce((sum, g) => sum + g.requestCount, 0);
677
+ return {
678
+ gaps,
679
+ matchRate: totalRequests > 0
680
+ ? Math.round((supportedRequests / totalRequests) * 10000) / 100
681
+ : 100,
682
+ };
683
+ }
684
+ // ============================================================================
685
+ // API Key Management (unchanged from V1)
686
+ // ============================================================================
687
+ /**
688
+ * Create an analytics API key for a domain
689
+ */
690
+ async function createAnalyticsApiKey(domainId, name) {
691
+ const db = (0, index_js_1.getDb)();
692
+ // Generate key: ucpa_<random>
693
+ const key = `ucpa_${(0, nanoid_1.nanoid)(32)}`;
694
+ const keyHash = await (0, bcryptjs_1.hash)(key, 10);
695
+ const keyPrefix = key.substring(0, 12);
696
+ const [apiKey] = await db
697
+ .insert(schema_js_1.analyticsApiKeys)
698
+ .values({
699
+ domainId,
700
+ keyHash,
701
+ keyPrefix,
702
+ name,
703
+ })
704
+ .returning();
705
+ return {
706
+ key,
707
+ apiKey: {
708
+ id: apiKey.id,
709
+ name: apiKey.name,
710
+ keyPrefix: apiKey.keyPrefix,
711
+ lastUsedAt: apiKey.lastUsedAt,
712
+ isActive: apiKey.isActive ?? true,
713
+ createdAt: apiKey.createdAt,
714
+ },
715
+ };
716
+ }
717
+ /**
718
+ * Validate an analytics API key
719
+ */
720
+ async function validateAnalyticsApiKey(key) {
721
+ const db = (0, index_js_1.getDb)();
722
+ // Extract prefix
723
+ const keyPrefix = key.substring(0, 12);
724
+ // Find keys with matching prefix
725
+ const keys = await db
726
+ .select()
727
+ .from(schema_js_1.analyticsApiKeys)
728
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsApiKeys.keyPrefix, keyPrefix), (0, drizzle_orm_1.eq)(schema_js_1.analyticsApiKeys.isActive, true)));
729
+ for (const apiKey of keys) {
730
+ const valid = await (0, bcryptjs_1.compare)(key, apiKey.keyHash);
731
+ if (valid) {
732
+ // Update last used
733
+ await db
734
+ .update(schema_js_1.analyticsApiKeys)
735
+ .set({ lastUsedAt: new Date() })
736
+ .where((0, drizzle_orm_1.eq)(schema_js_1.analyticsApiKeys.id, apiKey.id));
737
+ return { valid: true, domainId: apiKey.domainId, keyId: apiKey.id };
738
+ }
739
+ }
740
+ return { valid: false };
741
+ }
742
+ /**
743
+ * Get analytics API keys for a domain
744
+ */
745
+ async function getAnalyticsApiKeys(domainId) {
746
+ const db = (0, index_js_1.getDb)();
747
+ const keys = await db
748
+ .select({
749
+ id: schema_js_1.analyticsApiKeys.id,
750
+ name: schema_js_1.analyticsApiKeys.name,
751
+ keyPrefix: schema_js_1.analyticsApiKeys.keyPrefix,
752
+ lastUsedAt: schema_js_1.analyticsApiKeys.lastUsedAt,
753
+ isActive: schema_js_1.analyticsApiKeys.isActive,
754
+ createdAt: schema_js_1.analyticsApiKeys.createdAt,
755
+ })
756
+ .from(schema_js_1.analyticsApiKeys)
757
+ .where((0, drizzle_orm_1.eq)(schema_js_1.analyticsApiKeys.domainId, domainId))
758
+ .orderBy((0, drizzle_orm_1.desc)(schema_js_1.analyticsApiKeys.createdAt));
759
+ return keys.map(k => ({
760
+ ...k,
761
+ isActive: k.isActive ?? true,
762
+ }));
763
+ }
764
+ /**
765
+ * Delete an analytics API key
766
+ */
767
+ async function deleteAnalyticsApiKey(keyId, domainId) {
768
+ const db = (0, index_js_1.getDb)();
769
+ await db
770
+ .delete(schema_js_1.analyticsApiKeys)
771
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsApiKeys.id, keyId), (0, drizzle_orm_1.eq)(schema_js_1.analyticsApiKeys.domainId, domainId)));
772
+ return true;
773
+ }
774
+ /**
775
+ * Aggregate events for a specific date (for cron job)
776
+ */
777
+ async function aggregateEventsForDate(date) {
778
+ const db = (0, index_js_1.getDb)();
779
+ // Get start and end of day
780
+ const startOfDay = new Date(date);
781
+ startOfDay.setHours(0, 0, 0, 0);
782
+ const endOfDay = new Date(date);
783
+ endOfDay.setHours(23, 59, 59, 999);
784
+ // Get aggregated data grouped by domain, event type, and agent type
785
+ const aggregates = await db
786
+ .select({
787
+ domainId: schema_js_1.analyticsEvents.domainId,
788
+ eventType: schema_js_1.analyticsEvents.eventType,
789
+ agentType: (0, drizzle_orm_1.sql) `COALESCE(${schema_js_1.analyticsEvents.agentType}, 'unknown')`,
790
+ eventCount: (0, drizzle_orm_1.count)(),
791
+ uniqueSessions: (0, drizzle_orm_1.sql) `COUNT(DISTINCT ${schema_js_1.analyticsEvents.sessionId})`,
792
+ avgResponseTimeMs: (0, drizzle_orm_1.sql) `AVG(${schema_js_1.analyticsEvents.responseTimeMs})`,
793
+ errorCount: (0, drizzle_orm_1.sql) `COUNT(*) FILTER (WHERE ${schema_js_1.analyticsEvents.httpStatus} >= 400)`,
794
+ })
795
+ .from(schema_js_1.analyticsEvents)
796
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.gte)(schema_js_1.analyticsEvents.createdAt, startOfDay), (0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, endOfDay)))
797
+ .groupBy(schema_js_1.analyticsEvents.domainId, schema_js_1.analyticsEvents.eventType, (0, drizzle_orm_1.sql) `COALESCE(${schema_js_1.analyticsEvents.agentType}, 'unknown')`);
798
+ // Insert or update aggregates
799
+ for (const agg of aggregates) {
800
+ const existing = await db
801
+ .select()
802
+ .from(schema_js_1.analyticsAggregates)
803
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.analyticsAggregates.domainId, agg.domainId), (0, drizzle_orm_1.eq)(schema_js_1.analyticsAggregates.date, startOfDay), (0, drizzle_orm_1.eq)(schema_js_1.analyticsAggregates.eventType, agg.eventType), (0, drizzle_orm_1.eq)(schema_js_1.analyticsAggregates.agentType, agg.agentType)))
804
+ .limit(1);
805
+ if (existing.length > 0) {
806
+ await db
807
+ .update(schema_js_1.analyticsAggregates)
808
+ .set({
809
+ eventCount: Number(agg.eventCount),
810
+ uniqueSessions: Number(agg.uniqueSessions),
811
+ avgResponseTimeMs: agg.avgResponseTimeMs ? Math.round(Number(agg.avgResponseTimeMs)) : null,
812
+ errorCount: Number(agg.errorCount),
813
+ updatedAt: new Date(),
814
+ })
815
+ .where((0, drizzle_orm_1.eq)(schema_js_1.analyticsAggregates.id, existing[0].id));
816
+ }
817
+ else {
818
+ await db.insert(schema_js_1.analyticsAggregates).values({
819
+ domainId: agg.domainId,
820
+ date: startOfDay,
821
+ eventType: agg.eventType,
822
+ agentType: agg.agentType,
823
+ eventCount: Number(agg.eventCount),
824
+ uniqueSessions: Number(agg.uniqueSessions),
825
+ avgResponseTimeMs: agg.avgResponseTimeMs ? Math.round(Number(agg.avgResponseTimeMs)) : null,
826
+ errorCount: Number(agg.errorCount),
827
+ });
828
+ }
829
+ }
830
+ }
831
+ /**
832
+ * Purge old events (for data retention)
833
+ */
834
+ async function purgeOldEvents(retentionDays) {
835
+ const db = (0, index_js_1.getDb)();
836
+ const cutoffDate = new Date();
837
+ cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
838
+ await db
839
+ .delete(schema_js_1.analyticsEvents)
840
+ .where((0, drizzle_orm_1.lte)(schema_js_1.analyticsEvents.createdAt, cutoffDate));
841
+ return 0;
842
+ }
843
+ /**
844
+ * Get domain ID from analytics API key (for route validation)
845
+ */
846
+ async function getDomainFromApiKey(key) {
847
+ const result = await validateAnalyticsApiKey(key);
848
+ return result.valid ? result.domainId || null : null;
849
+ }
850
+ /**
851
+ * Check if user owns a domain (for dashboard access)
852
+ */
853
+ async function userOwnsDomain(userId, domainId) {
854
+ const db = (0, index_js_1.getDb)();
855
+ const result = await db
856
+ .select()
857
+ .from(schema_js_1.monitoredDomains)
858
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.id, domainId), (0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.userId, userId), (0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.isActive, true)))
859
+ .limit(1);
860
+ return result.length > 0;
861
+ }
862
+ //# sourceMappingURL=analytics.js.map