This document explains the comprehensive analytics and logging system implemented for the ZurichJS Conference platform. The system provides:
- Type-safe analytics tracking using PostHog
- Centralized, structured logging that replaces console.log
- Revenue analytics for conversion and sales tracking
- Error monitoring with contextual metadata
- Extensible event system that's easy to evolve
The analytics system is built around three core modules:
src/lib/analytics/events.ts- Type definitions for all analytics eventssrc/lib/analytics/client.ts- Browser-side analytics clientsrc/lib/analytics/server.ts- Server-side analytics clientsrc/lib/analytics/helpers.ts- Convenience functions for common tracking scenarios
- Full Type Safety: All events and properties are strictly typed
- Discriminated Unions: Ensures correct properties for each event type
- Client & Server Support: Works in both browser and Node.js environments
- Automatic Enrichment: Common properties (timestamp, URL, user agent) added automatically
- Revenue Tracking: Proper formatting for PostHog revenue analytics
All events are defined as a discriminated union in events.ts. This provides:
- Autocomplete in your IDE
- Type checking at compile time
- Centralized event documentation
- Easy refactoring and evolution
Event Categories:
- Page Events:
page_viewed,user_identified - Ticket Events:
ticket_viewed,ticket_added_to_cart,ticket_purchased,ticket_transferred,ticket_validated,ticket_checked_in - Workshop Events:
workshop_viewed,workshop_voucher_purchased,workshop_registered,workshop_cancelled - Checkout Events:
checkout_started,checkout_completed,checkout_abandoned - Payment Events:
payment_succeeded,payment_failed - Engagement Events:
button_clicked,form_submitted,form_error,link_clicked - Feature Events:
search_performed,filter_applied,share_clicked - Error Events:
error_occurred,api_error,webhook_received
import { analytics } from '@/lib/analytics/client'
// Track a simple event
analytics.pageView('/tickets', 'Tickets Page', 'tickets')
// Track a custom event with full type safety
analytics.track('ticket_added_to_cart', {
ticket_category: 'standard',
ticket_stage: 'early_bird',
ticket_price: 4900,
currency: 'CHF',
ticket_count: 2,
quantity: 2,
})
// Identify a user
analytics.identify('user_123', {
email: 'user@example.com',
name: 'John Doe',
company: 'Acme Inc',
})
// Track revenue
analytics.revenue({
amount: 9800,
currency: 'CHF',
type: 'ticket',
transactionId: 'cs_123',
productName: 'Early Bird Standard Ticket',
productCategory: 'standard',
})
// Track an error
analytics.error('Payment failed', error, {
type: 'payment',
severity: 'critical',
code: 'card_declined',
})import { serverAnalytics } from '@/lib/analytics/server'
// Track an event
await serverAnalytics.track('ticket_purchased', userId, {
ticket_id: 'ticket_123',
ticket_category: 'standard',
ticket_stage: 'early_bird',
ticket_price: 4900,
currency: 'CHF',
ticket_count: 1,
// ... other properties
})
// Identify a user
await serverAnalytics.identify(userId, {
email: 'user@example.com',
name: 'John Doe',
})
// Track revenue
await serverAnalytics.revenue(userId, {
amount: 4900,
currency: 'CHF',
type: 'ticket',
transactionId: 'cs_123',
productName: 'Early Bird Ticket',
})
// Don't forget to flush before serverless function terminates
await serverAnalytics.flush()For common scenarios, use the helper functions in helpers.ts:
import {
trackTicketAddedToCart,
trackCheckoutStarted,
trackFormError,
trackButtonClick,
} from '@/lib/analytics/helpers'
// Track ticket added to cart
trackTicketAddedToCart({
category: 'standard',
stage: 'early_bird',
price: 4900,
quantity: 2,
})
// Track checkout started
trackCheckoutStarted({
cartItemCount: 2,
cartTotalAmount: 9800,
cartCurrency: 'CHF',
cartItems: [
{
type: 'ticket',
category: 'standard',
stage: 'early_bird',
quantity: 2,
price: 4900,
},
],
email: 'user@example.com',
})
// Track form error
trackFormError({
formName: 'checkout_form',
formField: 'email',
errorMessage: 'Invalid email address',
errorType: 'validation',
})The logging system in src/lib/logger/index.ts provides structured, contextual logging that replaces all console.log statements.
- Structured logging with metadata
- Multiple log levels (debug, info, warn, error)
- Context-aware logging with scoped loggers
- PostHog integration for error tracking
- Development vs Production behavior
- Automatic error type inference
debug: Development-only verbose logginginfo: General informational messageswarn: Warning messages that don't break functionalityerror: Error conditions that should be monitored
import { logger } from '@/lib/logger'
// Info logging
logger.info('User logged in', { userId: '123' })
// Warning
logger.warn('Rate limit approaching', {
userId: '123',
requestCount: 95,
limit: 100,
})
// Error logging
logger.error('Payment processing failed', error, {
type: 'payment',
severity: 'critical',
code: 'card_declined',
orderId: 'order_123',
})
// Debug (only in development)
logger.debug('Processing webhook', {
eventType: 'checkout.session.completed',
sessionId: 'cs_123',
})Create scoped loggers for modules with default context:
import { logger } from '@/lib/logger'
// Create a scoped logger
const log = logger.scope('WebhookHandler', {
requestId: req.headers['x-request-id'],
})
// All logs from this logger include module and requestId
log.info('Processing webhook', { eventType: 'checkout.completed' })
log.error('Failed to create ticket', error, {
sessionId: 'cs_123',
})Development Mode:
🔍 [WebhookHandler] Processing webhook
Context: eventType="checkout.completed", requestId="req_123"
Production Mode (JSON):
{
"level": "info",
"message": "Processing webhook",
"timestamp": "2025-11-19T10:30:00.000Z",
"module": "WebhookHandler",
"context": {
"eventType": "checkout.completed",
"requestId": "req_123"
}
}Errors logged with logger.error() are automatically tracked in PostHog with:
- Error type inference (payment, auth, network, validation, system)
- Severity inference based on error type
- Stack traces
- Contextual metadata
logger.error('Failed to process payment', error, {
type: 'payment', // Optional: inferred if not provided
severity: 'critical', // Optional: inferred from type
code: 'card_declined',
userId: '123',
orderId: 'order_abc',
})PostHog Insights allow you to visualize and analyze your event data. Here's how to create common insights:
Total Revenue Over Time:
- Go to Insights → New Insight
- Select Trends
- Event:
purchase - Aggregation: Sum → Property value →
revenue_amount - Formula: Divide by 100 (to convert cents to dollars/CHF)
- Group by: Day/Week/Month
- Save as "Total Revenue"
Revenue by Ticket Type:
- Create a Trends insight
- Event:
purchase - Aggregation: Sum → Property value →
revenue_amount - Breakdown:
product_category - Save as "Revenue by Ticket Type"
Average Order Value:
- Create a Trends insight
- Event:
purchase - Add two series:
- Series A: Sum of
revenue_amount - Series B: Count of events
- Series A: Sum of
- Formula:
A / B / 100(divide by 100 for currency conversion) - Save as "Average Order Value"
Ticket Purchase Funnel:
- Go to Insights → New Insight
- Select Funnel
- Add steps:
- Step 1:
page_viewedwherepage_category=tickets - Step 2:
ticket_added_to_cart - Step 3:
checkout_started - Step 4:
payment_succeeded
- Step 1:
- Time window: 30 minutes (adjust as needed)
- Save as "Ticket Purchase Funnel"
Workshop Registration Funnel:
- Create a Funnel insight
- Steps:
- Step 1:
workshop_viewed - Step 2:
workshop_voucher_purchased - Step 3:
workshop_registered
- Step 1:
- Save as "Workshop Registration Funnel"
Most Clicked Buttons:
- Create a Trends insight
- Event:
button_clicked - Breakdown:
button_text - Sort by: Total count
- Save as "Top Buttons"
Form Errors:
- Create a Trends insight
- Event:
form_error - Breakdown:
form_nameorform_field - Save as "Form Errors"
Errors by Type:
- Create a Trends insight
- Event:
error_occurred - Breakdown:
error_type - Save as "Errors by Type"
Critical Errors:
- Create a Trends insight
- Event:
error_occurred - Filter:
error_severity=critical - Save as "Critical Errors"
Revenue Dashboard:
Create a dashboard combining:
- Total Revenue (trend)
- Revenue by Ticket Type (breakdown)
- Average Order Value (formula)
- Ticket Purchase Funnel
- Top selling tickets (table)
Operations Dashboard:
Combine:
- Tickets Sold (count)
- Tickets Validated (count)
- Tickets Checked In (count)
- Workshop Registrations (count)
- Errors by Type
- Critical Errors Alert
- In any insight, click Save → Create Alert
- Set threshold (e.g., "More than 10 errors in 1 hour")
- Choose notification method (email, Slack)
- Save alert
// In _app.tsx or layout
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { analytics } from '@/lib/analytics/client'
export default function App({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
const handleRouteChange = (url: string) => {
analytics.pageView(url)
}
router.events.on('routeChangeComplete', handleRouteChange)
return () => router.events.off('routeChangeComplete', handleRouteChange)
}, [router.events])
return <Component {...pageProps} />
}import { trackButtonClick } from '@/lib/analytics'
<button
onClick={() => {
trackButtonClick({
buttonText: 'Buy Tickets',
buttonLocation: 'hero_section',
buttonAction: 'navigate_to_tickets',
})
router.push('/tickets')
}}
>
Buy Tickets
</button>import { analytics } from '@/lib/analytics'
const handleSubmit = async (data) => {
try {
await submitForm(data)
analytics.track('form_submitted', {
form_name: 'checkout_form',
form_type: 'checkout',
form_success: true,
})
} catch (error) {
analytics.track('form_error', {
form_name: 'checkout_form',
error_message: error.message,
error_type: 'validation',
error_severity: 'low',
})
}
}import { logger } from '@/lib/logger'
import {
trackTicketPurchaseServer,
trackWebhookReceivedServer,
} from '@/lib/analytics'
export async function handleCheckoutSessionCompleted(
session: Stripe.Checkout.Session
) {
const log = logger.scope('WebhookHandler', {
sessionId: session.id,
})
const startTime = Date.now()
try {
log.info('Processing checkout session completed', {
paymentStatus: session.payment_status,
})
// ... process tickets ...
// Track ticket purchase
await trackTicketPurchaseServer({
distinctId: customerEmail,
ticketId: ticket.id,
category: ticketCategory,
stage: ticketStage,
price: amountPaid,
currency: 'CHF',
attendeeCount: 1,
stripeSessionId: session.id,
stripeCustomerId: stripeCustomerId,
email: customerEmail,
firstName,
lastName,
})
const processingTime = Date.now() - startTime
// Track successful webhook processing
await trackWebhookReceivedServer({
distinctId: 'system',
webhookSource: 'stripe',
webhookEventType: 'checkout.session.completed',
webhookId: session.id,
processingTimeMs: processingTime,
success: true,
})
log.info('Successfully processed checkout', {
ticketId: ticket.id,
processingTimeMs: processingTime,
})
} catch (error) {
log.error('Failed to process checkout', error, {
type: 'payment',
severity: 'critical',
})
await trackWebhookReceivedServer({
distinctId: 'system',
webhookSource: 'stripe',
webhookEventType: 'checkout.session.completed',
success: false,
})
throw error
}
}- Be Specific: Use descriptive event names (
ticket_purchasednotpurchase) - Consistent Naming: Follow snake_case convention for all events and properties
- Include Context: Always include relevant IDs, categories, and metadata
- Track User Actions: Track what users do, not what the system does
- Revenue Format: Always use smallest currency unit (cents) for revenue amounts
-
Use Appropriate Levels:
debug: Detailed flow information for developmentinfo: Important state changes and business eventswarn: Recoverable issues or important noticeserror: Failures that need attention
-
Include Context: Always provide relevant metadata
// Bad logger.info('User updated') // Good logger.info('User profile updated', { userId: '123', fieldsUpdated: ['email', 'name'], })
-
Use Scoped Loggers: Create module-specific loggers
const log = logger.scope('EmailService') log.info('Sending confirmation email', { ticketId: '123' })
-
Error Handling: Always log errors with full context
try { await processPayment() } catch (error) { log.error('Payment processing failed', error, { type: 'payment', severity: 'critical', orderId: 'order_123', amount: 4900, }) throw error }
- Batch Events: PostHog automatically batches events, but avoid excessive tracking
- Flush on Serverless: Always call
await serverAnalytics.flush()before function terminates - Async Tracking: Server-side tracking is async - await it in critical paths
- Sampling: For high-volume events, consider sampling in production
- Sanitize Data: Never track passwords, credit cards, or sensitive PII
- User Consent: Respect user privacy preferences
- GDPR Compliance: Use PostHog's data deletion features
- Mask Sensitive Fields: Use PostHog's masking features for forms
-
Use Type Exports: Import types from analytics module
import type { EventProperties } from '@/lib/analytics'
-
Extend Carefully: When adding new events, follow the discriminated union pattern
export interface NewEvent { event: 'new_event_name' properties: BaseEventProperties & { custom_field: string } } // Add to union type export type AnalyticsEvent = ... | NewEvent
-
Validate at Runtime: Use Zod or similar for API payloads
const eventSchema = z.object({ event: z.literal('ticket_purchased'), properties: z.object({ ticket_id: z.string(), // ... other fields }), })
Add your event to src/lib/analytics/events.ts:
export interface MyNewEvent {
event: 'my_new_event'
properties: BaseEventProperties & {
custom_field: string
custom_number: number
}
}
// Add to union type
export type AnalyticsEvent =
| PageViewedEvent
// ... other events
| MyNewEventimport { analytics } from '@/lib/analytics'
analytics.track('my_new_event', {
custom_field: 'value',
custom_number: 42,
})- Go to PostHog
- Create a new insight for your event
- Add to relevant dashboards
- Check PostHog API key in
.env - Verify
capture_pageviewand other settings - Check browser console for errors
- Verify reverse proxy configuration in
next.config.ts
- Ensure
await serverAnalytics.flush()is called - Check PostHog API key
- Verify network connectivity to PostHog EU endpoint
- Regenerate TypeScript types if database schema changed
- Ensure all event properties match defined interfaces
- Check for typos in event names
console.log('[Webhook] Processing checkout:', session.id)
console.log('[Webhook] Payment status:', session.payment_status)
console.error('[Webhook] Failed to create ticket:', error)const log = logger.scope('Webhook', { sessionId: session.id })
log.info('Processing checkout', {
paymentStatus: session.payment_status,
})
log.error('Failed to create ticket', error, {
type: 'payment',
severity: 'critical',
})- PostHog Documentation
- PostHog Revenue Analytics
- PostHog Next.js Integration
- Event Tracking Best Practices
For questions or issues:
- Check this documentation
- Review PostHog documentation
- Check implementation examples in the codebase
- Ask the team