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-middleware

Quick 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=true

API 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