@ariaflowagents/analytics-sdk 0.9.1

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 ADDED
@@ -0,0 +1,313 @@
1
+ # @ariaflowagents/analytics-sdk
2
+
3
+ Type-safe SDK for sending analytics events to AriaFlow Analytics Platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @ariaflowagents/analytics-sdk
9
+ # or
10
+ bun add @ariaflowagents/analytics-sdk
11
+ # or
12
+ yarn add @ariaflowagents/analytics-sdk
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Initialize the Client
18
+
19
+ ```typescript
20
+ import { createAnalyticsClient } from '@ariaflowagents/analytics-sdk';
21
+
22
+ const analytics = createAnalyticsClient({
23
+ apiKey: 'your-api-key',
24
+ workspaceId: 'your-workspace-id',
25
+ endpoint: 'https://analytics.ariaflow.dev/api/v1', // optional
26
+ flushInterval: 5000, // optional, default: 5000ms
27
+ maxBatchSize: 20, // optional, default: 20
28
+ enableDebug: true, // optional, default: false
29
+ });
30
+ ```
31
+
32
+ ### 2. Track Events
33
+
34
+ ```typescript
35
+ // Track a single event
36
+ await analytics.track({
37
+ sessionId: 'session-123',
38
+ agentId: 'hospital-agent',
39
+ workspaceId: 'workspace-456',
40
+ type: 'conversation.started',
41
+ data: { channel: 'web' }
42
+ });
43
+
44
+ // Track batch events
45
+ await analytics.trackBatch([
46
+ { sessionId: 'session-123', agentId: 'hospital-agent', workspaceId: 'workspace-456', type: 'node.entered', data: { nodeName: 'triage' } },
47
+ { sessionId: 'session-123', agentId: 'hospital-agent', workspaceId: 'workspace-456', type: 'tool.called', data: { toolName: 'create_booking' } },
48
+ ]);
49
+ ```
50
+
51
+ ### 3. Track Voice Calls
52
+
53
+ ```typescript
54
+ // Start a voice call
55
+ await analytics.trackVoiceCall({
56
+ sessionId: 'call-123',
57
+ workspaceId: 'workspace-456',
58
+ agentId: 'voice-agent',
59
+ userName: 'John Doe',
60
+ startedAt: new Date(),
61
+ });
62
+
63
+ // Update call metrics during the call
64
+ await analytics.updateVoiceCall('call-123', {
65
+ interruptions: 2,
66
+ userTurns: 5,
67
+ agentTurns: 4,
68
+ currentNode: 'booking_flow',
69
+ });
70
+
71
+ // End the call
72
+ await analytics.updateVoiceCall('call-123', {
73
+ endedAt: new Date(),
74
+ durationSeconds: 180,
75
+ outcome: 'booking_completed',
76
+ ttfMs: 850,
77
+ });
78
+ ```
79
+
80
+ ### 4. Set Context
81
+
82
+ ```typescript
83
+ // Set global context (applied to all events)
84
+ analytics.setContext({
85
+ userId: 'user-789',
86
+ conversationId: 'conv-abc',
87
+ agentId: 'hospital-agent',
88
+ });
89
+
90
+ // Identify user
91
+ analytics.identify('user-789', {
92
+ name: 'John Doe',
93
+ email: 'john@example.com',
94
+ plan: 'pro',
95
+ });
96
+ ```
97
+
98
+ ### 5. Flush Events
99
+
100
+ ```typescript
101
+ // Manually flush pending events
102
+ await analytics.flush();
103
+ ```
104
+
105
+ ## React Integration
106
+
107
+ ### AnalyticsProvider
108
+
109
+ ```tsx
110
+ import { AnalyticsProvider } from '@ariaflowagents/analytics-sdk/react';
111
+
112
+ function App() {
113
+ return (
114
+ <AnalyticsProvider config={{
115
+ apiKey: process.env.ANALYTICS_API_KEY!,
116
+ workspaceId: 'workspace-456',
117
+ }}>
118
+ <YourApp />
119
+ </AnalyticsProvider>
120
+ );
121
+ }
122
+ ```
123
+
124
+ ### useAnalytics Hook
125
+
126
+ ```tsx
127
+ import { useAnalytics } from '@ariaflowagents/analytics-sdk/react';
128
+
129
+ function MyComponent() {
130
+ const { track, setContext, identify } = useAnalytics();
131
+
132
+ const handleClick = async () => {
133
+ await track({
134
+ type: 'custom',
135
+ sessionId: 'session-123',
136
+ agentId: 'hospital-agent',
137
+ workspaceId: 'workspace-456',
138
+ data: { button: 'submit_form' }
139
+ });
140
+ };
141
+
142
+ return <button onClick={handleClick}>Submit</button>;
143
+ }
144
+ ```
145
+
146
+ ### usePageView Hook
147
+
148
+ ```tsx
149
+ import { usePageView } from '@ariaflowagents/analytics-sdk/react';
150
+
151
+ function DashboardPage() {
152
+ usePageView('dashboard', { section: 'analytics' });
153
+
154
+ return <div>Dashboard</div>;
155
+ }
156
+ ```
157
+
158
+ ### useVoiceCallTracker Hook
159
+
160
+ ```tsx
161
+ import { useVoiceCallTracker } from '@ariaflowagents/analytics-sdk/react';
162
+
163
+ function VoiceCallComponent({ sessionId, workspaceId }) {
164
+ const { startCall, endCall, trackInterruption, trackUserSpeech, trackAgentSpeech } =
165
+ useVoiceCallTracker(sessionId, workspaceId);
166
+
167
+ useEffect(() => {
168
+ startCall('voice-agent');
169
+
170
+ return () => {
171
+ endCall('completed');
172
+ };
173
+ }, []);
174
+
175
+ return (
176
+ <div>
177
+ <button onClick={trackInterruption}>User Interrupted</button>
178
+ </div>
179
+ );
180
+ }
181
+ ```
182
+
183
+ ## Event Types
184
+
185
+ | Event Type | Description |
186
+ |------------|-------------|
187
+ | `conversation.started` | Conversation began |
188
+ | `conversation.ended` | Conversation ended |
189
+ | `node.entered` | Flow node entered |
190
+ | `node.exited` | Flow node exited |
191
+ | `tool.called` | Tool execution started |
192
+ | `tool.completed` | Tool finished successfully |
193
+ | `tool.error` | Tool execution failed |
194
+ | `booking.completed` | Booking action completed |
195
+ | `handoff.initiated` | Agent handoff triggered |
196
+ | `emergency.detected` | Emergency condition detected |
197
+ | `call.started` | Voice call started |
198
+ | `call.ended` | Voice call ended |
199
+ | `user.spoke` | User spoke (voice) |
200
+ | `agent.spoke` | Agent spoke (voice) |
201
+ | `user.interrupted` | User interrupted agent |
202
+ | `silence.detected` | Silence detected |
203
+ | `latency.stt` | Speech-to-text latency |
204
+ | `latency.ttf` | Time to first response |
205
+ | `latency.e2e` | End-to-end latency |
206
+ | `latency.tts` | Text-to-speech latency |
207
+ | `error.occurred` | Error occurred |
208
+ | `custom` | Custom event |
209
+
210
+ ## Integration with AriaFlow Runtime
211
+
212
+ ```typescript
213
+ import { Runtime, createTelemetryHooks } from '@ariaflowagents/core';
214
+ import { createAnalyticsClient } from '@ariaflowagents/analytics-sdk';
215
+
216
+ const analytics = createAnalyticsClient({
217
+ apiKey: process.env.ANALYTICS_API_KEY!,
218
+ workspaceId: 'workspace-456',
219
+ });
220
+
221
+ // Create telemetry hooks that forward events to analytics
222
+ const telemetryHooks = {
223
+ onAgentStart: async (context, agentId) => {
224
+ await analytics.track({
225
+ sessionId: context.session.id,
226
+ agentId,
227
+ workspaceId: 'workspace-456',
228
+ type: 'conversation.started',
229
+ data: {}
230
+ });
231
+ },
232
+ onToolCall: async (context, call) => {
233
+ await analytics.track({
234
+ sessionId: context.session.id,
235
+ agentId: context.agentId,
236
+ workspaceId: 'workspace-456',
237
+ type: 'tool.called',
238
+ data: {
239
+ toolName: call.toolName,
240
+ toolCallId: call.toolCallId,
241
+ }
242
+ });
243
+ },
244
+ onToolResult: async (context, call) => {
245
+ await analytics.track({
246
+ sessionId: context.session.id,
247
+ agentId: context.agentId,
248
+ workspaceId: 'workspace-456',
249
+ type: 'tool.completed',
250
+ data: {
251
+ toolName: call.toolName,
252
+ success: call.success,
253
+ durationMs: call.durationMs,
254
+ }
255
+ });
256
+ },
257
+ onHandoff: async (context, from, to, reason) => {
258
+ await analytics.track({
259
+ sessionId: context.session.id,
260
+ agentId: from,
261
+ workspaceId: 'workspace-456',
262
+ type: 'handoff.initiated',
263
+ data: { from, to, reason }
264
+ });
265
+ },
266
+ };
267
+
268
+ const runtime = new Runtime({
269
+ agents: [...],
270
+ hooks: telemetryHooks,
271
+ });
272
+ ```
273
+
274
+ ## API Reference
275
+
276
+ ### `createAnalyticsClient(config: AnalyticsConfig): AnalyticsClient`
277
+
278
+ Creates a new analytics client instance.
279
+
280
+ ### `AnalyticsClient` Methods
281
+
282
+ | Method | Description |
283
+ |--------|-------------|
284
+ | `track(event)` | Track a single event |
285
+ | `trackBatch(events)` | Track multiple events |
286
+ | `trackVoiceCall(data)` | Create a voice call record |
287
+ | `updateVoiceCall(sessionId, data)` | Update voice call metrics |
288
+ | `flush()` | Flush pending events |
289
+ | `setContext(context)` | Set global context |
290
+ | `identify(userId, traits?)` | Identify user |
291
+
292
+ ### `AnalyticsConfig`
293
+
294
+ | Option | Type | Required | Default |
295
+ |--------|------|----------|---------|
296
+ | `apiKey` | `string` | ✅ | - |
297
+ | `workspaceId` | `string` | ✅ | - |
298
+ | `endpoint` | `string` | ❌ | `https://analytics.ariaflow.dev/api/v1` |
299
+ | `flushInterval` | `number` | ❌ | `5000` (ms) |
300
+ | `maxBatchSize` | `number` | ❌ | `20` |
301
+ | `enableDebug` | `boolean` | ❌ | `false` |
302
+
303
+ ## Best Practices
304
+
305
+ 1. **Initialize Once**: Create a single client instance and reuse it
306
+ 2. **Batch Events**: The SDK automatically batches events for performance
307
+ 3. **Context First**: Set context and user identity early in the session
308
+ 4. **Flush on Exit**: Call `flush()` before page unload or app shutdown
309
+ 5. **Error Handling**: Wrap calls in try-catch for production use
310
+
311
+ ## License
312
+
313
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // @bun
2
+ class s{queue=[];flushTimer=null;options;constructor(t){this.options=t,this.startFlushTimer()}add(t){if(this.queue.push(t),this.queue.length>=this.options.maxBatchSize)this.flush()}async flush(){if(this.queue.length===0)return;let t=[...this.queue];if(this.queue=[],this.options.enableDebug)console.log(`[Analytics] Flushing ${t.length} events`);try{await this.options.onFlush(t)}catch(e){console.error("[Analytics] Flush failed:",e),this.queue.unshift(...t)}}startFlushTimer(){this.flushTimer=setInterval(()=>{this.flush()},this.options.flushInterval)}destroy(){if(this.flushTimer)clearInterval(this.flushTimer),this.flushTimer=null;this.flush()}}class i{config;batcher;context;userId;userTraits;constructor(t){this.config={apiKey:t.apiKey,endpoint:t.endpoint??"https://analytics.ariaflow.dev/api/v1",workspaceId:t.workspaceId,flushInterval:t.flushInterval??5000,maxBatchSize:t.maxBatchSize??20,enableDebug:t.enableDebug??!1},this.context={workspaceId:this.config.workspaceId},this.batcher=new s({maxBatchSize:this.config.maxBatchSize,flushInterval:this.config.flushInterval,onFlush:this.sendEvents.bind(this),enableDebug:this.config.enableDebug})}async track(t){let e=this.enrichEvent(t);this.batcher.add(e)}async trackBatch(t){for(let e of t)await this.track(e)}async trackVoiceCall(t){if(this.config.enableDebug)console.log("[Analytics] Tracking voice call:",t.sessionId);let e=await fetch(`${this.config.endpoint}/voice-call`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify(t)});if(!e.ok){let n=await e.text();throw Error(`Failed to track voice call: ${n}`)}}async updateVoiceCall(t,e){if(this.config.enableDebug)console.log("[Analytics] Updating voice call:",t);let n=await fetch(`${this.config.endpoint}/voice-call/${t}`,{method:"PUT",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify(e)});if(!n.ok){let a=await n.text();throw Error(`Failed to update voice call: ${a}`)}}async flush(){await this.batcher.flush()}setContext(t){this.context={...this.context,...t}}identify(t,e){this.userId=t,this.userTraits=e}enrichEvent(t){return{...t,workspaceId:t.workspaceId??this.context.workspaceId,sessionId:t.sessionId??this.context.sessionId??"",agentId:t.agentId??this.context.agentId??"",conversationId:t.conversationId??this.context.conversationId,timestamp:t.timestamp??new Date,data:{...t.data,userId:t.data.userId??this.userId,userTraits:this.userTraits}}}async sendEvents(t){let e=await fetch(`${this.config.endpoint}/events`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({events:t})});if(!e.ok){let n=await e.text();throw Error(`Failed to send events: ${n}`)}}destroy(){this.batcher.destroy()}}function r(t){return new i(t)}var c=i;export{c as default,r as createAnalyticsClient,s as Batcher,i as AriaFlowAnalytics};
3
+
4
+ //# debugId=C025AF11B008174864756E2164756E21
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": [
5
+ "export interface AnalyticsEvent {\n id?: string;\n timestamp?: Date;\n sessionId: string;\n conversationId?: string;\n agentId: string;\n workspaceId: string;\n type: AnalyticsEventType;\n data: Record<string, unknown>;\n}\n\nexport type AnalyticsEventType =\n | \"conversation.started\"\n | \"conversation.ended\"\n | \"node.entered\"\n | \"node.exited\"\n | \"tool.called\"\n | \"tool.completed\"\n | \"tool.error\"\n | \"booking.completed\"\n | \"handoff.initiated\"\n | \"emergency.detected\"\n | \"call.started\"\n | \"call.ended\"\n | \"user.spoke\"\n | \"agent.spoke\"\n | \"user.interrupted\"\n | \"silence.detected\"\n | \"latency.stt\"\n | \"latency.ttf\"\n | \"latency.e2e\"\n | \"latency.tts\"\n | \"error.occurred\"\n | \"custom\";\n\nexport interface VoiceCallData {\n sessionId: string;\n workspaceId: string;\n agentId?: string;\n userName?: string;\n userId?: string;\n startedAt: Date;\n endedAt?: Date;\n durationSeconds?: number;\n userTurns?: number;\n agentTurns?: number;\n interruptions?: number;\n silenceEvents?: number;\n errors?: number;\n totalUserSpeechMs?: number;\n totalAgentSpeechMs?: number;\n totalSilenceMs?: number;\n ttfMs?: number;\n avgSttMs?: number;\n avgTtsMs?: number;\n e2eMs?: number;\n outcome?: string;\n outcomeData?: Record<string, unknown>;\n currentNode?: string;\n agentName?: string;\n transcript?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface AnalyticsConfig {\n apiKey: string;\n endpoint?: string;\n workspaceId: string;\n flushInterval?: number;\n maxBatchSize?: number;\n enableDebug?: boolean;\n}\n\nexport interface AnalyticsClient {\n track: (event: AnalyticsEvent) => Promise<void>;\n trackBatch: (events: AnalyticsEvent[]) => Promise<void>;\n trackVoiceCall: (data: VoiceCallData) => Promise<void>;\n updateVoiceCall: (sessionId: string, data: Partial<VoiceCallData>) => Promise<void>;\n flush: () => Promise<void>;\n setContext: (context: Partial<AnalyticsContext>) => void;\n identify: (userId: string, traits?: Record<string, unknown>) => void;\n}\n\nexport interface AnalyticsContext {\n workspaceId: string;\n agentId?: string;\n sessionId?: string;\n userId?: string;\n conversationId?: string;\n}\n\nexport interface BatcherOptions {\n maxBatchSize: number;\n flushInterval: number;\n onFlush: (events: AnalyticsEvent[]) => Promise<void>;\n enableDebug?: boolean;\n}\n\nexport class Batcher {\n private queue: AnalyticsEvent[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private options: BatcherOptions;\n\n constructor(options: BatcherOptions) {\n this.options = options;\n this.startFlushTimer();\n }\n\n add(event: AnalyticsEvent): void {\n this.queue.push(event);\n\n if (this.queue.length >= this.options.maxBatchSize) {\n this.flush();\n }\n }\n\n async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n\n const events = [...this.queue];\n this.queue = [];\n\n if (this.options.enableDebug) {\n console.log(`[Analytics] Flushing ${events.length} events`);\n }\n\n try {\n await this.options.onFlush(events);\n } catch (error) {\n console.error(\"[Analytics] Flush failed:\", error);\n this.queue.unshift(...events);\n }\n }\n\n private startFlushTimer(): void {\n this.flushTimer = setInterval(() => {\n this.flush();\n }, this.options.flushInterval);\n }\n\n destroy(): void {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n this.flush();\n }\n}\n\nexport class AriaFlowAnalytics implements AnalyticsClient {\n private config: Required<AnalyticsConfig>;\n private batcher: Batcher;\n private context: AnalyticsContext;\n private userId?: string;\n private userTraits?: Record<string, unknown>;\n\n constructor(config: AnalyticsConfig) {\n this.config = {\n apiKey: config.apiKey,\n endpoint: config.endpoint ?? \"https://analytics.ariaflow.dev/api/v1\",\n workspaceId: config.workspaceId,\n flushInterval: config.flushInterval ?? 5000,\n maxBatchSize: config.maxBatchSize ?? 20,\n enableDebug: config.enableDebug ?? false,\n };\n\n this.context = {\n workspaceId: this.config.workspaceId,\n };\n\n this.batcher = new Batcher({\n maxBatchSize: this.config.maxBatchSize,\n flushInterval: this.config.flushInterval,\n onFlush: this.sendEvents.bind(this),\n enableDebug: this.config.enableDebug,\n });\n }\n\n async track(event: AnalyticsEvent): Promise<void> {\n const enrichedEvent = this.enrichEvent(event);\n this.batcher.add(enrichedEvent);\n }\n\n async trackBatch(events: AnalyticsEvent[]): Promise<void> {\n for (const event of events) {\n await this.track(event);\n }\n }\n\n async trackVoiceCall(data: VoiceCallData): Promise<void> {\n if (this.config.enableDebug) {\n console.log(\"[Analytics] Tracking voice call:\", data.sessionId);\n }\n\n const response = await fetch(`${this.config.endpoint}/voice-call`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify(data),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to track voice call: ${error}`);\n }\n }\n\n async updateVoiceCall(sessionId: string, data: Partial<VoiceCallData>): Promise<void> {\n if (this.config.enableDebug) {\n console.log(\"[Analytics] Updating voice call:\", sessionId);\n }\n\n const response = await fetch(`${this.config.endpoint}/voice-call/${sessionId}`, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify(data),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to update voice call: ${error}`);\n }\n }\n\n async flush(): Promise<void> {\n await this.batcher.flush();\n }\n\n setContext(context: Partial<AnalyticsContext>): void {\n this.context = { ...this.context, ...context };\n }\n\n identify(userId: string, traits?: Record<string, unknown>): void {\n this.userId = userId;\n this.userTraits = traits;\n }\n\n private enrichEvent(event: AnalyticsEvent): AnalyticsEvent {\n return {\n ...event,\n workspaceId: event.workspaceId ?? this.context.workspaceId,\n sessionId: event.sessionId ?? this.context.sessionId ?? \"\",\n agentId: event.agentId ?? this.context.agentId ?? \"\",\n conversationId: event.conversationId ?? this.context.conversationId,\n timestamp: event.timestamp ?? new Date(),\n data: {\n ...event.data,\n userId: event.data.userId ?? this.userId,\n userTraits: this.userTraits,\n },\n };\n }\n\n private async sendEvents(events: AnalyticsEvent[]): Promise<void> {\n const response = await fetch(`${this.config.endpoint}/events`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({ events }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to send events: ${error}`);\n }\n }\n\n destroy(): void {\n this.batcher.destroy();\n }\n}\n\nexport function createAnalyticsClient(config: AnalyticsConfig): AnalyticsClient {\n return new AriaFlowAnalytics(config);\n}\n\nexport default AriaFlowAnalytics;\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "C025AF11B008174864756E2164756E21",
9
+ "names": []
10
+ }
package/dist/react.js ADDED
@@ -0,0 +1,5 @@
1
+ // @bun
2
+ class m{queue=[];flushTimer=null;options;constructor(t){this.options=t,this.startFlushTimer()}add(t){if(this.queue.push(t),this.queue.length>=this.options.maxBatchSize)this.flush()}async flush(){if(this.queue.length===0)return;let t=[...this.queue];if(this.queue=[],this.options.enableDebug)console.log(`[Analytics] Flushing ${t.length} events`);try{await this.options.onFlush(t)}catch(n){console.error("[Analytics] Flush failed:",n),this.queue.unshift(...t)}}startFlushTimer(){this.flushTimer=setInterval(()=>{this.flush()},this.options.flushInterval)}destroy(){if(this.flushTimer)clearInterval(this.flushTimer),this.flushTimer=null;this.flush()}}class g{config;batcher;context;userId;userTraits;constructor(t){this.config={apiKey:t.apiKey,endpoint:t.endpoint??"https://analytics.ariaflow.dev/api/v1",workspaceId:t.workspaceId,flushInterval:t.flushInterval??5000,maxBatchSize:t.maxBatchSize??20,enableDebug:t.enableDebug??!1},this.context={workspaceId:this.config.workspaceId},this.batcher=new m({maxBatchSize:this.config.maxBatchSize,flushInterval:this.config.flushInterval,onFlush:this.sendEvents.bind(this),enableDebug:this.config.enableDebug})}async track(t){let n=this.enrichEvent(t);this.batcher.add(n)}async trackBatch(t){for(let n of t)await this.track(n)}async trackVoiceCall(t){if(this.config.enableDebug)console.log("[Analytics] Tracking voice call:",t.sessionId);let n=await fetch(`${this.config.endpoint}/voice-call`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify(t)});if(!n.ok){let e=await n.text();throw Error(`Failed to track voice call: ${e}`)}}async updateVoiceCall(t,n){if(this.config.enableDebug)console.log("[Analytics] Updating voice call:",t);let e=await fetch(`${this.config.endpoint}/voice-call/${t}`,{method:"PUT",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify(n)});if(!e.ok){let r=await e.text();throw Error(`Failed to update voice call: ${r}`)}}async flush(){await this.batcher.flush()}setContext(t){this.context={...this.context,...t}}identify(t,n){this.userId=t,this.userTraits=n}enrichEvent(t){return{...t,workspaceId:t.workspaceId??this.context.workspaceId,sessionId:t.sessionId??this.context.sessionId??"",agentId:t.agentId??this.context.agentId??"",conversationId:t.conversationId??this.context.conversationId,timestamp:t.timestamp??new Date,data:{...t.data,userId:t.data.userId??this.userId,userTraits:this.userTraits}}}async sendEvents(t){let n=await fetch(`${this.config.endpoint}/events`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.apiKey}`},body:JSON.stringify({events:t})});if(!n.ok){let e=await n.text();throw Error(`Failed to send events: ${e}`)}}destroy(){this.batcher.destroy()}}function v(t){return new g(t)}var x=g;import{useEffect as f,useRef as C,useCallback as s}from"react";var l=null;function w(t){if(l)l.destroy();return l=v(t),l}function k(){return l}function A(t){let n=C(null);f(()=>{if(t&&!n.current)n.current=v(t);return()=>{if(n.current)n.current.destroy(),n.current=null}},[t]);let e=n.current??l,r=s(async(a)=>{if(!e){console.warn("[Analytics] Client not initialized");return}await e.track(a)},[e]),i=s(async(a)=>{if(!e){console.warn("[Analytics] Client not initialized");return}await e.trackBatch(a)},[e]),u=s(async(a)=>{if(!e){console.warn("[Analytics] Client not initialized");return}await e.trackVoiceCall(a)},[e]),d=s(async(a,o)=>{if(!e){console.warn("[Analytics] Client not initialized");return}await e.updateVoiceCall(a,o)},[e]),h=s(async()=>{if(!e){console.warn("[Analytics] Client not initialized");return}await e.flush()},[e]),p=s((a)=>{if(!e){console.warn("[Analytics] Client not initialized");return}e.setContext(a)},[e]),y=s((a,o)=>{if(!e){console.warn("[Analytics] Client not initialized");return}e.identify(a,o)},[e]);return{track:r,trackBatch:i,trackVoiceCall:u,updateVoiceCall:d,flush:h,setContext:p,identify:y}}function D({children:t,config:n}){return f(()=>{return w(n),()=>{let e=k();if(e)e.destroy()}},[n]),t}function E(){let{track:t}=A();return t}function S(t,n){let{track:e}=A();f(()=>{e({type:"custom",sessionId:"",agentId:"",workspaceId:"",data:{event:"page_view",page:t,...n}})},[t,n,e])}function V(t,n){let{trackVoiceCall:e,updateVoiceCall:r}=A(),i=C({sessionId:t,workspaceId:n,userTurns:0,agentTurns:0,interruptions:0,totalUserSpeechMs:0,totalAgentSpeechMs:0}),u=s(async(a)=>{let o={sessionId:t,workspaceId:n,agentId:a,startedAt:new Date};i.current={...i.current,...o},await e(o)},[t,n,e]),d=s(async(a,o)=>{let c={endedAt:new Date,outcome:a,...i.current,...o};if(c.startedAt&&c.endedAt)c.durationSeconds=Math.floor((c.endedAt.getTime()-c.startedAt.getTime())/1000);await r(t,c)},[t,r]),h=s(()=>{i.current.interruptions=(i.current.interruptions??0)+1,r(t,{interruptions:i.current.interruptions})},[t,r]),p=s((a)=>{i.current.userTurns=(i.current.userTurns??0)+1,i.current.totalUserSpeechMs=(i.current.totalUserSpeechMs??0)+a,r(t,{userTurns:i.current.userTurns,totalUserSpeechMs:i.current.totalUserSpeechMs})},[t,r]),y=s((a)=>{i.current.agentTurns=(i.current.agentTurns??0)+1,i.current.totalAgentSpeechMs=(i.current.totalAgentSpeechMs??0)+a,r(t,{agentTurns:i.current.agentTurns,totalAgentSpeechMs:i.current.totalAgentSpeechMs})},[t,r]);return{startCall:u,endCall:d,trackInterruption:h,trackUserSpeech:p,trackAgentSpeech:y}}export{V as useVoiceCallTracker,E as useTrackEvent,S as usePageView,A as useAnalytics,w as initAnalytics,k as getAnalyticsClient,D as AnalyticsProvider};
3
+
4
+ //# debugId=752A6A65667196E164756E2164756E21
5
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../src/react.ts"],
4
+ "sourcesContent": [
5
+ "export interface AnalyticsEvent {\n id?: string;\n timestamp?: Date;\n sessionId: string;\n conversationId?: string;\n agentId: string;\n workspaceId: string;\n type: AnalyticsEventType;\n data: Record<string, unknown>;\n}\n\nexport type AnalyticsEventType =\n | \"conversation.started\"\n | \"conversation.ended\"\n | \"node.entered\"\n | \"node.exited\"\n | \"tool.called\"\n | \"tool.completed\"\n | \"tool.error\"\n | \"booking.completed\"\n | \"handoff.initiated\"\n | \"emergency.detected\"\n | \"call.started\"\n | \"call.ended\"\n | \"user.spoke\"\n | \"agent.spoke\"\n | \"user.interrupted\"\n | \"silence.detected\"\n | \"latency.stt\"\n | \"latency.ttf\"\n | \"latency.e2e\"\n | \"latency.tts\"\n | \"error.occurred\"\n | \"custom\";\n\nexport interface VoiceCallData {\n sessionId: string;\n workspaceId: string;\n agentId?: string;\n userName?: string;\n userId?: string;\n startedAt: Date;\n endedAt?: Date;\n durationSeconds?: number;\n userTurns?: number;\n agentTurns?: number;\n interruptions?: number;\n silenceEvents?: number;\n errors?: number;\n totalUserSpeechMs?: number;\n totalAgentSpeechMs?: number;\n totalSilenceMs?: number;\n ttfMs?: number;\n avgSttMs?: number;\n avgTtsMs?: number;\n e2eMs?: number;\n outcome?: string;\n outcomeData?: Record<string, unknown>;\n currentNode?: string;\n agentName?: string;\n transcript?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface AnalyticsConfig {\n apiKey: string;\n endpoint?: string;\n workspaceId: string;\n flushInterval?: number;\n maxBatchSize?: number;\n enableDebug?: boolean;\n}\n\nexport interface AnalyticsClient {\n track: (event: AnalyticsEvent) => Promise<void>;\n trackBatch: (events: AnalyticsEvent[]) => Promise<void>;\n trackVoiceCall: (data: VoiceCallData) => Promise<void>;\n updateVoiceCall: (sessionId: string, data: Partial<VoiceCallData>) => Promise<void>;\n flush: () => Promise<void>;\n setContext: (context: Partial<AnalyticsContext>) => void;\n identify: (userId: string, traits?: Record<string, unknown>) => void;\n}\n\nexport interface AnalyticsContext {\n workspaceId: string;\n agentId?: string;\n sessionId?: string;\n userId?: string;\n conversationId?: string;\n}\n\nexport interface BatcherOptions {\n maxBatchSize: number;\n flushInterval: number;\n onFlush: (events: AnalyticsEvent[]) => Promise<void>;\n enableDebug?: boolean;\n}\n\nexport class Batcher {\n private queue: AnalyticsEvent[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private options: BatcherOptions;\n\n constructor(options: BatcherOptions) {\n this.options = options;\n this.startFlushTimer();\n }\n\n add(event: AnalyticsEvent): void {\n this.queue.push(event);\n\n if (this.queue.length >= this.options.maxBatchSize) {\n this.flush();\n }\n }\n\n async flush(): Promise<void> {\n if (this.queue.length === 0) return;\n\n const events = [...this.queue];\n this.queue = [];\n\n if (this.options.enableDebug) {\n console.log(`[Analytics] Flushing ${events.length} events`);\n }\n\n try {\n await this.options.onFlush(events);\n } catch (error) {\n console.error(\"[Analytics] Flush failed:\", error);\n this.queue.unshift(...events);\n }\n }\n\n private startFlushTimer(): void {\n this.flushTimer = setInterval(() => {\n this.flush();\n }, this.options.flushInterval);\n }\n\n destroy(): void {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n this.flush();\n }\n}\n\nexport class AriaFlowAnalytics implements AnalyticsClient {\n private config: Required<AnalyticsConfig>;\n private batcher: Batcher;\n private context: AnalyticsContext;\n private userId?: string;\n private userTraits?: Record<string, unknown>;\n\n constructor(config: AnalyticsConfig) {\n this.config = {\n apiKey: config.apiKey,\n endpoint: config.endpoint ?? \"https://analytics.ariaflow.dev/api/v1\",\n workspaceId: config.workspaceId,\n flushInterval: config.flushInterval ?? 5000,\n maxBatchSize: config.maxBatchSize ?? 20,\n enableDebug: config.enableDebug ?? false,\n };\n\n this.context = {\n workspaceId: this.config.workspaceId,\n };\n\n this.batcher = new Batcher({\n maxBatchSize: this.config.maxBatchSize,\n flushInterval: this.config.flushInterval,\n onFlush: this.sendEvents.bind(this),\n enableDebug: this.config.enableDebug,\n });\n }\n\n async track(event: AnalyticsEvent): Promise<void> {\n const enrichedEvent = this.enrichEvent(event);\n this.batcher.add(enrichedEvent);\n }\n\n async trackBatch(events: AnalyticsEvent[]): Promise<void> {\n for (const event of events) {\n await this.track(event);\n }\n }\n\n async trackVoiceCall(data: VoiceCallData): Promise<void> {\n if (this.config.enableDebug) {\n console.log(\"[Analytics] Tracking voice call:\", data.sessionId);\n }\n\n const response = await fetch(`${this.config.endpoint}/voice-call`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify(data),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to track voice call: ${error}`);\n }\n }\n\n async updateVoiceCall(sessionId: string, data: Partial<VoiceCallData>): Promise<void> {\n if (this.config.enableDebug) {\n console.log(\"[Analytics] Updating voice call:\", sessionId);\n }\n\n const response = await fetch(`${this.config.endpoint}/voice-call/${sessionId}`, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify(data),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to update voice call: ${error}`);\n }\n }\n\n async flush(): Promise<void> {\n await this.batcher.flush();\n }\n\n setContext(context: Partial<AnalyticsContext>): void {\n this.context = { ...this.context, ...context };\n }\n\n identify(userId: string, traits?: Record<string, unknown>): void {\n this.userId = userId;\n this.userTraits = traits;\n }\n\n private enrichEvent(event: AnalyticsEvent): AnalyticsEvent {\n return {\n ...event,\n workspaceId: event.workspaceId ?? this.context.workspaceId,\n sessionId: event.sessionId ?? this.context.sessionId ?? \"\",\n agentId: event.agentId ?? this.context.agentId ?? \"\",\n conversationId: event.conversationId ?? this.context.conversationId,\n timestamp: event.timestamp ?? new Date(),\n data: {\n ...event.data,\n userId: event.data.userId ?? this.userId,\n userTraits: this.userTraits,\n },\n };\n }\n\n private async sendEvents(events: AnalyticsEvent[]): Promise<void> {\n const response = await fetch(`${this.config.endpoint}/events`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({ events }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to send events: ${error}`);\n }\n }\n\n destroy(): void {\n this.batcher.destroy();\n }\n}\n\nexport function createAnalyticsClient(config: AnalyticsConfig): AnalyticsClient {\n return new AriaFlowAnalytics(config);\n}\n\nexport default AriaFlowAnalytics;\n",
6
+ "import { useEffect, useRef, useCallback } from \"react\";\nimport {\n AriaFlowAnalytics,\n createAnalyticsClient,\n type AnalyticsConfig,\n type AnalyticsEvent,\n type AnalyticsContext,\n type VoiceCallData,\n} from \"./index.js\";\n\nexport interface UseAnalyticsOptions extends AnalyticsConfig {\n autoFlush?: boolean;\n}\n\nlet globalClient: AriaFlowAnalytics | null = null;\n\nexport function initAnalytics(config: AnalyticsConfig): AriaFlowAnalytics {\n if (globalClient) {\n globalClient.destroy();\n }\n globalClient = createAnalyticsClient(config) as AriaFlowAnalytics;\n return globalClient;\n}\n\nexport function getAnalyticsClient(): AriaFlowAnalytics | null {\n return globalClient;\n}\n\nexport function useAnalytics(config?: UseAnalyticsOptions): {\n track: (event: AnalyticsEvent) => Promise<void>;\n trackBatch: (events: AnalyticsEvent[]) => Promise<void>;\n trackVoiceCall: (data: VoiceCallData) => Promise<void>;\n updateVoiceCall: (sessionId: string, data: Partial<VoiceCallData>) => Promise<void>;\n flush: () => Promise<void>;\n setContext: (context: Partial<AnalyticsContext>) => void;\n identify: (userId: string, traits?: Record<string, unknown>) => void;\n} {\n const clientRef = useRef<AriaFlowAnalytics | null>(null);\n\n useEffect(() => {\n if (config && !clientRef.current) {\n clientRef.current = createAnalyticsClient(config) as AriaFlowAnalytics;\n }\n\n return () => {\n if (clientRef.current) {\n clientRef.current.destroy();\n clientRef.current = null;\n }\n };\n }, [config]);\n\n const client = clientRef.current ?? globalClient;\n\n const track = useCallback(\n async (event: AnalyticsEvent) => {\n if (!client) {\n console.warn(\"[Analytics] Client not initialized\");\n return;\n }\n await client.track(event);\n },\n [client]\n );\n\n const trackBatch = useCallback(\n async (events: AnalyticsEvent[]) => {\n if (!client) {\n console.warn(\"[Analytics] Client not initialized\");\n return;\n }\n await client.trackBatch(events);\n },\n [client]\n );\n\n const trackVoiceCall = useCallback(\n async (data: VoiceCallData) => {\n if (!client) {\n console.warn(\"[Analytics] Client not initialized\");\n return;\n }\n await client.trackVoiceCall(data);\n },\n [client]\n );\n\n const updateVoiceCall = useCallback(\n async (sessionId: string, data: Partial<VoiceCallData>) => {\n if (!client) {\n console.warn(\"[Analytics] Client not initialized\");\n return;\n }\n await client.updateVoiceCall(sessionId, data);\n },\n [client]\n );\n\n const flush = useCallback(async () => {\n if (!client) {\n console.warn(\"[Analytics] Client not initialized\");\n return;\n }\n await client.flush();\n }, [client]);\n\n const setContext = useCallback(\n (context: Partial<AnalyticsContext>) => {\n if (!client) {\n console.warn(\"[Analytics] Client not initialized\");\n return;\n }\n client.setContext(context);\n },\n [client]\n );\n\n const identify = useCallback(\n (userId: string, traits?: Record<string, unknown>) => {\n if (!client) {\n console.warn(\"[Analytics] Client not initialized\");\n return;\n }\n client.identify(userId, traits);\n },\n [client]\n );\n\n return {\n track,\n trackBatch,\n trackVoiceCall,\n updateVoiceCall,\n flush,\n setContext,\n identify,\n };\n}\n\nexport function AnalyticsProvider({\n children,\n config,\n}: {\n children: React.ReactNode;\n config: AnalyticsConfig;\n}): React.ReactElement {\n useEffect(() => {\n initAnalytics(config);\n return () => {\n const client = getAnalyticsClient();\n if (client) {\n client.destroy();\n }\n };\n }, [config]);\n\n return children as React.ReactElement;\n}\n\nexport function useTrackEvent(): (event: AnalyticsEvent) => Promise<void> {\n const { track } = useAnalytics();\n return track;\n}\n\nexport function usePageView(pageName: string, properties?: Record<string, unknown>): void {\n const { track } = useAnalytics();\n\n useEffect(() => {\n track({\n type: \"custom\",\n sessionId: \"\",\n agentId: \"\",\n workspaceId: \"\",\n data: {\n event: \"page_view\",\n page: pageName,\n ...properties,\n },\n });\n }, [pageName, properties, track]);\n}\n\nexport function useVoiceCallTracker(sessionId: string, workspaceId: string): {\n startCall: (agentId?: string) => Promise<void>;\n endCall: (outcome?: string, data?: Partial<VoiceCallData>) => Promise<void>;\n trackInterruption: () => void;\n trackUserSpeech: (durationMs: number) => void;\n trackAgentSpeech: (durationMs: number) => void;\n} {\n const { trackVoiceCall, updateVoiceCall } = useAnalytics();\n const callDataRef = useRef<Partial<VoiceCallData>>({\n sessionId,\n workspaceId,\n userTurns: 0,\n agentTurns: 0,\n interruptions: 0,\n totalUserSpeechMs: 0,\n totalAgentSpeechMs: 0,\n });\n\n const startCall = useCallback(\n async (agentId?: string) => {\n const data: VoiceCallData = {\n sessionId,\n workspaceId,\n agentId,\n startedAt: new Date(),\n };\n\n callDataRef.current = {\n ...callDataRef.current,\n ...data,\n };\n\n await trackVoiceCall(data);\n },\n [sessionId, workspaceId, trackVoiceCall]\n );\n\n const endCall = useCallback(\n async (outcome?: string, additionalData?: Partial<VoiceCallData>) => {\n const data: Partial<VoiceCallData> = {\n endedAt: new Date(),\n outcome,\n ...callDataRef.current,\n ...additionalData,\n };\n\n if (data.startedAt && data.endedAt) {\n data.durationSeconds = Math.floor(\n (data.endedAt.getTime() - data.startedAt.getTime()) / 1000\n );\n }\n\n await updateVoiceCall(sessionId, data);\n },\n [sessionId, updateVoiceCall]\n );\n\n const trackInterruption = useCallback(() => {\n callDataRef.current.interruptions = (callDataRef.current.interruptions ?? 0) + 1;\n updateVoiceCall(sessionId, {\n interruptions: callDataRef.current.interruptions,\n });\n }, [sessionId, updateVoiceCall]);\n\n const trackUserSpeech = useCallback(\n (durationMs: number) => {\n callDataRef.current.userTurns = (callDataRef.current.userTurns ?? 0) + 1;\n callDataRef.current.totalUserSpeechMs =\n (callDataRef.current.totalUserSpeechMs ?? 0) + durationMs;\n\n updateVoiceCall(sessionId, {\n userTurns: callDataRef.current.userTurns,\n totalUserSpeechMs: callDataRef.current.totalUserSpeechMs,\n });\n },\n [sessionId, updateVoiceCall]\n );\n\n const trackAgentSpeech = useCallback(\n (durationMs: number) => {\n callDataRef.current.agentTurns = (callDataRef.current.agentTurns ?? 0) + 1;\n callDataRef.current.totalAgentSpeechMs =\n (callDataRef.current.totalAgentSpeechMs ?? 0) + durationMs;\n\n updateVoiceCall(sessionId, {\n agentTurns: callDataRef.current.agentTurns,\n totalAgentSpeechMs: callDataRef.current.totalAgentSpeechMs,\n });\n },\n [sessionId, updateVoiceCall]\n );\n\n return {\n startCall,\n endCall,\n trackInterruption,\n trackUserSpeech,\n trackAgentSpeech,\n };\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "752A6A65667196E164756E2164756E21",
10
+ "names": []
11
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@ariaflowagents/analytics-sdk",
3
+ "version": "0.9.1",
4
+ "type": "module",
5
+ "description": "Type-safe SDK for sending analytics events to AriaFlow Analytics Platform",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./react": "./src/react.ts"
9
+ },
10
+ "main": "./src/index.ts",
11
+ "types": "./src/index.ts",
12
+ "scripts": {
13
+ "build": "~/.bun/bin/bun build ./src/index.ts ./src/react.ts --outdir ./dist --target bun --minify --sourcemap --external react",
14
+ "dev": "~/.bun/bin/bun build ./src/index.ts ./src/react.ts --outdir ./dist --target bun --watch --external react"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "dist"
19
+ ],
20
+ "keywords": [
21
+ "ariaflow",
22
+ "analytics",
23
+ "sdk",
24
+ "telemetry"
25
+ ],
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "zod": "^3.24.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bun": "latest",
32
+ "@types/react": "^19.0.0",
33
+ "react": "^19.0.0"
34
+ },
35
+ "peerDependencies": {
36
+ "react": ">=18.0.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "react": {
40
+ "optional": true
41
+ }
42
+ }
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,284 @@
1
+ export interface AnalyticsEvent {
2
+ id?: string;
3
+ timestamp?: Date;
4
+ sessionId: string;
5
+ conversationId?: string;
6
+ agentId: string;
7
+ workspaceId: string;
8
+ type: AnalyticsEventType;
9
+ data: Record<string, unknown>;
10
+ }
11
+
12
+ export type AnalyticsEventType =
13
+ | "conversation.started"
14
+ | "conversation.ended"
15
+ | "node.entered"
16
+ | "node.exited"
17
+ | "tool.called"
18
+ | "tool.completed"
19
+ | "tool.error"
20
+ | "booking.completed"
21
+ | "handoff.initiated"
22
+ | "emergency.detected"
23
+ | "call.started"
24
+ | "call.ended"
25
+ | "user.spoke"
26
+ | "agent.spoke"
27
+ | "user.interrupted"
28
+ | "silence.detected"
29
+ | "latency.stt"
30
+ | "latency.ttf"
31
+ | "latency.e2e"
32
+ | "latency.tts"
33
+ | "error.occurred"
34
+ | "custom";
35
+
36
+ export interface VoiceCallData {
37
+ sessionId: string;
38
+ workspaceId: string;
39
+ agentId?: string;
40
+ userName?: string;
41
+ userId?: string;
42
+ startedAt: Date;
43
+ endedAt?: Date;
44
+ durationSeconds?: number;
45
+ userTurns?: number;
46
+ agentTurns?: number;
47
+ interruptions?: number;
48
+ silenceEvents?: number;
49
+ errors?: number;
50
+ totalUserSpeechMs?: number;
51
+ totalAgentSpeechMs?: number;
52
+ totalSilenceMs?: number;
53
+ ttfMs?: number;
54
+ avgSttMs?: number;
55
+ avgTtsMs?: number;
56
+ e2eMs?: number;
57
+ outcome?: string;
58
+ outcomeData?: Record<string, unknown>;
59
+ currentNode?: string;
60
+ agentName?: string;
61
+ transcript?: string;
62
+ metadata?: Record<string, unknown>;
63
+ }
64
+
65
+ export interface AnalyticsConfig {
66
+ apiKey: string;
67
+ endpoint?: string;
68
+ workspaceId: string;
69
+ flushInterval?: number;
70
+ maxBatchSize?: number;
71
+ enableDebug?: boolean;
72
+ }
73
+
74
+ export interface AnalyticsClient {
75
+ track: (event: AnalyticsEvent) => Promise<void>;
76
+ trackBatch: (events: AnalyticsEvent[]) => Promise<void>;
77
+ trackVoiceCall: (data: VoiceCallData) => Promise<void>;
78
+ updateVoiceCall: (sessionId: string, data: Partial<VoiceCallData>) => Promise<void>;
79
+ flush: () => Promise<void>;
80
+ setContext: (context: Partial<AnalyticsContext>) => void;
81
+ identify: (userId: string, traits?: Record<string, unknown>) => void;
82
+ }
83
+
84
+ export interface AnalyticsContext {
85
+ workspaceId: string;
86
+ agentId?: string;
87
+ sessionId?: string;
88
+ userId?: string;
89
+ conversationId?: string;
90
+ }
91
+
92
+ export interface BatcherOptions {
93
+ maxBatchSize: number;
94
+ flushInterval: number;
95
+ onFlush: (events: AnalyticsEvent[]) => Promise<void>;
96
+ enableDebug?: boolean;
97
+ }
98
+
99
+ export class Batcher {
100
+ private queue: AnalyticsEvent[] = [];
101
+ private flushTimer: ReturnType<typeof setInterval> | null = null;
102
+ private options: BatcherOptions;
103
+
104
+ constructor(options: BatcherOptions) {
105
+ this.options = options;
106
+ this.startFlushTimer();
107
+ }
108
+
109
+ add(event: AnalyticsEvent): void {
110
+ this.queue.push(event);
111
+
112
+ if (this.queue.length >= this.options.maxBatchSize) {
113
+ this.flush();
114
+ }
115
+ }
116
+
117
+ async flush(): Promise<void> {
118
+ if (this.queue.length === 0) return;
119
+
120
+ const events = [...this.queue];
121
+ this.queue = [];
122
+
123
+ if (this.options.enableDebug) {
124
+ console.log(`[Analytics] Flushing ${events.length} events`);
125
+ }
126
+
127
+ try {
128
+ await this.options.onFlush(events);
129
+ } catch (error) {
130
+ console.error("[Analytics] Flush failed:", error);
131
+ this.queue.unshift(...events);
132
+ }
133
+ }
134
+
135
+ private startFlushTimer(): void {
136
+ this.flushTimer = setInterval(() => {
137
+ this.flush();
138
+ }, this.options.flushInterval);
139
+ }
140
+
141
+ destroy(): void {
142
+ if (this.flushTimer) {
143
+ clearInterval(this.flushTimer);
144
+ this.flushTimer = null;
145
+ }
146
+ this.flush();
147
+ }
148
+ }
149
+
150
+ export class AriaFlowAnalytics implements AnalyticsClient {
151
+ private config: Required<AnalyticsConfig>;
152
+ private batcher: Batcher;
153
+ private context: AnalyticsContext;
154
+ private userId?: string;
155
+ private userTraits?: Record<string, unknown>;
156
+
157
+ constructor(config: AnalyticsConfig) {
158
+ this.config = {
159
+ apiKey: config.apiKey,
160
+ endpoint: config.endpoint ?? "https://analytics.ariaflow.dev/api/v1",
161
+ workspaceId: config.workspaceId,
162
+ flushInterval: config.flushInterval ?? 5000,
163
+ maxBatchSize: config.maxBatchSize ?? 20,
164
+ enableDebug: config.enableDebug ?? false,
165
+ };
166
+
167
+ this.context = {
168
+ workspaceId: this.config.workspaceId,
169
+ };
170
+
171
+ this.batcher = new Batcher({
172
+ maxBatchSize: this.config.maxBatchSize,
173
+ flushInterval: this.config.flushInterval,
174
+ onFlush: this.sendEvents.bind(this),
175
+ enableDebug: this.config.enableDebug,
176
+ });
177
+ }
178
+
179
+ async track(event: AnalyticsEvent): Promise<void> {
180
+ const enrichedEvent = this.enrichEvent(event);
181
+ this.batcher.add(enrichedEvent);
182
+ }
183
+
184
+ async trackBatch(events: AnalyticsEvent[]): Promise<void> {
185
+ for (const event of events) {
186
+ await this.track(event);
187
+ }
188
+ }
189
+
190
+ async trackVoiceCall(data: VoiceCallData): Promise<void> {
191
+ if (this.config.enableDebug) {
192
+ console.log("[Analytics] Tracking voice call:", data.sessionId);
193
+ }
194
+
195
+ const response = await fetch(`${this.config.endpoint}/voice-call`, {
196
+ method: "POST",
197
+ headers: {
198
+ "Content-Type": "application/json",
199
+ Authorization: `Bearer ${this.config.apiKey}`,
200
+ },
201
+ body: JSON.stringify(data),
202
+ });
203
+
204
+ if (!response.ok) {
205
+ const error = await response.text();
206
+ throw new Error(`Failed to track voice call: ${error}`);
207
+ }
208
+ }
209
+
210
+ async updateVoiceCall(sessionId: string, data: Partial<VoiceCallData>): Promise<void> {
211
+ if (this.config.enableDebug) {
212
+ console.log("[Analytics] Updating voice call:", sessionId);
213
+ }
214
+
215
+ const response = await fetch(`${this.config.endpoint}/voice-call/${sessionId}`, {
216
+ method: "PUT",
217
+ headers: {
218
+ "Content-Type": "application/json",
219
+ Authorization: `Bearer ${this.config.apiKey}`,
220
+ },
221
+ body: JSON.stringify(data),
222
+ });
223
+
224
+ if (!response.ok) {
225
+ const error = await response.text();
226
+ throw new Error(`Failed to update voice call: ${error}`);
227
+ }
228
+ }
229
+
230
+ async flush(): Promise<void> {
231
+ await this.batcher.flush();
232
+ }
233
+
234
+ setContext(context: Partial<AnalyticsContext>): void {
235
+ this.context = { ...this.context, ...context };
236
+ }
237
+
238
+ identify(userId: string, traits?: Record<string, unknown>): void {
239
+ this.userId = userId;
240
+ this.userTraits = traits;
241
+ }
242
+
243
+ private enrichEvent(event: AnalyticsEvent): AnalyticsEvent {
244
+ return {
245
+ ...event,
246
+ workspaceId: event.workspaceId ?? this.context.workspaceId,
247
+ sessionId: event.sessionId ?? this.context.sessionId ?? "",
248
+ agentId: event.agentId ?? this.context.agentId ?? "",
249
+ conversationId: event.conversationId ?? this.context.conversationId,
250
+ timestamp: event.timestamp ?? new Date(),
251
+ data: {
252
+ ...event.data,
253
+ userId: event.data.userId ?? this.userId,
254
+ userTraits: this.userTraits,
255
+ },
256
+ };
257
+ }
258
+
259
+ private async sendEvents(events: AnalyticsEvent[]): Promise<void> {
260
+ const response = await fetch(`${this.config.endpoint}/events`, {
261
+ method: "POST",
262
+ headers: {
263
+ "Content-Type": "application/json",
264
+ Authorization: `Bearer ${this.config.apiKey}`,
265
+ },
266
+ body: JSON.stringify({ events }),
267
+ });
268
+
269
+ if (!response.ok) {
270
+ const error = await response.text();
271
+ throw new Error(`Failed to send events: ${error}`);
272
+ }
273
+ }
274
+
275
+ destroy(): void {
276
+ this.batcher.destroy();
277
+ }
278
+ }
279
+
280
+ export function createAnalyticsClient(config: AnalyticsConfig): AnalyticsClient {
281
+ return new AriaFlowAnalytics(config);
282
+ }
283
+
284
+ export default AriaFlowAnalytics;
package/src/react.ts ADDED
@@ -0,0 +1,282 @@
1
+ import { useEffect, useRef, useCallback } from "react";
2
+ import {
3
+ AriaFlowAnalytics,
4
+ createAnalyticsClient,
5
+ type AnalyticsConfig,
6
+ type AnalyticsEvent,
7
+ type AnalyticsContext,
8
+ type VoiceCallData,
9
+ } from "./index.js";
10
+
11
+ export interface UseAnalyticsOptions extends AnalyticsConfig {
12
+ autoFlush?: boolean;
13
+ }
14
+
15
+ let globalClient: AriaFlowAnalytics | null = null;
16
+
17
+ export function initAnalytics(config: AnalyticsConfig): AriaFlowAnalytics {
18
+ if (globalClient) {
19
+ globalClient.destroy();
20
+ }
21
+ globalClient = createAnalyticsClient(config) as AriaFlowAnalytics;
22
+ return globalClient;
23
+ }
24
+
25
+ export function getAnalyticsClient(): AriaFlowAnalytics | null {
26
+ return globalClient;
27
+ }
28
+
29
+ export function useAnalytics(config?: UseAnalyticsOptions): {
30
+ track: (event: AnalyticsEvent) => Promise<void>;
31
+ trackBatch: (events: AnalyticsEvent[]) => Promise<void>;
32
+ trackVoiceCall: (data: VoiceCallData) => Promise<void>;
33
+ updateVoiceCall: (sessionId: string, data: Partial<VoiceCallData>) => Promise<void>;
34
+ flush: () => Promise<void>;
35
+ setContext: (context: Partial<AnalyticsContext>) => void;
36
+ identify: (userId: string, traits?: Record<string, unknown>) => void;
37
+ } {
38
+ const clientRef = useRef<AriaFlowAnalytics | null>(null);
39
+
40
+ useEffect(() => {
41
+ if (config && !clientRef.current) {
42
+ clientRef.current = createAnalyticsClient(config) as AriaFlowAnalytics;
43
+ }
44
+
45
+ return () => {
46
+ if (clientRef.current) {
47
+ clientRef.current.destroy();
48
+ clientRef.current = null;
49
+ }
50
+ };
51
+ }, [config]);
52
+
53
+ const client = clientRef.current ?? globalClient;
54
+
55
+ const track = useCallback(
56
+ async (event: AnalyticsEvent) => {
57
+ if (!client) {
58
+ console.warn("[Analytics] Client not initialized");
59
+ return;
60
+ }
61
+ await client.track(event);
62
+ },
63
+ [client]
64
+ );
65
+
66
+ const trackBatch = useCallback(
67
+ async (events: AnalyticsEvent[]) => {
68
+ if (!client) {
69
+ console.warn("[Analytics] Client not initialized");
70
+ return;
71
+ }
72
+ await client.trackBatch(events);
73
+ },
74
+ [client]
75
+ );
76
+
77
+ const trackVoiceCall = useCallback(
78
+ async (data: VoiceCallData) => {
79
+ if (!client) {
80
+ console.warn("[Analytics] Client not initialized");
81
+ return;
82
+ }
83
+ await client.trackVoiceCall(data);
84
+ },
85
+ [client]
86
+ );
87
+
88
+ const updateVoiceCall = useCallback(
89
+ async (sessionId: string, data: Partial<VoiceCallData>) => {
90
+ if (!client) {
91
+ console.warn("[Analytics] Client not initialized");
92
+ return;
93
+ }
94
+ await client.updateVoiceCall(sessionId, data);
95
+ },
96
+ [client]
97
+ );
98
+
99
+ const flush = useCallback(async () => {
100
+ if (!client) {
101
+ console.warn("[Analytics] Client not initialized");
102
+ return;
103
+ }
104
+ await client.flush();
105
+ }, [client]);
106
+
107
+ const setContext = useCallback(
108
+ (context: Partial<AnalyticsContext>) => {
109
+ if (!client) {
110
+ console.warn("[Analytics] Client not initialized");
111
+ return;
112
+ }
113
+ client.setContext(context);
114
+ },
115
+ [client]
116
+ );
117
+
118
+ const identify = useCallback(
119
+ (userId: string, traits?: Record<string, unknown>) => {
120
+ if (!client) {
121
+ console.warn("[Analytics] Client not initialized");
122
+ return;
123
+ }
124
+ client.identify(userId, traits);
125
+ },
126
+ [client]
127
+ );
128
+
129
+ return {
130
+ track,
131
+ trackBatch,
132
+ trackVoiceCall,
133
+ updateVoiceCall,
134
+ flush,
135
+ setContext,
136
+ identify,
137
+ };
138
+ }
139
+
140
+ export function AnalyticsProvider({
141
+ children,
142
+ config,
143
+ }: {
144
+ children: React.ReactNode;
145
+ config: AnalyticsConfig;
146
+ }): React.ReactElement {
147
+ useEffect(() => {
148
+ initAnalytics(config);
149
+ return () => {
150
+ const client = getAnalyticsClient();
151
+ if (client) {
152
+ client.destroy();
153
+ }
154
+ };
155
+ }, [config]);
156
+
157
+ return children as React.ReactElement;
158
+ }
159
+
160
+ export function useTrackEvent(): (event: AnalyticsEvent) => Promise<void> {
161
+ const { track } = useAnalytics();
162
+ return track;
163
+ }
164
+
165
+ export function usePageView(pageName: string, properties?: Record<string, unknown>): void {
166
+ const { track } = useAnalytics();
167
+
168
+ useEffect(() => {
169
+ track({
170
+ type: "custom",
171
+ sessionId: "",
172
+ agentId: "",
173
+ workspaceId: "",
174
+ data: {
175
+ event: "page_view",
176
+ page: pageName,
177
+ ...properties,
178
+ },
179
+ });
180
+ }, [pageName, properties, track]);
181
+ }
182
+
183
+ export function useVoiceCallTracker(sessionId: string, workspaceId: string): {
184
+ startCall: (agentId?: string) => Promise<void>;
185
+ endCall: (outcome?: string, data?: Partial<VoiceCallData>) => Promise<void>;
186
+ trackInterruption: () => void;
187
+ trackUserSpeech: (durationMs: number) => void;
188
+ trackAgentSpeech: (durationMs: number) => void;
189
+ } {
190
+ const { trackVoiceCall, updateVoiceCall } = useAnalytics();
191
+ const callDataRef = useRef<Partial<VoiceCallData>>({
192
+ sessionId,
193
+ workspaceId,
194
+ userTurns: 0,
195
+ agentTurns: 0,
196
+ interruptions: 0,
197
+ totalUserSpeechMs: 0,
198
+ totalAgentSpeechMs: 0,
199
+ });
200
+
201
+ const startCall = useCallback(
202
+ async (agentId?: string) => {
203
+ const data: VoiceCallData = {
204
+ sessionId,
205
+ workspaceId,
206
+ agentId,
207
+ startedAt: new Date(),
208
+ };
209
+
210
+ callDataRef.current = {
211
+ ...callDataRef.current,
212
+ ...data,
213
+ };
214
+
215
+ await trackVoiceCall(data);
216
+ },
217
+ [sessionId, workspaceId, trackVoiceCall]
218
+ );
219
+
220
+ const endCall = useCallback(
221
+ async (outcome?: string, additionalData?: Partial<VoiceCallData>) => {
222
+ const data: Partial<VoiceCallData> = {
223
+ endedAt: new Date(),
224
+ outcome,
225
+ ...callDataRef.current,
226
+ ...additionalData,
227
+ };
228
+
229
+ if (data.startedAt && data.endedAt) {
230
+ data.durationSeconds = Math.floor(
231
+ (data.endedAt.getTime() - data.startedAt.getTime()) / 1000
232
+ );
233
+ }
234
+
235
+ await updateVoiceCall(sessionId, data);
236
+ },
237
+ [sessionId, updateVoiceCall]
238
+ );
239
+
240
+ const trackInterruption = useCallback(() => {
241
+ callDataRef.current.interruptions = (callDataRef.current.interruptions ?? 0) + 1;
242
+ updateVoiceCall(sessionId, {
243
+ interruptions: callDataRef.current.interruptions,
244
+ });
245
+ }, [sessionId, updateVoiceCall]);
246
+
247
+ const trackUserSpeech = useCallback(
248
+ (durationMs: number) => {
249
+ callDataRef.current.userTurns = (callDataRef.current.userTurns ?? 0) + 1;
250
+ callDataRef.current.totalUserSpeechMs =
251
+ (callDataRef.current.totalUserSpeechMs ?? 0) + durationMs;
252
+
253
+ updateVoiceCall(sessionId, {
254
+ userTurns: callDataRef.current.userTurns,
255
+ totalUserSpeechMs: callDataRef.current.totalUserSpeechMs,
256
+ });
257
+ },
258
+ [sessionId, updateVoiceCall]
259
+ );
260
+
261
+ const trackAgentSpeech = useCallback(
262
+ (durationMs: number) => {
263
+ callDataRef.current.agentTurns = (callDataRef.current.agentTurns ?? 0) + 1;
264
+ callDataRef.current.totalAgentSpeechMs =
265
+ (callDataRef.current.totalAgentSpeechMs ?? 0) + durationMs;
266
+
267
+ updateVoiceCall(sessionId, {
268
+ agentTurns: callDataRef.current.agentTurns,
269
+ totalAgentSpeechMs: callDataRef.current.totalAgentSpeechMs,
270
+ });
271
+ },
272
+ [sessionId, updateVoiceCall]
273
+ );
274
+
275
+ return {
276
+ startCall,
277
+ endCall,
278
+ trackInterruption,
279
+ trackUserSpeech,
280
+ trackAgentSpeech,
281
+ };
282
+ }