@ucptools/validator 1.0.1 → 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.
- package/dist/auth/config.d.ts +20 -0
- package/dist/auth/config.d.ts.map +1 -0
- package/dist/auth/config.js +114 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +17 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +45 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +170 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/service.d.ts +80 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +298 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/cli/index.js +96 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mock-server.d.ts +20 -0
- package/dist/cli/mock-server.d.ts.map +1 -0
- package/dist/cli/mock-server.js +261 -0
- package/dist/cli/mock-server.js.map +1 -0
- package/dist/db/index.d.ts +8 -2
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +22 -5
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema.d.ts +3570 -128
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +377 -17
- package/dist/db/schema.js.map +1 -1
- package/dist/db/utils.d.ts +252 -0
- package/dist/db/utils.d.ts.map +1 -0
- package/dist/db/utils.js +295 -0
- package/dist/db/utils.js.map +1 -0
- package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -1
- package/dist/feed-analyzer/feed-analyzer.js +218 -4
- package/dist/feed-analyzer/feed-analyzer.js.map +1 -1
- package/dist/feed-analyzer/types.d.ts +82 -1
- package/dist/feed-analyzer/types.d.ts.map +1 -1
- package/dist/feed-analyzer/types.js +13 -0
- package/dist/feed-analyzer/types.js.map +1 -1
- package/dist/lib/analytics.d.ts +337 -0
- package/dist/lib/analytics.d.ts.map +1 -0
- package/dist/lib/analytics.js +188 -0
- package/dist/lib/analytics.js.map +1 -0
- package/dist/security/security-scanner.d.ts.map +1 -1
- package/dist/security/security-scanner.js +130 -2
- package/dist/security/security-scanner.js.map +1 -1
- package/dist/security/types.d.ts +32 -0
- package/dist/security/types.d.ts.map +1 -1
- package/dist/security/types.js.map +1 -1
- package/dist/services/analytics.d.ts +114 -0
- package/dist/services/analytics.d.ts.map +1 -0
- package/dist/services/analytics.js +862 -0
- package/dist/services/analytics.js.map +1 -0
- package/dist/services/badge.d.ts +31 -0
- package/dist/services/badge.d.ts.map +1 -0
- package/dist/services/badge.js +152 -0
- package/dist/services/badge.js.map +1 -0
- package/dist/services/cron.d.ts +125 -0
- package/dist/services/cron.d.ts.map +1 -0
- package/dist/services/cron.js +613 -0
- package/dist/services/cron.js.map +1 -0
- package/dist/services/directory.d.ts +2 -0
- package/dist/services/directory.d.ts.map +1 -1
- package/dist/services/directory.js +45 -27
- package/dist/services/directory.js.map +1 -1
- package/dist/services/email.d.ts +112 -0
- package/dist/services/email.d.ts.map +1 -0
- package/dist/services/email.js +772 -0
- package/dist/services/email.js.map +1 -0
- package/dist/services/hosted-profiles.d.ts +77 -0
- package/dist/services/hosted-profiles.d.ts.map +1 -0
- package/dist/services/hosted-profiles.js +433 -0
- package/dist/services/hosted-profiles.js.map +1 -0
- package/dist/services/latency.d.ts +67 -0
- package/dist/services/latency.d.ts.map +1 -0
- package/dist/services/latency.js +274 -0
- package/dist/services/latency.js.map +1 -0
- package/dist/services/manifest-compliance.d.ts +64 -0
- package/dist/services/manifest-compliance.d.ts.map +1 -0
- package/dist/services/manifest-compliance.js +271 -0
- package/dist/services/manifest-compliance.js.map +1 -0
- package/dist/services/monitoring-diff.d.ts +31 -0
- package/dist/services/monitoring-diff.d.ts.map +1 -0
- package/dist/services/monitoring-diff.js +189 -0
- package/dist/services/monitoring-diff.js.map +1 -0
- package/dist/services/notifications.d.ts +46 -0
- package/dist/services/notifications.d.ts.map +1 -0
- package/dist/services/notifications.js +88 -0
- package/dist/services/notifications.js.map +1 -0
- package/dist/services/stripe.d.ts +93 -0
- package/dist/services/stripe.d.ts.map +1 -0
- package/dist/services/stripe.js +490 -0
- package/dist/services/stripe.js.map +1 -0
- package/dist/services/validation-history.d.ts +99 -0
- package/dist/services/validation-history.d.ts.map +1 -0
- package/dist/services/validation-history.js +344 -0
- package/dist/services/validation-history.js.map +1 -0
- package/dist/services/validation-logging.d.ts +103 -0
- package/dist/services/validation-logging.d.ts.map +1 -0
- package/dist/services/validation-logging.js +210 -0
- package/dist/services/validation-logging.js.map +1 -0
- package/dist/services/validation.d.ts +119 -0
- package/dist/services/validation.d.ts.map +1 -0
- package/dist/services/validation.js +1185 -0
- package/dist/services/validation.js.map +1 -0
- package/dist/simulator/agent-simulator.d.ts.map +1 -1
- package/dist/simulator/agent-simulator.js +229 -9
- package/dist/simulator/agent-simulator.js.map +1 -1
- package/dist/simulator/types.d.ts +26 -0
- package/dist/simulator/types.d.ts.map +1 -1
- package/dist/simulator/types.js.map +1 -1
- package/dist/types/acp-validation.d.ts +87 -0
- package/dist/types/acp-validation.d.ts.map +1 -0
- package/dist/types/acp-validation.js +40 -0
- package/dist/types/acp-validation.js.map +1 -0
- package/dist/types/analytics.d.ts +182 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/analytics.js +7 -0
- package/dist/types/analytics.js.map +1 -0
- package/dist/types/ucp-profile.d.ts +10 -2
- package/dist/types/ucp-profile.d.ts.map +1 -1
- package/dist/types/ucp-profile.js.map +1 -1
- package/dist/types/validation.d.ts +8 -0
- package/dist/types/validation.d.ts.map +1 -1
- package/dist/types/validation.js +10 -0
- package/dist/types/validation.js.map +1 -1
- package/dist/validator/acp/index.d.ts +31 -0
- package/dist/validator/acp/index.d.ts.map +1 -0
- package/dist/validator/acp/index.js +574 -0
- package/dist/validator/acp/index.js.map +1 -0
- package/dist/validator/network-validator.d.ts.map +1 -1
- package/dist/validator/network-validator.js +4 -4
- package/dist/validator/network-validator.js.map +1 -1
- package/dist/validator/rules-validator.d.ts +8 -0
- package/dist/validator/rules-validator.d.ts.map +1 -1
- package/dist/validator/rules-validator.js +92 -43
- package/dist/validator/rules-validator.js.map +1 -1
- package/dist/validator/structural-validator.d.ts.map +1 -1
- package/dist/validator/structural-validator.js +187 -53
- package/dist/validator/structural-validator.js.map +1 -1
- package/dist/validator/utils.d.ts +51 -0
- package/dist/validator/utils.d.ts.map +1 -0
- package/dist/validator/utils.js +132 -0
- package/dist/validator/utils.js.map +1 -0
- package/package.json +44 -12
- package/.claude/settings.local.json +0 -60
- package/.vercel/README.txt +0 -11
- package/.vercel/project.json +0 -1
- package/publish-output.txt +0 -0
- package/tsconfig.json +0 -20
|
@@ -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
|