Analytics & Observability
Track conversation metrics, user behavior, and flow performance with FlowKit's analytics system.
Overview
The AnalyticsCollector class provides:
- Event tracking
- Conversation metrics
- Flow performance analysis
- Funnel visualization
- Export capabilities
Basic Usage
typescript
import { AnalyticsCollector } from "@andresaya/flowkit";
const analytics = new AnalyticsCollector();
// Track events
analytics.track({
type: "conversation:start",
conversationId: "user-123",
timestamp: Date.now(),
});
analytics.track({
type: "step:enter",
conversationId: "user-123",
timestamp: Date.now(),
data: { stepId: "greeting" },
});
analytics.track({
type: "extract:success",
conversationId: "user-123",
timestamp: Date.now(),
data: { extractType: "name", value: "John" },
});Integration with FlowEngine
typescript
import { FlowEngine, AnalyticsCollector, EngineEvent } from "@andresaya/flowkit";
const analytics = new AnalyticsCollector();
// Create engine with event handler
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: (event: EngineEvent) => {
// Track all events to analytics
if (event.type === "step:enter") {
analytics.track({
type: "step:enter",
conversationId: event.conversationId,
timestamp: Date.now(),
data: { stepId: event.step },
});
}
if (event.type === "extract:success") {
analytics.track({
type: "extract:success",
conversationId: event.conversationId,
timestamp: Date.now(),
data: {
slot: event.slot,
value: event.value,
},
});
}
if (event.type === "flow:end") {
analytics.track({
type: "conversation:end",
conversationId: event.conversationId,
timestamp: Date.now(),
data: { reason: "completed" },
});
}
}
});Event Types
typescript
type AnalyticsEventType =
| "conversation:start" // Conversation started
| "conversation:end" // Conversation ended
| "step:enter" // Entered a step
| "step:exit" // Left a step
| "extract:success" // Successfully extracted data
| "extract:fail" // Failed to extract data
| "handoff:request" // Human handoff requested
| "timeout" // Conversation timed out
| "error" // Error occurred
| "custom"; // Custom eventGetting Metrics
Conversation Metrics
typescript
const conversationMetrics = analytics.getConversationMetrics("user-123");
console.log(conversationMetrics);
// {
// conversationId: "user-123",
// startTime: 1703123456789,
// endTime: 1703123556789,
// duration: 100000, // ms
// messageCount: 5,
// stepsVisited: ["greeting", "get_name", "confirm", "done"],
// extractionSuccessRate: 0.8,
// completed: true,
// handoffRequested: false,
// }Flow Metrics
typescript
const flowMetrics = analytics.getFlowMetrics();
console.log(flowMetrics);
// {
// totalConversations: 150,
// completedConversations: 120,
// completionRate: 0.8,
// averageDuration: 45000, // ms
// handoffRate: 0.05,
// stepMetrics: {
// "greeting": { visits: 150, avgTime: 5000 },
// "get_name": { visits: 145, avgTime: 8000 },
// "confirm": { visits: 130, avgTime: 3000 },
// },
// extractionMetrics: {
// "name": { attempts: 145, successes: 140, rate: 0.97 },
// "email": { attempts: 130, successes: 120, rate: 0.92 },
// },
// }Funnel Analysis
typescript
const funnel = analytics.getFunnel(["greeting", "get_name", "get_email", "confirm", "done"]);
console.log(funnel);
// {
// steps: [
// { step: "greeting", count: 1000, dropoff: 0 },
// { step: "get_name", count: 950, dropoff: 0.05 },
// { step: "get_email", count: 800, dropoff: 0.16 },
// { step: "confirm", count: 750, dropoff: 0.06 },
// { step: "done", count: 700, dropoff: 0.07 },
// ],
// overallConversion: 0.70,
// }Exporting Data
Export All Events
typescript
const allEvents = analytics.export();
// Returns: AnalyticsEvent[]
// Save to file
import { writeFileSync } from "fs";
writeFileSync("analytics.json", JSON.stringify(allEvents, null, 2));Export to Custom Format
typescript
// Export for external analytics (Google Analytics, Mixpanel, etc.)
const events = analytics.export();
for (const event of events) {
await mixpanel.track(event.type, {
distinct_id: event.conversationId,
timestamp: new Date(event.timestamp),
...event.data,
});
}Custom Events
Track custom business events:
typescript
// Track product interest
analytics.track({
type: "custom",
conversationId: "user-123",
timestamp: Date.now(),
data: {
name: "product_viewed",
productId: "PRD-001",
productName: "Premium Plan",
},
});
// Track purchase intent
analytics.track({
type: "custom",
conversationId: "user-123",
timestamp: Date.now(),
data: {
name: "purchase_intent",
value: 500,
currency: "USD",
},
});Real-time Dashboard
Build a simple real-time dashboard:
typescript
import { AnalyticsCollector, FlowEngine } from "@andresaya/flowkit";
const analytics = new AnalyticsCollector();
// Update dashboard every 5 seconds
setInterval(() => {
const metrics = analytics.getFlowMetrics();
console.clear();
console.log("=== FlowKit Dashboard ===");
console.log(`Active Conversations: ${metrics.totalConversations - metrics.completedConversations}`);
console.log(`Completion Rate: ${(metrics.completionRate * 100).toFixed(1)}%`);
console.log(`Avg Duration: ${(metrics.averageDuration / 1000).toFixed(1)}s`);
console.log(`Handoff Rate: ${(metrics.handoffRate * 100).toFixed(1)}%`);
console.log("");
console.log("Step Performance:");
for (const [step, data] of Object.entries(metrics.stepMetrics)) {
console.log(` ${step}: ${data.visits} visits, ${(data.avgTime / 1000).toFixed(1)}s avg`);
}
}, 5000);Retention & Cleanup
Manage analytics data size:
typescript
// Clear old events (older than 7 days)
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
analytics.clearBefore(oneWeekAgo);
// Or export and clear
const events = analytics.export();
await saveToDatabase(events);
analytics.clear();Integration Examples
With Prometheus/Grafana
typescript
import { Registry, Counter, Histogram } from "prom-client";
const registry = new Registry();
const conversationsTotal = new Counter({
name: "flowkit_conversations_total",
help: "Total conversations",
labelNames: ["status"],
registers: [registry],
});
const stepDuration = new Histogram({
name: "flowkit_step_duration_seconds",
help: "Time spent in each step",
labelNames: ["step"],
buckets: [1, 5, 10, 30, 60],
registers: [registry],
});
const stepTimers = new Map<string, number>();
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: (event) => {
if (event.type === "flow:end") {
conversationsTotal.inc({ status: "completed" });
}
if (event.type === "step:enter") {
stepTimers.set(`${event.conversationId}:${event.step}`, Date.now());
}
if (event.type === "step:exit") {
const start = stepTimers.get(`${event.conversationId}:${event.step}`);
if (start) {
const duration = (Date.now() - start) / 1000;
stepDuration.observe({ step: event.step }, duration);
stepTimers.delete(`${event.conversationId}:${event.step}`);
}
}
}
});With Database Storage
typescript
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
async function saveAnalytics(event: AnalyticsEvent) {
await pool.query(`
INSERT INTO analytics_events
(conversation_id, event_type, timestamp, data)
VALUES ($1, $2, $3, $4)
`, [
event.conversationId,
event.type,
new Date(event.timestamp),
JSON.stringify(event.data),
]);
}Tips
- Track meaningful events - Focus on business-relevant metrics
- Use consistent naming - Establish event naming conventions
- Don't over-track - Too many events can be noise
- Export regularly - Don't let in-memory data grow too large
- Set up alerts - Monitor for anomalies (high handoff rate, low completion)