@od-oneapp/analytics 2026.1.1301
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/README.md +509 -0
- package/dist/ai-YMnynb-t.mjs +3347 -0
- package/dist/ai-YMnynb-t.mjs.map +1 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client-CTzJVFU5.mjs +9 -0
- package/dist/client-CTzJVFU5.mjs.map +1 -0
- package/dist/client-CcFTauAh.mjs +54 -0
- package/dist/client-CcFTauAh.mjs.map +1 -0
- package/dist/client-CeOLjbac.mjs +281 -0
- package/dist/client-CeOLjbac.mjs.map +1 -0
- package/dist/client-D339NFJS.mjs +267 -0
- package/dist/client-D339NFJS.mjs.map +1 -0
- package/dist/client-next.d.mts +62 -0
- package/dist/client-next.d.mts.map +1 -0
- package/dist/client-next.mjs +525 -0
- package/dist/client-next.mjs.map +1 -0
- package/dist/client.d.mts +30 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +186 -0
- package/dist/client.mjs.map +1 -0
- package/dist/config-DPS6bSYo.d.mts +34 -0
- package/dist/config-DPS6bSYo.d.mts.map +1 -0
- package/dist/config-P6P5adJg.mjs +287 -0
- package/dist/config-P6P5adJg.mjs.map +1 -0
- package/dist/console-8bND3mMU.mjs +128 -0
- package/dist/console-8bND3mMU.mjs.map +1 -0
- package/dist/ecommerce-Cgu4wlux.mjs +993 -0
- package/dist/ecommerce-Cgu4wlux.mjs.map +1 -0
- package/dist/emitters-6-nKo8i-.mjs +208 -0
- package/dist/emitters-6-nKo8i-.mjs.map +1 -0
- package/dist/emitters-DldkVSPp.d.mts +12 -0
- package/dist/emitters-DldkVSPp.d.mts.map +1 -0
- package/dist/index-BfNWgfa5.d.mts +1494 -0
- package/dist/index-BfNWgfa5.d.mts.map +1 -0
- package/dist/index-BkIWe--N.d.mts +953 -0
- package/dist/index-BkIWe--N.d.mts.map +1 -0
- package/dist/index-jPzXRn52.d.mts +184 -0
- package/dist/index-jPzXRn52.d.mts.map +1 -0
- package/dist/manager-DvRRjza6.d.mts +76 -0
- package/dist/manager-DvRRjza6.d.mts.map +1 -0
- package/dist/posthog-bootstrap-CYfIy_WS.mjs +1769 -0
- package/dist/posthog-bootstrap-CYfIy_WS.mjs.map +1 -0
- package/dist/posthog-bootstrap-DWxFrxlt.d.mts +81 -0
- package/dist/posthog-bootstrap-DWxFrxlt.d.mts.map +1 -0
- package/dist/providers-http-client.d.mts +37 -0
- package/dist/providers-http-client.d.mts.map +1 -0
- package/dist/providers-http-client.mjs +320 -0
- package/dist/providers-http-client.mjs.map +1 -0
- package/dist/providers-http-server.d.mts +31 -0
- package/dist/providers-http-server.d.mts.map +1 -0
- package/dist/providers-http-server.mjs +297 -0
- package/dist/providers-http-server.mjs.map +1 -0
- package/dist/providers-http.d.mts +46 -0
- package/dist/providers-http.d.mts.map +1 -0
- package/dist/providers-http.mjs +4 -0
- package/dist/server-edge.d.mts +9 -0
- package/dist/server-edge.d.mts.map +1 -0
- package/dist/server-edge.mjs +373 -0
- package/dist/server-edge.mjs.map +1 -0
- package/dist/server-next.d.mts +67 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +193 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +10 -0
- package/dist/server.mjs +7 -0
- package/dist/service-cYtBBL8x.mjs +945 -0
- package/dist/service-cYtBBL8x.mjs.map +1 -0
- package/dist/shared.d.mts +16 -0
- package/dist/shared.d.mts.map +1 -0
- package/dist/shared.mjs +93 -0
- package/dist/shared.mjs.map +1 -0
- package/dist/types-BxBnNQ0V.d.mts +354 -0
- package/dist/types-BxBnNQ0V.d.mts.map +1 -0
- package/dist/types-CBvxUEaF.d.mts +216 -0
- package/dist/types-CBvxUEaF.d.mts.map +1 -0
- package/dist/types.d.mts +4 -0
- package/dist/types.mjs +0 -0
- package/dist/vercel-types-lwakUfoI.d.mts +102 -0
- package/dist/vercel-types-lwakUfoI.d.mts.map +1 -0
- package/package.json +129 -0
- package/src/client/index.ts +164 -0
- package/src/client/manager.ts +71 -0
- package/src/client/next/components.tsx +270 -0
- package/src/client/next/hooks.ts +217 -0
- package/src/client/next/manager.ts +141 -0
- package/src/client/next.ts +144 -0
- package/src/client-next.ts +101 -0
- package/src/client.ts +89 -0
- package/src/examples/ai-sdk-patterns.ts +583 -0
- package/src/examples/emitter-patterns.ts +476 -0
- package/src/examples/nextjs-emitter-patterns.tsx +403 -0
- package/src/next/app-router.tsx +564 -0
- package/src/next/client.ts +419 -0
- package/src/next/index.ts +84 -0
- package/src/next/middleware.ts +429 -0
- package/src/next/rsc.tsx +300 -0
- package/src/next/server.ts +253 -0
- package/src/next/types.d.ts +220 -0
- package/src/providers/base-provider.ts +419 -0
- package/src/providers/console/client.ts +10 -0
- package/src/providers/console/index.ts +152 -0
- package/src/providers/console/server.ts +6 -0
- package/src/providers/console/types.ts +15 -0
- package/src/providers/http/client.ts +464 -0
- package/src/providers/http/index.ts +30 -0
- package/src/providers/http/server.ts +396 -0
- package/src/providers/http/types.ts +135 -0
- package/src/providers/posthog/client.ts +518 -0
- package/src/providers/posthog/index.ts +11 -0
- package/src/providers/posthog/server.ts +329 -0
- package/src/providers/posthog/types.ts +104 -0
- package/src/providers/segment/client.ts +113 -0
- package/src/providers/segment/index.ts +11 -0
- package/src/providers/segment/server.ts +115 -0
- package/src/providers/segment/types.ts +51 -0
- package/src/providers/vercel/client.ts +102 -0
- package/src/providers/vercel/index.ts +11 -0
- package/src/providers/vercel/server.ts +89 -0
- package/src/providers/vercel/types.ts +27 -0
- package/src/server/index.ts +103 -0
- package/src/server/manager.ts +62 -0
- package/src/server/next.ts +210 -0
- package/src/server-edge.ts +442 -0
- package/src/server-next.ts +39 -0
- package/src/server.ts +106 -0
- package/src/shared/emitters/ai/README.md +981 -0
- package/src/shared/emitters/ai/events/agent.ts +130 -0
- package/src/shared/emitters/ai/events/artifacts.ts +167 -0
- package/src/shared/emitters/ai/events/chat.ts +126 -0
- package/src/shared/emitters/ai/events/chatbot-ecommerce.ts +133 -0
- package/src/shared/emitters/ai/events/completion.ts +103 -0
- package/src/shared/emitters/ai/events/content-generation.ts +347 -0
- package/src/shared/emitters/ai/events/conversation.ts +332 -0
- package/src/shared/emitters/ai/events/product-features.ts +1402 -0
- package/src/shared/emitters/ai/events/streaming.ts +114 -0
- package/src/shared/emitters/ai/events/tool.ts +93 -0
- package/src/shared/emitters/ai/index.ts +69 -0
- package/src/shared/emitters/ai/track-ai-sdk.ts +74 -0
- package/src/shared/emitters/ai/track-ai.ts +50 -0
- package/src/shared/emitters/ai/types.ts +1041 -0
- package/src/shared/emitters/ai/utils.ts +468 -0
- package/src/shared/emitters/ecommerce/events/cart-checkout.ts +106 -0
- package/src/shared/emitters/ecommerce/events/coupon.ts +49 -0
- package/src/shared/emitters/ecommerce/events/engagement.ts +61 -0
- package/src/shared/emitters/ecommerce/events/marketplace.ts +119 -0
- package/src/shared/emitters/ecommerce/events/order.ts +199 -0
- package/src/shared/emitters/ecommerce/events/product.ts +205 -0
- package/src/shared/emitters/ecommerce/events/registry.ts +123 -0
- package/src/shared/emitters/ecommerce/events/wishlist-sharing.ts +140 -0
- package/src/shared/emitters/ecommerce/index.ts +46 -0
- package/src/shared/emitters/ecommerce/track-ecommerce.ts +53 -0
- package/src/shared/emitters/ecommerce/types.ts +314 -0
- package/src/shared/emitters/ecommerce/utils.ts +216 -0
- package/src/shared/emitters/emitter-types.ts +974 -0
- package/src/shared/emitters/emitters.ts +292 -0
- package/src/shared/emitters/helpers.ts +419 -0
- package/src/shared/emitters/index.ts +66 -0
- package/src/shared/index.ts +142 -0
- package/src/shared/ingestion/index.ts +66 -0
- package/src/shared/ingestion/schemas.ts +386 -0
- package/src/shared/ingestion/service.ts +628 -0
- package/src/shared/node22-features.ts +848 -0
- package/src/shared/providers/console-provider.ts +160 -0
- package/src/shared/types/base-types.ts +54 -0
- package/src/shared/types/console-types.ts +19 -0
- package/src/shared/types/posthog-types.ts +131 -0
- package/src/shared/types/segment-types.ts +15 -0
- package/src/shared/types/types.ts +397 -0
- package/src/shared/types/vercel-types.ts +19 -0
- package/src/shared/utils/config-client.ts +19 -0
- package/src/shared/utils/config.ts +250 -0
- package/src/shared/utils/emitter-adapter.ts +212 -0
- package/src/shared/utils/manager.test.ts +36 -0
- package/src/shared/utils/manager.ts +1322 -0
- package/src/shared/utils/posthog-bootstrap.ts +136 -0
- package/src/shared/utils/posthog-client-utils.ts +48 -0
- package/src/shared/utils/posthog-next-utils.ts +282 -0
- package/src/shared/utils/posthog-server-utils.ts +210 -0
- package/src/shared/utils/rate-limit.ts +289 -0
- package/src/shared/utils/security.ts +545 -0
- package/src/shared/utils/validation-client.ts +161 -0
- package/src/shared/utils/validation.ts +399 -0
- package/src/shared.ts +155 -0
- package/src/types/index.ts +62 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility functions for AI event tracking
|
|
3
|
+
* Utility functions for AI event tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BaseAIProperties, ModelConfigProperties, TokenUsageProperties } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Remove undefined and null values from an object
|
|
10
|
+
*/
|
|
11
|
+
export function cleanProperties<T extends Record<string, any>>(props: T): Partial<T> {
|
|
12
|
+
const cleaned: Partial<T> = {};
|
|
13
|
+
|
|
14
|
+
for (const key in props) {
|
|
15
|
+
if (!Object.prototype.hasOwnProperty.call(props, key)) continue;
|
|
16
|
+
const value = props[key];
|
|
17
|
+
if (value !== undefined && value !== null) {
|
|
18
|
+
cleaned[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return cleaned;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate that required properties are present
|
|
27
|
+
*/
|
|
28
|
+
export function validateRequiredProperties<T extends Record<string, any>>(
|
|
29
|
+
properties: T,
|
|
30
|
+
required: (keyof T)[],
|
|
31
|
+
): void {
|
|
32
|
+
const missing: string[] = [];
|
|
33
|
+
|
|
34
|
+
for (const key of required) {
|
|
35
|
+
if (properties[key] === undefined || properties[key] === null) {
|
|
36
|
+
missing.push(String(key));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (missing.length > 0) {
|
|
41
|
+
throw new Error(`Missing required properties: ${missing.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalize base AI operation properties
|
|
47
|
+
*/
|
|
48
|
+
export function normalizeBaseProperties(props: BaseAIProperties): BaseAIProperties {
|
|
49
|
+
return {
|
|
50
|
+
model_id: props.model_id,
|
|
51
|
+
agent_id: props.agent_id,
|
|
52
|
+
agent_name: props.agent_name,
|
|
53
|
+
conversation_id: props.conversation_id,
|
|
54
|
+
session_id: props.session_id,
|
|
55
|
+
user_id: props.user_id,
|
|
56
|
+
operation_id: props.operation_id,
|
|
57
|
+
model_provider: props.model_provider,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalize token usage properties
|
|
63
|
+
*/
|
|
64
|
+
export function normalizeTokenUsage(props: TokenUsageProperties): TokenUsageProperties {
|
|
65
|
+
const normalized: TokenUsageProperties = {};
|
|
66
|
+
|
|
67
|
+
if (props.input_tokens !== undefined) {
|
|
68
|
+
normalized.input_tokens = Math.max(0, props.input_tokens);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (props.output_tokens !== undefined) {
|
|
72
|
+
normalized.output_tokens = Math.max(0, props.output_tokens);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (props.total_tokens !== undefined) {
|
|
76
|
+
normalized.total_tokens = Math.max(0, props.total_tokens);
|
|
77
|
+
} else if (normalized.input_tokens !== undefined && normalized.output_tokens !== undefined) {
|
|
78
|
+
// Calculate total from already-normalized values (clamped to 0)
|
|
79
|
+
normalized.total_tokens = normalized.input_tokens + normalized.output_tokens;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (props.cost !== undefined) {
|
|
83
|
+
normalized.cost = Math.max(0, props.cost);
|
|
84
|
+
normalized.cost_currency = props.cost_currency ?? 'USD';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return normalized;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Normalize model configuration properties
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeModelConfig(props: ModelConfigProperties): ModelConfigProperties {
|
|
94
|
+
const normalized: ModelConfigProperties = {};
|
|
95
|
+
|
|
96
|
+
if (props.temperature !== undefined) {
|
|
97
|
+
normalized.temperature = Math.max(0, Math.min(2, props.temperature));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (props.max_tokens !== undefined) {
|
|
101
|
+
normalized.max_tokens = Math.max(1, props.max_tokens);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (props.max_output_tokens !== undefined) {
|
|
105
|
+
normalized.max_output_tokens = Math.max(1, props.max_output_tokens);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (props.top_p !== undefined) {
|
|
109
|
+
normalized.top_p = Math.max(0, Math.min(1, props.top_p));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (props.frequency_penalty !== undefined) {
|
|
113
|
+
normalized.frequency_penalty = Math.max(-2, Math.min(2, props.frequency_penalty));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (props.presence_penalty !== undefined) {
|
|
117
|
+
normalized.presence_penalty = Math.max(-2, Math.min(2, props.presence_penalty));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (props.seed !== undefined) {
|
|
121
|
+
normalized.seed = props.seed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (props.stop_sequences !== undefined) {
|
|
125
|
+
normalized.stop_sequences = props.stop_sequences;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return normalized;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Calculate cost based on token usage and model pricing
|
|
133
|
+
*/
|
|
134
|
+
export function calculateCost(
|
|
135
|
+
usage: TokenUsageProperties,
|
|
136
|
+
pricing: {
|
|
137
|
+
input_price_per_1k?: number;
|
|
138
|
+
output_price_per_1k?: number;
|
|
139
|
+
},
|
|
140
|
+
): number {
|
|
141
|
+
let cost = 0;
|
|
142
|
+
|
|
143
|
+
if (usage.input_tokens && pricing.input_price_per_1k) {
|
|
144
|
+
cost += (usage.input_tokens / 1000) * pricing.input_price_per_1k;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (usage.output_tokens && pricing.output_price_per_1k) {
|
|
148
|
+
cost += (usage.output_tokens / 1000) * pricing.output_price_per_1k;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return cost;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Extract model provider from model ID
|
|
156
|
+
*/
|
|
157
|
+
export function extractModelProvider(
|
|
158
|
+
modelId: string,
|
|
159
|
+
): 'anthropic' | 'openai' | 'google' | 'perplexity' | 'other' {
|
|
160
|
+
const lowercaseId = modelId.toLowerCase();
|
|
161
|
+
|
|
162
|
+
if (lowercaseId.includes('claude') || lowercaseId.includes('anthropic')) {
|
|
163
|
+
return 'anthropic';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (lowercaseId.includes('gpt') || lowercaseId.includes('openai')) {
|
|
167
|
+
return 'openai';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (lowercaseId.includes('gemini') || lowercaseId.includes('google')) {
|
|
171
|
+
return 'google';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (lowercaseId.includes('pplx') || lowercaseId.includes('perplexity')) {
|
|
175
|
+
return 'perplexity';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return 'other';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Generate a unique operation ID
|
|
183
|
+
*/
|
|
184
|
+
export function generateOperationId(prefix = 'ai'): string {
|
|
185
|
+
const timestamp = Date.now();
|
|
186
|
+
const random = Math.random().toString(36).slice(2, 9);
|
|
187
|
+
return `${prefix}_${timestamp}_${random}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sanitize error message for tracking
|
|
192
|
+
*/
|
|
193
|
+
export function sanitizeErrorMessage(error: Error | string): string {
|
|
194
|
+
const message = typeof error === 'string' ? error : error.message;
|
|
195
|
+
|
|
196
|
+
// Remove potentially sensitive information
|
|
197
|
+
// Matches: "API key sk-xxx", "api_key: xxx", "api-keys=xxx", etc.
|
|
198
|
+
return message
|
|
199
|
+
.replaceAll(/api[\s_\-]?keys?[\s:=]+[\w-]+/gi, 'api_key=***')
|
|
200
|
+
.replaceAll(/tokens?[\s:=]+[\w-]+/gi, 'token=***')
|
|
201
|
+
.replaceAll(/passwords?[\s:=]+[\w-]+/gi, 'password=***')
|
|
202
|
+
.replaceAll(/secrets?[\s:=]+[\w-]+/gi, 'secret=***')
|
|
203
|
+
.slice(0, 500); // Limit length
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Calculate metrics for completion events
|
|
208
|
+
*/
|
|
209
|
+
export function calculateCompletionMetrics(
|
|
210
|
+
startTime: number,
|
|
211
|
+
endTime: number,
|
|
212
|
+
tokenUsage?: TokenUsageProperties,
|
|
213
|
+
): {
|
|
214
|
+
duration_ms: number;
|
|
215
|
+
tokens_per_second?: number;
|
|
216
|
+
} {
|
|
217
|
+
const duration_ms = endTime - startTime;
|
|
218
|
+
const metrics: { duration_ms: number; tokens_per_second?: number } = {
|
|
219
|
+
duration_ms,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (tokenUsage?.total_tokens && duration_ms > 0) {
|
|
223
|
+
metrics.tokens_per_second = (tokenUsage.total_tokens / duration_ms) * 1000;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return metrics;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Detect intent from chat message
|
|
231
|
+
*/
|
|
232
|
+
export function detectIntent(
|
|
233
|
+
message: string,
|
|
234
|
+
):
|
|
235
|
+
| 'product-search'
|
|
236
|
+
| 'product-inquiry'
|
|
237
|
+
| 'product-comparison'
|
|
238
|
+
| 'cart-management'
|
|
239
|
+
| 'checkout-help'
|
|
240
|
+
| 'order-tracking'
|
|
241
|
+
| 'returns-exchanges'
|
|
242
|
+
| 'support-question'
|
|
243
|
+
| 'general-inquiry'
|
|
244
|
+
| 'complaint'
|
|
245
|
+
| 'feedback' {
|
|
246
|
+
const lowercase = message.toLowerCase();
|
|
247
|
+
|
|
248
|
+
// Order matters: more specific patterns (like order-tracking, complaint) should come
|
|
249
|
+
// before patterns with broad keywords (like product-comparison with 'or')
|
|
250
|
+
const intentPatterns = {
|
|
251
|
+
// Put order-tracking early since "order" contains "or" which could match comparison
|
|
252
|
+
'order-tracking': [
|
|
253
|
+
'track',
|
|
254
|
+
'order status',
|
|
255
|
+
'shipping',
|
|
256
|
+
'delivery',
|
|
257
|
+
'where is my',
|
|
258
|
+
'when will',
|
|
259
|
+
'my order',
|
|
260
|
+
],
|
|
261
|
+
// Put complaint before comparison since "disappointed" could match patterns in other intents
|
|
262
|
+
complaint: ['disappointed', 'angry', 'frustrated', 'terrible', 'worst', 'complain', 'unhappy'],
|
|
263
|
+
'returns-exchanges': ['return', 'refund', 'exchange', 'cancel order', 'send back'],
|
|
264
|
+
'product-search': [
|
|
265
|
+
'looking for',
|
|
266
|
+
'search for',
|
|
267
|
+
'find',
|
|
268
|
+
'show me',
|
|
269
|
+
'need',
|
|
270
|
+
'want to buy',
|
|
271
|
+
'shopping for',
|
|
272
|
+
],
|
|
273
|
+
'product-inquiry': [
|
|
274
|
+
'tell me about',
|
|
275
|
+
'details',
|
|
276
|
+
'specs',
|
|
277
|
+
'features',
|
|
278
|
+
'price',
|
|
279
|
+
'available',
|
|
280
|
+
'in stock',
|
|
281
|
+
],
|
|
282
|
+
'product-comparison': [
|
|
283
|
+
'compare',
|
|
284
|
+
'difference between',
|
|
285
|
+
'which is better',
|
|
286
|
+
'vs',
|
|
287
|
+
' or ', // Use space-bounded 'or' to avoid matching 'order', 'for', etc.
|
|
288
|
+
'better than',
|
|
289
|
+
],
|
|
290
|
+
'cart-management': ['add to cart', 'remove from cart', 'cart', 'shopping bag', 'my items'],
|
|
291
|
+
'checkout-help': ['checkout', 'payment', 'pay', 'purchase', 'buy now', 'complete order'],
|
|
292
|
+
feedback: ['review', 'feedback', 'rate', 'experience', 'satisfied'],
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
for (const [intent, patterns] of Object.entries(intentPatterns)) {
|
|
296
|
+
if (patterns.some(pattern => lowercase.includes(pattern))) {
|
|
297
|
+
return intent as any;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return 'general-inquiry';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Detect sentiment from message
|
|
306
|
+
*/
|
|
307
|
+
export function detectSentiment(message: string): 'positive' | 'negative' | 'neutral' | 'mixed' {
|
|
308
|
+
const lowercase = message.toLowerCase();
|
|
309
|
+
|
|
310
|
+
const positiveWords = [
|
|
311
|
+
'great',
|
|
312
|
+
'love',
|
|
313
|
+
'excellent',
|
|
314
|
+
'perfect',
|
|
315
|
+
'amazing',
|
|
316
|
+
'wonderful',
|
|
317
|
+
'awesome',
|
|
318
|
+
'fantastic',
|
|
319
|
+
'helpful',
|
|
320
|
+
'thank',
|
|
321
|
+
];
|
|
322
|
+
const negativeWords = [
|
|
323
|
+
'bad',
|
|
324
|
+
'terrible',
|
|
325
|
+
'awful',
|
|
326
|
+
'disappointed',
|
|
327
|
+
'angry',
|
|
328
|
+
'frustrated',
|
|
329
|
+
'annoyed',
|
|
330
|
+
'useless',
|
|
331
|
+
'worst',
|
|
332
|
+
'hate',
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
const positiveCount = positiveWords.filter(word => lowercase.includes(word)).length;
|
|
336
|
+
const negativeCount = negativeWords.filter(word => lowercase.includes(word)).length;
|
|
337
|
+
|
|
338
|
+
if (positiveCount > 0 && negativeCount > 0) {
|
|
339
|
+
return 'mixed';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (positiveCount > negativeCount) {
|
|
343
|
+
return 'positive';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (negativeCount > positiveCount) {
|
|
347
|
+
return 'negative';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return 'neutral';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Truncate array to max length for tracking
|
|
355
|
+
*/
|
|
356
|
+
export function truncateArray<T>(arr: T[], maxLength = 10): T[] {
|
|
357
|
+
return arr.slice(0, maxLength);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Safe JSON stringify for complex objects
|
|
362
|
+
*/
|
|
363
|
+
export function safeStringify(obj?: unknown, maxLength = 1000): string {
|
|
364
|
+
try {
|
|
365
|
+
if (obj === undefined) return '{}';
|
|
366
|
+
const str = JSON.stringify(obj);
|
|
367
|
+
return str.length > maxLength ? `${str.slice(0, Math.max(0, maxLength))}...` : str;
|
|
368
|
+
} catch {
|
|
369
|
+
return '[Unable to stringify]';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Calculate conversation quality score
|
|
375
|
+
*/
|
|
376
|
+
export function calculateQualityScore(metrics: {
|
|
377
|
+
message_count: number;
|
|
378
|
+
avg_response_time_ms?: number;
|
|
379
|
+
misunderstanding_count?: number;
|
|
380
|
+
clarification_count?: number;
|
|
381
|
+
satisfaction_score?: number;
|
|
382
|
+
}): number {
|
|
383
|
+
let score = 100;
|
|
384
|
+
|
|
385
|
+
// Penalize for too few messages (abandoned)
|
|
386
|
+
if (metrics.message_count < 3) {
|
|
387
|
+
score -= 20;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Penalize for slow responses
|
|
391
|
+
if (metrics.avg_response_time_ms && metrics.avg_response_time_ms > 3000) {
|
|
392
|
+
score -= 10;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Penalize for misunderstandings
|
|
396
|
+
if (metrics.misunderstanding_count) {
|
|
397
|
+
score -= metrics.misunderstanding_count * 5;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Penalize for excessive clarifications
|
|
401
|
+
if (metrics.clarification_count && metrics.clarification_count > 2) {
|
|
402
|
+
score -= (metrics.clarification_count - 2) * 3;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Boost for high satisfaction
|
|
406
|
+
if (metrics.satisfaction_score && metrics.satisfaction_score >= 4) {
|
|
407
|
+
score += 10;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return Math.max(0, Math.min(100, score));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Detect if message needs clarification
|
|
415
|
+
*/
|
|
416
|
+
export function needsClarification(message: string): boolean {
|
|
417
|
+
const lowercase = message.toLowerCase();
|
|
418
|
+
|
|
419
|
+
const ambiguousPatterns = [
|
|
420
|
+
'it',
|
|
421
|
+
'that one',
|
|
422
|
+
'this',
|
|
423
|
+
'them',
|
|
424
|
+
'those',
|
|
425
|
+
'something like',
|
|
426
|
+
'kind of',
|
|
427
|
+
'sort of',
|
|
428
|
+
'maybe',
|
|
429
|
+
'not sure',
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
return ambiguousPatterns.some(pattern => lowercase.includes(pattern));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Extract product mentions from message
|
|
437
|
+
*/
|
|
438
|
+
export function extractProductMentions(message: string): string[] {
|
|
439
|
+
const mentions: string[] = [];
|
|
440
|
+
|
|
441
|
+
// Common product categories
|
|
442
|
+
const categories = [
|
|
443
|
+
'laptop',
|
|
444
|
+
'phone',
|
|
445
|
+
'tablet',
|
|
446
|
+
'watch',
|
|
447
|
+
'headphones',
|
|
448
|
+
'shoes',
|
|
449
|
+
'shirt',
|
|
450
|
+
'pants',
|
|
451
|
+
'dress',
|
|
452
|
+
'jacket',
|
|
453
|
+
];
|
|
454
|
+
|
|
455
|
+
const lowercase = message.toLowerCase();
|
|
456
|
+
for (const category of categories) {
|
|
457
|
+
if (lowercase.includes(category)) {
|
|
458
|
+
mentions.push(category);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return mentions;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Alias for detectIntent for chatbot-specific contexts
|
|
467
|
+
*/
|
|
468
|
+
export const detectChatbotIntent = detectIntent;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Cart and checkout-related ecommerce events
|
|
3
|
+
* Cart and checkout-related ecommerce events
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { trackEcommerce } from '../track-ecommerce';
|
|
7
|
+
import {
|
|
8
|
+
type CartAbandonmentProperties,
|
|
9
|
+
type CartProperties,
|
|
10
|
+
type CartUpdateProperties,
|
|
11
|
+
type CheckoutProgressProperties,
|
|
12
|
+
ECOMMERCE_EVENTS,
|
|
13
|
+
type EcommerceEventSpec,
|
|
14
|
+
} from '../types';
|
|
15
|
+
import {
|
|
16
|
+
cleanProperties,
|
|
17
|
+
normalizeProductProperties,
|
|
18
|
+
normalizeProducts,
|
|
19
|
+
validateRequiredProperties,
|
|
20
|
+
} from '../utils';
|
|
21
|
+
|
|
22
|
+
import type { EmitterOptions, EmitterTrackPayload } from '../../emitter-types';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Track cart updates (add, remove, update) with a single event
|
|
26
|
+
*/
|
|
27
|
+
export function cartUpdated(
|
|
28
|
+
properties: CartUpdateProperties,
|
|
29
|
+
options?: EmitterOptions,
|
|
30
|
+
): EmitterTrackPayload {
|
|
31
|
+
const { cart_id, action, cart_total, product, quantity_change } = properties;
|
|
32
|
+
const normalizedProduct = normalizeProductProperties(product);
|
|
33
|
+
|
|
34
|
+
validateRequiredProperties(properties, ['action']);
|
|
35
|
+
validateRequiredProperties(normalizedProduct, ['product_id']);
|
|
36
|
+
|
|
37
|
+
const eventSpec: EcommerceEventSpec = {
|
|
38
|
+
name: ECOMMERCE_EVENTS.CART_UPDATED,
|
|
39
|
+
category: 'ecommerce',
|
|
40
|
+
properties: cleanProperties({
|
|
41
|
+
action,
|
|
42
|
+
...normalizedProduct,
|
|
43
|
+
cart_id,
|
|
44
|
+
cart_total,
|
|
45
|
+
quantity_change,
|
|
46
|
+
}),
|
|
47
|
+
requiredProperties: ['action', 'product_id'],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return trackEcommerce(eventSpec, options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Track when a user views their shopping cart
|
|
55
|
+
*/
|
|
56
|
+
export function cartViewed(properties: CartProperties): EcommerceEventSpec<CartProperties> {
|
|
57
|
+
const normalizedProps: CartProperties = {
|
|
58
|
+
cart_id: properties.cart_id,
|
|
59
|
+
products: properties.products ? normalizeProducts(properties.products) : undefined,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: ECOMMERCE_EVENTS.CART_VIEWED,
|
|
64
|
+
category: 'ecommerce',
|
|
65
|
+
properties: cleanProperties(normalizedProps),
|
|
66
|
+
requiredProperties: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Track when a cart is abandoned
|
|
72
|
+
*/
|
|
73
|
+
export function cartAbandoned(properties: CartAbandonmentProperties): EcommerceEventSpec {
|
|
74
|
+
validateRequiredProperties(properties, ['cart_id', 'cart_value']);
|
|
75
|
+
|
|
76
|
+
const normalizedProps = {
|
|
77
|
+
...properties,
|
|
78
|
+
products: normalizeProducts(properties.products),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
name: ECOMMERCE_EVENTS.CART_ABANDONED,
|
|
83
|
+
category: 'ecommerce',
|
|
84
|
+
properties: cleanProperties(normalizedProps),
|
|
85
|
+
requiredProperties: ['cart_id', 'cart_value'],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Track checkout progression with a single event
|
|
91
|
+
*/
|
|
92
|
+
export function checkoutProgressed(properties: CheckoutProgressProperties): EcommerceEventSpec {
|
|
93
|
+
validateRequiredProperties(properties, ['step', 'step_name', 'action']);
|
|
94
|
+
|
|
95
|
+
const normalizedProps = {
|
|
96
|
+
...properties,
|
|
97
|
+
products: properties.products ? normalizeProducts(properties.products) : undefined,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
name: ECOMMERCE_EVENTS.CHECKOUT_PROGRESSED,
|
|
102
|
+
category: 'ecommerce',
|
|
103
|
+
properties: cleanProperties(normalizedProps),
|
|
104
|
+
requiredProperties: ['step', 'step_name', 'action'],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Coupon-related ecommerce events
|
|
3
|
+
* Coupon-related ecommerce events
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type CouponProperties, ECOMMERCE_EVENTS, type EcommerceEventSpec } from '../types';
|
|
7
|
+
import { cleanProperties } from '../utils';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Track when a coupon is successfully applied
|
|
11
|
+
*/
|
|
12
|
+
export function couponApplied(properties: CouponProperties): EcommerceEventSpec<CouponProperties> {
|
|
13
|
+
const normalizedProps: CouponProperties = {
|
|
14
|
+
cart_id: properties.cart_id,
|
|
15
|
+
coupon_id: properties.coupon_id,
|
|
16
|
+
order_id: properties.order_id,
|
|
17
|
+
coupon_name: properties.coupon_name,
|
|
18
|
+
discount: properties.discount,
|
|
19
|
+
reason: properties.reason,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
name: ECOMMERCE_EVENTS.COUPON_APPLIED,
|
|
24
|
+
category: 'ecommerce',
|
|
25
|
+
properties: cleanProperties(normalizedProps),
|
|
26
|
+
requiredProperties: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Track when a coupon is removed
|
|
32
|
+
*/
|
|
33
|
+
export function couponRemoved(properties: CouponProperties): EcommerceEventSpec<CouponProperties> {
|
|
34
|
+
const normalizedProps: CouponProperties = {
|
|
35
|
+
cart_id: properties.cart_id,
|
|
36
|
+
coupon_id: properties.coupon_id,
|
|
37
|
+
order_id: properties.order_id,
|
|
38
|
+
coupon_name: properties.coupon_name,
|
|
39
|
+
discount: properties.discount,
|
|
40
|
+
reason: properties.reason,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
name: ECOMMERCE_EVENTS.COUPON_REMOVED,
|
|
45
|
+
category: 'ecommerce',
|
|
46
|
+
properties: cleanProperties(normalizedProps),
|
|
47
|
+
requiredProperties: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Customer engagement events for ecommerce
|
|
3
|
+
* Customer engagement events for ecommerce
|
|
4
|
+
* Track user interactions that indicate interest but aren't direct purchases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type BaseProductProperties, ECOMMERCE_EVENTS, type EcommerceEventSpec } from '../types';
|
|
8
|
+
import { cleanProperties, normalizeProductProperties, validateRequiredProperties } from '../utils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Track when a user sets a price alert for a product
|
|
12
|
+
*/
|
|
13
|
+
export function priceAlertSet(
|
|
14
|
+
properties: BaseProductProperties & {
|
|
15
|
+
threshold_price: number;
|
|
16
|
+
notification_method?: 'email' | 'sms' | 'push';
|
|
17
|
+
currency?: string;
|
|
18
|
+
},
|
|
19
|
+
): EcommerceEventSpec {
|
|
20
|
+
const { currency, notification_method, threshold_price, ...productProps } = properties;
|
|
21
|
+
const normalizedProduct = normalizeProductProperties(productProps);
|
|
22
|
+
validateRequiredProperties(normalizedProduct, ['product_id']);
|
|
23
|
+
validateRequiredProperties({ threshold_price }, ['threshold_price']);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
name: ECOMMERCE_EVENTS.PRICE_ALERT_SET,
|
|
27
|
+
category: 'ecommerce',
|
|
28
|
+
properties: cleanProperties({
|
|
29
|
+
...normalizedProduct,
|
|
30
|
+
action_type: 'price_alert',
|
|
31
|
+
currency,
|
|
32
|
+
notification_method,
|
|
33
|
+
threshold_price,
|
|
34
|
+
}),
|
|
35
|
+
requiredProperties: ['product_id', 'threshold_price'],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Track when a user requests notification for out-of-stock items
|
|
41
|
+
*/
|
|
42
|
+
export function backInStockRequested(
|
|
43
|
+
properties: BaseProductProperties & {
|
|
44
|
+
notification_method?: 'email' | 'sms' | 'push';
|
|
45
|
+
},
|
|
46
|
+
): EcommerceEventSpec {
|
|
47
|
+
const { notification_method, ...productProps } = properties;
|
|
48
|
+
const normalizedProduct = normalizeProductProperties(productProps);
|
|
49
|
+
validateRequiredProperties(normalizedProduct, ['product_id']);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name: ECOMMERCE_EVENTS.BACK_IN_STOCK_REQUESTED,
|
|
53
|
+
category: 'ecommerce',
|
|
54
|
+
properties: cleanProperties({
|
|
55
|
+
...normalizedProduct,
|
|
56
|
+
action_type: 'back_in_stock',
|
|
57
|
+
notification_method,
|
|
58
|
+
}),
|
|
59
|
+
requiredProperties: ['product_id'],
|
|
60
|
+
};
|
|
61
|
+
}
|