Fastify Middleware
Fastify plugin with hook-based tracking
Fastify Middleware
The @entrolytics/fastify-middleware package provides a Fastify plugin for analytics tracking using Fastify's hook system, offering high performance and minimal overhead.
Installation
pnpm add @entrolytics/fastify-middlewareQuick Start
Basic Setup
import fastify from 'fastify'
import { entrolyticsPlugin } from '@entrolytics/fastify-middleware'
const app = fastify()
// Register Entrolytics plugin
await app.register(entrolyticsPlugin, {
websiteId: 'your-website-id',
apiKey: 'your-api-key'
})
// Your routes
app.get('/', async (request, reply) => {
return { hello: 'world' }
})
await app.listen({ port: 3000 })With Environment Variables
import fastify from 'fastify'
import { entrolyticsPlugin } from '@entrolytics/fastify-middleware'
const app = fastify()
await app.register(entrolyticsPlugin, {
websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
apiKey: process.env.ENTROLYTICS_API_KEY!,
host: process.env.ENTROLYTICS_HOST || 'https://entrolytics.dev'
})Configuration
Plugin Options
interface FastifyPluginConfig {
/** Website ID (required) */
websiteId: string
/** API key for authentication */
apiKey?: string
/** Custom API host */
host?: string
/** Auto-track requests (default: true) */
autoTrack?: boolean
/** Track errors and exceptions (default: true) */
trackErrors?: boolean
/** Track response times (default: true) */
trackPerformance?: boolean
/** Exclude specific paths from tracking */
excludePaths?: string[]
/** Include specific headers in events */
includeHeaders?: string[]
/** Enable debug logging */
debug?: boolean
/** Async tracking (default: true) */
async?: boolean
/** Custom hooks to register */
hooks?: Array<'onRequest' | 'preHandler' | 'onResponse' | 'onError'>
}Environment Variables
# .env
ENTROLYTICS_WEBSITE_ID=your-website-id
ENTROLYTICS_API_KEY=your-api-key
ENTROLYTICS_HOST=https://entrolytics.dev
ENTROLYTICS_DEBUG=false
ENTROLYTICS_ASYNC=trueAPI Reference
Fastify Request API
The plugin adds an entrolytics object to the Fastify request:
interface EntrolyticsFastifyRequest {
/** Track a custom event */
track(event: string, properties?: Record<string, any>): Promise<void>
/** Track multiple events in batch */
trackBatch(
events: Array<{ event: string; properties?: Record<string, any> }>
): Promise<void>
/** Identify a user */
identify(userId: string, traits?: Record<string, any>): Promise<void>
/** Set user properties */
setUserProperties(properties: Record<string, any>): Promise<void>
/** Track a page view */
page(url?: string, properties?: Record<string, any>): Promise<void>
/** Get current configuration */
getConfig(): FastifyPluginConfig
/** Get session ID */
getSessionId(): string
/** Get user ID */
getUserId(): string | undefined
}Usage Examples
app.get('/api/users/:id', async (request, reply) => {
// Track custom event
await request.entrolytics.track('user_profile_view', {
userId: (request.params as any).id,
source: 'api'
})
// Identify user
await request.entrolytics.identify((request.params as any).id, {
email: 'user@example.com',
role: 'user'
})
// Track batch events
await request.entrolytics.trackBatch([
{ event: 'api_request', properties: { endpoint: '/api/users' } },
{ event: 'database_query', properties: { table: 'users' } }
])
const user = await getUserById((request.params as any).id)
return { user }
})Advanced Usage
Custom Hooks
import fastify from 'fastify'
import { entrolyticsPlugin } from '@entrolytics/fastify-middleware'
const app = fastify()
await app.register(entrolyticsPlugin, {
websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
apiKey: process.env.ENTROLYTICS_API_KEY!,
hooks: ['onRequest', 'preHandler', 'onResponse', 'onError']
})
// Add custom hook logic
app.addHook('preHandler', async (request, reply) => {
// Track authenticated users
if (request.user) {
await request.entrolytics.identify(request.user.id, {
email: request.user.email,
role: request.user.role
})
}
// Track API version
const apiVersion = request.headers['api-version']
if (apiVersion) {
await request.entrolytics.track('api_request', {
version: apiVersion,
method: request.method,
path: request.url
})
}
})Performance Monitoring
app.addHook('onRequest', async (request, reply) => {
request.startTime = process.hrtime.bigint()
})
app.addHook('onResponse', async (request, reply) => {
const endTime = process.hrtime.bigint()
const duration = Number(endTime - request.startTime) / 1000000
await request.entrolytics.track('request_performance', {
method: request.method,
path: request.url,
statusCode: reply.statusCode,
duration,
contentLength: reply.getHeader('content-length'),
cacheHit: reply.getHeader('x-cache-status') === 'hit'
})
})Error Tracking
app.addHook('onError', async (request, reply, error) => {
await request.entrolytics.track('error', {
message: error.message,
stack: error.stack,
url: request.url,
method: request.method,
userAgent: request.headers['user-agent'],
ip: request.ip
})
await request.entrolytics.track('server_error', {
errorType: error.constructor.name,
statusCode: reply.statusCode || 500,
validationError: error.validation ? error.validation : undefined
})
})Route-Specific Tracking
app.get(
'/public/:id',
{
preHandler: async (request, reply) => {
await request.entrolytics.track('public_access', {
path: request.url,
params: request.params
})
}
},
async (request, reply) => {
return { data: 'public data' }
}
)
app.post(
'/api/secure',
{
preHandler: [
async (request, reply) => {
// Authentication check
if (!request.user) {
return reply.code(401).send({ error: 'Unauthorized' })
}
},
async (request, reply) => {
await request.entrolytics.identify(request.user.id, {
email: request.user.email
})
await request.entrolytics.track('secure_api_access', {
endpoint: request.url,
userRole: request.user.role
})
}
]
},
async (request, reply) => {
return { data: 'secure data' }
}
)Conditional Tracking
app.addHook('preHandler', async (request, reply) => {
// Only track production traffic
if (process.env.NODE_ENV === 'production') {
await request.entrolytics.track('production_request', {
path: request.url,
method: request.method
})
}
// Exclude bot traffic
const userAgent = request.headers['user-agent'] || ''
if (userAgent.includes('bot') || userAgent.includes('crawler')) {
await request.entrolytics.track('bot_request', {
userAgent,
path: request.url
})
return
}
// Track authenticated vs anonymous users
if (request.user) {
await request.entrolytics.track('authenticated_request')
} else {
await request.entrolytics.track('anonymous_request')
}
})Authentication Integration
JWT Authentication
import fastify from 'fastify'
const app = fastify()
// Register JWT plugin
await app.register(require('@fastify/jwt'), {
secret: process.env.JWT_SECRET!
})
// Add authentication hook
app.addHook('preHandler', async (request, reply) => {
try {
await request.jwtVerify()
request.user = request.decoded
// Identify user in analytics
await request.entrolytics.identify(request.user.sub, {
email: request.user.email,
role: request.user.role,
permissions: request.user.permissions
})
await request.entrolytics.track('jwt_authenticated', {
userId: request.user.sub,
issuer: request.user.iss
})
} catch (error) {
await request.entrolytics.track('jwt_auth_failed', {
error: error.message,
token: request.headers.authorization?.substring(0, 10) + '...'
})
}
})Session Management
await app.register(require('@fastify/cookie'))
await app.register(require('@fastify/session'), {
secret: process.env.SESSION_SECRET!,
cookie: { secure: false } // for development
})
app.addHook('preHandler', async (request, reply) => {
if (request.session && !request.session.analyticsTracked) {
await request.entrolytics.track('session_start', {
sessionId: request.session.sessionId,
isNewSession: !request.session.userId
})
request.session.analyticsTracked = true
}
if (request.session && request.session.createdAt) {
const sessionDuration = Date.now() - request.session.createdAt
await request.entrolytics.track('session_duration', {
duration: sessionDuration,
sessionId: request.session.sessionId
})
}
})Testing
Unit Tests
import fastify from 'fastify'
import { entrolyticsPlugin } from '@entrolytics/fastify-middleware'
// Mock the analytics client
jest.mock('@entrolytics/fastify-middleware', () => ({
entrolyticsPlugin: jest.fn((fastify, options) => {
fastify.addHook('preHandler', async (request, reply) => {
request.entrolytics = {
track: jest.fn(),
identify: jest.fn(),
page: jest.fn()
}
})
})
}))
describe('Fastify Analytics Plugin', () => {
let app: fastify.FastifyInstance
beforeEach(async () => {
app = fastify()
await app.register(entrolyticsPlugin, {
websiteId: 'test-website-id',
apiKey: 'test-api-key'
})
app.get('/test', async (request, reply) => {
await request.entrolytics.track('test_event', { property: 'value' })
return { success: true }
})
})
it('should add entrolytics to request', async () => {
const response = await app.inject({
method: 'GET',
url: '/test'
})
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({ success: true })
})
it('should track events', async () => {
await app.inject({
method: 'GET',
url: '/test'
})
// Verify track was called (would need to access the mock)
})
})Integration Tests
import fastify from 'fastify'
import { entrolyticsPlugin } from '@entrolytics/fastify-middleware'
describe('Analytics Integration', () => {
let app: fastify.FastifyInstance
beforeAll(async () => {
app = fastify()
await app.register(entrolyticsPlugin, {
websiteId: process.env.TEST_WEBSITE_ID!,
apiKey: process.env.TEST_API_KEY!,
debug: true
})
app.get('/users/:id', async (request, reply) => {
await request.entrolytics.track('user_view', {
userId: (request.params as any).id
})
return { userId: (request.params as any).id }
})
})
it('should track user views', async () => {
const response = await app.inject({
method: 'GET',
url: '/users/123'
})
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({ userId: '123' })
})
})Performance Optimization
Async Operations
await app.register(entrolyticsPlugin, {
websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
apiKey: process.env.ENTROLYTICS_API_KEY!,
async: true // Don't block request processing
})Event Batching
// Global event batch
const eventBatch: Array<{ event: string; properties?: Record<string, any> }> =
[]
// Process batch every 10 seconds
setInterval(async () => {
if (eventBatch.length > 0) {
// This would need to be implemented in the plugin
console.log(`Processing batch of ${eventBatch.length} events`)
eventBatch.length = 0
}
}, 10000)
app.addHook('preHandler', async (request, reply) => {
// Add to batch instead of immediate tracking
eventBatch.push({
event: 'request',
properties: { path: request.url, method: request.method }
})
})Request Sampling
app.addHook('preHandler', async (request, reply) => {
// Only sample 10% of requests in high traffic
if (Math.random() > 0.9) {
await request.entrolytics.track('sampled_request', {
path: request.url,
method: request.method
})
}
})Troubleshooting
Best Practices
Migration Guide
From Express Middleware
// Express
import express from 'express'
import { entrolyticsMiddleware } from '@entrolytics/express-middleware'
const app = express()
app.use(entrolyticsMiddleware(config))
// Fastify
import fastify from 'fastify'
import { entrolyticsPlugin } from '@entrolytics/fastify-middleware'
const app = fastify()
await app.register(entrolyticsPlugin, config)From Manual Tracking
// Before
app.get('/api/data', async (request, reply) => {
analytics.track('api_call', { endpoint: '/api/data' })
return { data }
})
// After with plugin
app.get(
'/api/data',
{
preHandler: async (request, reply) => {
await request.entrolytics.track('api_call', {
endpoint: request.url
})
}
},
async (request, reply) => {
return { data }
}
)Fastify plugin for Entrolytics - First-party growth analytics for the edge