Appearance
This guide focuses on monitoring application-level metrics, user behavior analytics, business KPIs, and performance tracking for Pocketbook.
Overview
While deployment monitoring (internal documentation) covers infrastructure and system metrics, this guide focuses on:
- Application performance metrics
- User behavior analytics
- Business KPIs
- Custom application events
- Feature usage tracking
Application Performance Monitoring
Response Time Tracking
typescript
// src/middleware/performance.ts
import { Request, Response, NextFunction } from 'express';
interface PerformanceMetrics {
endpoint: string;
method: string;
duration: number;
statusCode: number;
timestamp: Date;
}
export function performanceMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const start = process.hrtime.bigint();
// Capture response
res.on('finish', () => {
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1_000_000; // Convert to ms
const metrics: PerformanceMetrics = {
endpoint: req.route?.path || req.path,
method: req.method,
duration,
statusCode: res.statusCode,
timestamp: new Date(),
};
// Log metrics
logPerformance(metrics);
// Send to monitoring service
sendMetrics(metrics);
});
next();
}
function logPerformance(metrics: PerformanceMetrics) {
logger.info('Request completed', {
...metrics,
slow: metrics.duration > 1000, // Flag slow requests
});
}
function sendMetrics(metrics: PerformanceMetrics) {
// Send to Prometheus, Datadog, etc.
prometheusMetrics.histogram('http_request_duration_ms', metrics.duration, {
endpoint: metrics.endpoint,
method: metrics.method,
status: metrics.statusCode.toString(),
});
}Database Query Performance
typescript
// src/monitoring/database.ts
import { Pool } from 'pg';
class MonitoredPool extends Pool {
async query(...args: any[]) {
const start = Date.now();
const query = args[0];
try {
const result = await super.query(...args);
const duration = Date.now() - start;
// Log slow queries
if (duration > 100) {
logger.warn('Slow query detected', {
query: typeof query === 'string' ? query : query.text,
duration,
rows: result.rowCount,
});
}
// Track metrics
metrics.histogram('db_query_duration', duration, {
slow: duration > 100,
});
return result;
} catch (error) {
metrics.increment('db_query_errors');
throw error;
}
}
}
export const db = new MonitoredPool({
connectionString: process.env.DATABASE_URL,
});User Behavior Analytics
Event Tracking
typescript
// src/analytics/events.ts
interface AnalyticsEvent {
userId?: string;
sessionId: string;
event: string;
properties: Record<string, any>;
timestamp: Date;
}
class Analytics {
track(event: string, properties: Record<string, any> = {}) {
const analyticsEvent: AnalyticsEvent = {
userId: this.getUserId(),
sessionId: this.getSessionId(),
event,
properties,
timestamp: new Date(),
};
// Send to analytics service
this.send(analyticsEvent);
}
private send(event: AnalyticsEvent) {
// Send to Mixpanel, Amplitude, Segment, etc.
if (process.env.ANALYTICS_ENABLED === 'true') {
fetch(process.env.ANALYTICS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
}
// Also store locally for analysis
db.analyticsEvents.create(event);
}
private getUserId(): string | undefined {
// Get from request context
return RequestContext.getCurrentUserId();
}
private getSessionId(): string {
return RequestContext.getSessionId();
}
}
export const analytics = new Analytics();
// Usage
analytics.track('transaction_created', {
amount: 5000,
currency: 'USD',
paymentMethod: 'card',
});
analytics.track('user_signed_up', {
method: 'email',
referralSource: 'google',
});Feature Usage Tracking
typescript
// src/analytics/features.ts
interface FeatureUsage {
feature: string;
userId: string;
action: 'viewed' | 'clicked' | 'completed';
metadata?: Record<string, any>;
}
export function trackFeatureUsage(feature: string, action: 'viewed' | 'clicked' | 'completed', metadata?: Record<string, any>) {
const usage: FeatureUsage = {
feature,
userId: RequestContext.getCurrentUserId(),
action,
metadata,
};
// Increment feature usage counter
metrics.increment('feature_usage', {
feature,
action,
});
// Store for analysis
db.featureUsage.create({
...usage,
timestamp: new Date(),
});
// Track in analytics
analytics.track(`feature_${action}`, {
feature,
...metadata,
});
}
// Usage in routes
app.get('/api/analytics/dashboard', async (req, res) => {
trackFeatureUsage('analytics_dashboard', 'viewed');
const data = await getAnalyticsDashboard();
res.json(data);
});
app.post('/api/export', async (req, res) => {
trackFeatureUsage('data_export', 'clicked', {
format: req.body.format,
});
// ... export logic
});Business Metrics
Revenue Tracking
typescript
// src/monitoring/business.ts
interface RevenueMetrics {
totalRevenue: number;
transactionCount: number;
averageTransactionValue: number;
period: 'hour' | 'day' | 'month';
}
class BusinessMetrics {
async trackRevenue(amount: number, currency: string) {
// Convert to USD for consistent tracking
const usdAmount = await this.convertToUSD(amount, currency);
// Increment revenue counter
metrics.counter('revenue_usd', usdAmount);
// Track transaction count
metrics.increment('transaction_count');
// Store for historical analysis
await db.revenueMetrics.create({
amount: usdAmount,
currency: 'USD',
originalAmount: amount,
originalCurrency: currency,
timestamp: new Date(),
});
}
async getRevenueMetrics(period: 'hour' | 'day' | 'month'): Promise<RevenueMetrics> {
const since = this.getPeriodStart(period);
const transactions = await db.transactions.find({
createdAt: { $gte: since },
status: 'completed',
});
const totalRevenue = transactions.reduce((sum, t) => sum + t.amount, 0);
const transactionCount = transactions.length;
const averageTransactionValue = totalRevenue / transactionCount || 0;
return {
totalRevenue,
transactionCount,
averageTransactionValue,
period,
};
}
private getPeriodStart(period: 'hour' | 'day' | 'month'): Date {
const now = new Date();
switch (period) {
case 'hour':
now.setHours(now.getHours() - 1);
break;
case 'day':
now.setDate(now.getDate() - 1);
break;
case 'month':
now.setMonth(now.getMonth() - 1);
break;
}
return now;
}
private async convertToUSD(amount: number, currency: string): Promise<number> {
if (currency === 'USD') return amount;
// Implement currency conversion
const rate = await this.getExchangeRate(currency, 'USD');
return amount * rate;
}
private async getExchangeRate(from: string, to: string): Promise<number> {
// Fetch from exchange rate API
return 1; // Placeholder
}
}
export const businessMetrics = new BusinessMetrics();
// Usage
app.post('/api/transactions', async (req, res) => {
const transaction = await createTransaction(req.body);
await businessMetrics.trackRevenue(
transaction.amount,
transaction.currency
);
res.json(transaction);
});User Growth Tracking
typescript
// src/monitoring/growth.ts
interface GrowthMetrics {
newUsers: number;
activeUsers: number;
churnedUsers: number;
retentionRate: number;
period: string;
}
export async function trackUserGrowth(): Promise<GrowthMetrics> {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
// New users today
const newUsers = await db.users.count({
createdAt: { $gte: yesterday },
});
// Active users (logged in within last 24h)
const activeUsers = await db.users.count({
lastLoginAt: { $gte: yesterday },
});
// Churned users (no activity in 30 days)
const thirtyDaysAgo = new Date(today);
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const churnedUsers = await db.users.count({
lastLoginAt: { $lt: thirtyDaysAgo },
status: 'active',
});
// Calculate retention rate
const totalUsers = await db.users.count();
const retentionRate = ((totalUsers - churnedUsers) / totalUsers) * 100;
const metrics: GrowthMetrics = {
newUsers,
activeUsers,
churnedUsers,
retentionRate,
period: 'daily',
};
// Send to monitoring
Object.entries(metrics).forEach(([key, value]) => {
if (typeof value === 'number') {
prometheusMetrics.gauge(`user_${key}`, value);
}
});
return metrics;
}
// Run daily
cron.schedule('0 0 * * *', async () => {
const metrics = await trackUserGrowth();
logger.info('Daily growth metrics', metrics);
});Custom Dashboards
Grafana Dashboard Configuration
json
{
"dashboard": {
"title": "Pocketbook Business Metrics",
"panels": [
{
"title": "Revenue (24h)",
"targets": [
{
"expr": "sum(revenue_usd)",
"legendFormat": "Total Revenue"
}
],
"type": "stat"
},
{
"title": "Transaction Rate",
"targets": [
{
"expr": "rate(transaction_count[5m])",
"legendFormat": "Transactions/sec"
}
],
"type": "graph"
},
{
"title": "Active Users",
"targets": [
{
"expr": "user_activeUsers",
"legendFormat": "Active Users (24h)"
}
],
"type": "stat"
},
{
"title": "Feature Usage",
"targets": [
{
"expr": "topk(10, sum by (feature) (feature_usage))",
"legendFormat": "{{feature}}"
}
],
"type": "bar"
}
]
}
}Error Monitoring
Error Rate Tracking
typescript
// src/monitoring/errors.ts
interface ErrorMetrics {
type: string;
count: number;
lastOccurrence: Date;
affectedUsers: string[];
}
class ErrorMonitor {
private errors: Map<string, ErrorMetrics> = new Map();
trackError(error: Error, userId?: string) {
const errorType = error.constructor.name;
// Update error metrics
const existing = this.errors.get(errorType) || {
type: errorType,
count: 0,
lastOccurrence: new Date(),
affectedUsers: [],
};
existing.count++;
existing.lastOccurrence = new Date();
if (userId && !existing.affectedUsers.includes(userId)) {
existing.affectedUsers.push(userId);
}
this.errors.set(errorType, existing);
// Send to monitoring
metrics.increment('application_errors', {
type: errorType,
});
// Alert on high error rate
if (this.isHighErrorRate(errorType)) {
this.alertTeam(errorType, existing);
}
// Send to Sentry
Sentry.captureException(error, {
tags: { errorType },
user: userId ? { id: userId } : undefined,
});
}
private isHighErrorRate(errorType: string): boolean {
const metrics = this.errors.get(errorType);
if (!metrics) return false;
// Alert if more than 10 errors in last 5 minutes
return metrics.count > 10;
}
private alertTeam(errorType: string, metrics: ErrorMetrics) {
// Send alert to Slack, PagerDuty, etc.
}
getErrorSummary(): ErrorMetrics[] {
return Array.from(this.errors.values())
.sort((a, b) => b.count - a.count)
.slice(0, 10); // Top 10 errors
}
}
export const errorMonitor = new ErrorMonitor();
// Global error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
errorMonitor.trackError(err, req.user?.id);
res.status(500).json({
error: {
message: 'Internal server error',
requestId: req.id,
},
});
});Real-Time Monitoring
WebSocket Connection Tracking
typescript
// src/monitoring/websockets.ts
class WebSocketMonitor {
private connections: Set<string> = new Set();
onConnect(connectionId: string) {
this.connections.add(connectionId);
metrics.gauge('websocket_connections', this.connections.size);
logger.info('WebSocket connected', {
connectionId,
totalConnections: this.connections.size,
});
}
onDisconnect(connectionId: string) {
this.connections.delete(connectionId);
metrics.gauge('websocket_connections', this.connections.size);
logger.info('WebSocket disconnected', {
connectionId,
totalConnections: this.connections.size,
});
}
getConnectionCount(): number {
return this.connections.size;
}
}
export const wsMonitor = new WebSocketMonitor();Performance Budgets
Set Performance Targets
typescript
// src/monitoring/budgets.ts
interface PerformanceBudget {
endpoint: string;
maxDuration: number; // milliseconds
p95Threshold: number;
p99Threshold: number;
}
const budgets: PerformanceBudget[] = [
{
endpoint: '/api/transactions',
maxDuration: 500,
p95Threshold: 300,
p99Threshold: 400,
},
{
endpoint: '/api/users',
maxDuration: 200,
p95Threshold: 150,
p99Threshold: 180,
},
];
export function checkPerformanceBudget(
endpoint: string,
duration: number
): boolean {
const budget = budgets.find((b) => b.endpoint === endpoint);
if (!budget) return true;
if (duration > budget.maxDuration) {
logger.warn('Performance budget exceeded', {
endpoint,
duration,
budget: budget.maxDuration,
exceeded: duration - budget.maxDuration,
});
metrics.increment('performance_budget_exceeded', { endpoint });
return false;
}
return true;
}Monitoring Best Practices
- Track the right metrics: Focus on actionable metrics
- Set up alerts: Be notified of issues before users report them
- Monitor trends: Look for patterns over time
- Segment data: Break down metrics by user cohorts, regions, etc.
- Privacy-first: Anonymize user data in analytics
- Performance budgets: Set and enforce performance targets
- Business metrics: Track KPIs alongside technical metrics
- Regular reviews: Review dashboards weekly
- A/B testing: Monitor impact of feature changes
- User feedback: Combine metrics with qualitative feedback
Compliance & Privacy
GDPR-Compliant Analytics
typescript
// src/analytics/gdpr.ts
export function trackEvent(event: string, properties: any, consent: boolean) {
if (!consent) {
// Only track anonymized metrics
metrics.increment(event);
return;
}
// Full tracking with user consent
analytics.track(event, properties);
}
// Right to deletion
export async function deleteUserAnalytics(userId: string) {
await db.analyticsEvents.deleteMany({ userId });
await db.featureUsage.deleteMany({ userId });
logger.info('User analytics deleted', { userId });
}Next Steps
- Deployment Monitoring (internal documentation) - Infrastructure monitoring
- Error Handling - API error tracking
- Performance Guide - Optimization strategies
- Analytics Integration - Third-party analytics
