Vercel Plugin

Vercel integration with edge functions and analytics

Vercel Plugin

The @entrolytics/vercel-plugin provides seamless Vercel integration with edge functions, automatic analytics collection, and deployment-time configuration.

Installation

npm install @entrolytics/vercel-plugin

Quick Start

Configure vercel.json

{
  "functions": {
    "api/**/*.ts": {
      "runtime": "@vercel/node"
    }
  },
  "build": {
    "env": {
      "ENTROLYTICS_WEBSITE_ID": "@entrolytics-website-id",
      "ENTROLYTICS_API_KEY": "@entrolytics-api-key"
    }
  },
  "postbuild": "entrolytics-vercel-build"
}

Environment Variables

# Vercel Environment Variables
ENTROLYTICS_WEBSITE_ID=your-website-id
ENTROLYTICS_API_KEY=your-api-key
ENTROLYTICS_HOST=https://entrolytics.click
ENTROLYTICS_DEBUG=false

Edge Function Integration

// api/track.ts
import { VercelRequest, VercelResponse } from '@vercel/node'
import { EntrolyticsVercel } from '@entrolytics/vercel-plugin'

const entrolytics = new EntrolyticsVercel({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
  apiKey: process.env.ENTROLYTICS_API_KEY
})

export default async function handler(req: VercelRequest, res: VercelResponse) {
  // Track API requests
  await entrolytics.track('api_request', {
    method: req.method,
    path: req.url,
    userAgent: req.headers['user-agent'],
    ip: req.headers['x-forwarded-for'] || req.headers['x-real-ip']
  })

  // Your API logic here
  res.status(200).json({ message: 'Success' })
}

Middleware Integration

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { EntrolyticsVercelMiddleware } from '@entrolytics/vercel-plugin'

const analytics = new EntrolyticsVercelMiddleware({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
  apiKey: process.env.ENTROLYTICS_API_KEY
})

export async function middleware(req: NextRequest) {
  // Track page views
  if (req.nextUrl.pathname.startsWith('/')) {
    await analytics.trackPageView(req, {
      referrer: req.referrer,
      userAgent: req.headers.get('user-agent'),
      country: req.geo?.country,
      region: req.geo?.region,
      city: req.geo?.city
    })
  }

  // Track API requests
  if (req.nextUrl.pathname.startsWith('/api/')) {
    await analytics.trackApiRequest(req, {
      method: req.method,
      endpoint: req.nextUrl.pathname,
      apiVersion: req.headers.get('api-version')
    })
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}

Configuration

Plugin Configuration

// entrolytics.config.js
module.exports = {
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID,
  apiKey: process.env.ENTROLYTICS_API_KEY,
  host: process.env.ENTROLYTICS_HOST || 'https://entrolytics.click',

  // Edge function configuration
  edgeFunctions: {
    enabled: true,
    trackRequests: true,
    trackErrors: true,
    trackPerformance: true,
    excludePatterns: ['/_next/*', '/api/health']
  },

  // Build-time analytics
  buildAnalytics: {
    enabled: true,
    trackBuildTime: true,
    trackBundleSize: true,
    trackDependencies: true
  },

  // Deployment analytics
  deploymentAnalytics: {
    enabled: true,
    trackDeployments: true,
    trackEnvironment: true,
    trackPerformance: true
  },

  // Debug mode
  debug: process.env.NODE_ENV === 'development'
}

Vercel Configuration

{
  "version": 2,
  "builds": [
    {
      "src": "api/**/*.ts",
      "use": "@vercel/node"
    },
    {
      "src": "package.json",
      "use": "@vercel/static-build",
      "config": {
        "distDir": "out"
      }
    }
  ],
  "functions": {
    "api/**/*.ts": {
      "maxDuration": 30,
      "memory": 1024
    }
  },
  "routes": [
    {
      "src": "/api/(.*)",
      "dest": "/api/$1"
    }
  ],
  "env": {
    "ENTROLYTICS_WEBSITE_ID": "@entrolytics-website-id",
    "ENTROLYTICS_API_KEY": "@entrolytics-api-key",
    "ENTROLYTICS_HOST": "@entrolytics-host"
  },
  "build": {
    "env": {
      "ENTROLYTICS_WEBSITE_ID": "@entrolytics-website-id",
      "ENTROLYTICS_API_KEY": "@entrolytics-api-key"
    }
  }
}

API Reference

Core Classes

class EntrolyticsVercel {
  constructor(config: VercelConfig)

  // Event tracking
  track(event: string, properties?: Record<string, any>): Promise<void>
  trackBatch(events: AnalyticsEvent[]): Promise<void>
  page(url: string, properties?: Record<string, any>): Promise<void>

  // Request tracking
  trackRequest(
    req: VercelRequest,
    properties?: Record<string, any>
  ): Promise<void>
  trackApiRequest(
    req: VercelRequest,
    properties?: Record<string, any>
  ): Promise<void>
  trackPageView(
    req: NextRequest,
    properties?: Record<string, any>
  ): Promise<void>

  // Performance tracking
  trackPerformance(
    metric: string,
    value: number,
    properties?: Record<string, any>
  ): Promise<void>
  trackWebVitals(metrics: WebVitalsMetrics): Promise<void>

  // Error tracking
  trackError(error: Error, context?: Record<string, any>): Promise<void>
  trackUnhandledRejection(event: PromiseRejectionEvent): Promise<void>

  // Build analytics
  trackBuildStart(): Promise<void>
  trackBuildComplete(metrics: BuildMetrics): Promise<void>
  trackDeployment(deploymentInfo: DeploymentInfo): Promise<void>
}

class EntrolyticsVercelMiddleware {
  constructor(config: VercelConfig)

  trackPageView(
    req: NextRequest,
    properties?: Record<string, any>
  ): Promise<void>
  trackApiRequest(
    req: NextRequest,
    properties?: Record<string, any>
  ): Promise<void>
  trackPerformance(req: NextRequest, metrics: PerformanceMetrics): Promise<void>
}

Type Definitions

interface VercelConfig {
  websiteId: string
  apiKey?: string
  host?: string
  edgeFunctions?: {
    enabled?: boolean
    trackRequests?: boolean
    trackErrors?: boolean
    trackPerformance?: boolean
    excludePatterns?: string[]
  }
  buildAnalytics?: {
    enabled?: boolean
    trackBuildTime?: boolean
    trackBundleSize?: boolean
    trackDependencies?: boolean
  }
  deploymentAnalytics?: {
    enabled?: boolean
    trackDeployments?: boolean
    trackEnvironment?: boolean
    trackPerformance?: boolean
  }
  debug?: boolean
}

interface BuildMetrics {
  buildTime: number
  bundleSize: number
  dependencies: string[]
  environment: string
}

interface DeploymentInfo {
  url: string
  environment: string
  commit: string
  branch: string
  timestamp: Date
}

interface WebVitalsMetrics {
  lcp?: number // Largest Contentful Paint
  fid?: number // First Input Delay
  cls?: number // Cumulative Layout Shift
  fcp?: number // First Contentful Paint
  ttfb?: number // Time to First Byte
}

Advanced Usage

Edge Function Analytics

// api/analytics/edge.ts
import { VercelRequest, VercelResponse } from '@vercel/node'
import { EntrolyticsVercel } from '@entrolytics/vercel-plugin'

const entrolytics = new EntrolyticsVercel({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
  apiKey: process.env.ENTROLYTICS_API_KEY,
  edgeFunctions: {
    enabled: true,
    trackPerformance: true,
    trackErrors: true
  }
})

export default async function handler(req: VercelRequest, res: VercelResponse) {
  const startTime = Date.now()

  try {
    // Track incoming request
    await entrolytics.trackRequest(req, {
      function: 'analytics-edge',
      region: process.env.VERCEL_REGION || 'unknown',
      coldStart: !globalThis.warm
    })

    // Your business logic
    const result = await processAnalyticsData(req.body)

    // Track performance
    const duration = Date.now() - startTime
    await entrolytics.trackPerformance('function_duration', duration, {
      function: 'analytics-edge',
      success: true
    })

    // Mark as warm for subsequent requests
    globalThis.warm = true

    res.status(200).json(result)
  } catch (error) {
    // Track error
    await entrolytics.trackError(error as Error, {
      function: 'analytics-edge',
      region: process.env.VERCEL_REGION
    })

    res.status(500).json({ error: 'Internal server error' })
  }
}

Next.js Integration

// pages/_app.tsx
import type { AppProps } from 'next/app'
import { useEffect } from 'react'
import { EntrolyticsVercel } from '@entrolytics/vercel-plugin'

const entrolytics = new EntrolyticsVercel({
  websiteId: process.env.NEXT_PUBLIC_ENTROLYTICS_WEBSITE_ID!,
})

export default function App({ Component, pageProps }: AppProps) {
  useEffect(() => {
    // Track client-side performance
    if ('web-vitals' in window) {
      import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
        getCLS((metric) => entrolytics.trackPerformance('CLS', metric.value))
        getFID((metric) => entrolytics.trackPerformance('FID', metric.value))
        getFCP((metric) => entrolytics.trackPerformance('FCP', metric.value))
        getLCP((metric) => entrolytics.trackPerformance('LCP', metric.value))
        getTTFB((metric) => entrolytics.trackPerformance('TTFB', metric.value))
      })
    }
  }, [])

  return <Component {...pageProps} />
}

// pages/api/track.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { EntrolyticsVercel } from '@entrolytics/vercel-plugin'

const entrolytics = new EntrolyticsVercel({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
  apiKey: process.env.ENTROLYTICS_API_KEY!,
})

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    const { event, properties } = req.body

    await entrolytics.track(event, {
      ...properties,
      source: 'client-api',
      timestamp: new Date().toISOString(),
      vercelRegion: process.env.VERCEL_REGION,
    })

    res.status(200).json({ success: true })
  } catch (error) {
    console.error('Analytics tracking error:', error)
    res.status(500).json({ error: 'Failed to track event' })
  }
}

Build-time Analytics

// scripts/build-analytics.js
const { EntrolyticsVercel } = require('@entrolytics/vercel-plugin')

const entrolytics = new EntrolyticsVercel({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID,
  apiKey: process.env.ENTROLYTICS_API_KEY,
  buildAnalytics: {
    enabled: true,
    trackBuildTime: true,
    trackBundleSize: true,
    trackDependencies: true
  }
})

async function trackBuildStart() {
  console.log('📊 Tracking build start...')

  await entrolytics.trackBuildStart()

  console.log('✅ Build start tracked')
}

async function trackBuildComplete(buildMetrics) {
  console.log('📊 Tracking build completion...')

  await entrolytics.trackBuildComplete({
    buildTime: buildMetrics.duration,
    bundleSize: buildMetrics.bundleSize,
    dependencies: buildMetrics.dependencies,
    environment: process.env.NODE_ENV,
    vercelEnvironment: process.env.VERCEL_ENV
  })

  console.log('✅ Build completion tracked')
}

// Export for use in build scripts
module.exports = {
  trackBuildStart,
  trackBuildComplete
}
// package.json scripts
{
  "scripts": {
    "build": "node scripts/track-build-start.js && next build && node scripts/track-build-complete.js",
    "postbuild": "entrolytics-vercel-build"
  }
}

Deployment Analytics

// scripts/deployment-analytics.js
const { EntrolyticsVercel } = require('@entrolytics/vercel-plugin')

const entrolytics = new EntrolyticsVercel({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID,
  apiKey: process.env.ENTROLYTICS_API_KEY,
  deploymentAnalytics: {
    enabled: true,
    trackDeployments: true,
    trackEnvironment: true,
    trackPerformance: true
  }
})

async function trackDeployment(deploymentInfo) {
  console.log('📊 Tracking deployment...')

  await entrolytics.trackDeployment({
    url: deploymentInfo.url,
    environment: deploymentInfo.environment,
    commit: deploymentInfo.commit,
    branch: deploymentInfo.branch,
    timestamp: new Date(),
    vercelEnvironment: process.env.VERCEL_ENV,
    vercelRegion: process.env.VERCEL_REGION,
    buildTime: deploymentInfo.buildTime
  })

  console.log('✅ Deployment tracked')
}

// Usage in CI/CD
if (process.env.VERCEL === '1') {
  trackDeployment({
    url: process.env.VERCEL_URL,
    environment: process.env.VERCEL_ENV,
    commit: process.env.VERCEL_GIT_COMMIT_SHA,
    branch: process.env.VERCEL_GIT_COMMIT_REF,
    buildTime: process.env.BUILD_TIME
  })
}

Real-time Analytics Dashboard

// api/analytics/dashboard.ts
import { VercelRequest, VercelResponse } from '@vercel/node'
import { EntrolyticsVercel } from '@entrolytics/vercel-plugin'

const entrolytics = new EntrolyticsVercel({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
  apiKey: process.env.ENTROLYTICS_API_KEY!,
})

export default async function handler(req: VercelRequest, res: VercelResponse) {
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    const { timeRange = '1h', metrics = 'all' } = req.query

    const dashboardData = await entrolytics.getDashboardData({
      timeRange: timeRange as string,
      metrics: metrics as string[],
      filters: {
        vercelEnvironment: process.env.VERCEL_ENV,
        vercelRegion: process.env.VERCEL_REGION,
      },
    })

    res.status(200).json(dashboardData)
  } catch (error) {
    console.error('Dashboard data error:', error)
    res.status(500).json({ error: 'Failed to fetch dashboard data' })
  }
}

// components/AnalyticsDashboard.tsx
import { useEffect, useState } from 'react'

interface DashboardData {
  totalEvents: number
  uniqueUsers: number
  topPages: Array<{ page: string; views: number }>
  performanceMetrics: {
    avgResponseTime: number
    errorRate: number
    uptime: number
  }
  realTimeEvents: Array<{
    timestamp: string
    event: string
    properties: Record<string, any>
  }>
}

export default function AnalyticsDashboard() {
  const [data, setData] = useState<DashboardData | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const fetchDashboardData = async () => {
      try {
        const response = await fetch('/api/analytics/dashboard?timeRange=1h')
        const dashboardData = await response.json()
        setData(dashboardData)
      } catch (error) {
        console.error('Failed to fetch dashboard data:', error)
      } finally {
        setLoading(false)
      }
    }

    fetchDashboardData()

    // Refresh every 30 seconds
    const interval = setInterval(fetchDashboardData, 30000)

    return () => clearInterval(interval)
  }, [])

  if (loading) return <div>Loading analytics...</div>

  if (!data) return <div>Failed to load analytics</div>

  return (
    <div className="analytics-dashboard">
      <div className="metrics-grid">
        <div className="metric-card">
          <h3>Total Events</h3>
          <p className="metric-value">{data.totalEvents.toLocaleString()}</p>
        </div>
        <div className="metric-card">
          <h3>Unique Users</h3>
          <p className="metric-value">{data.uniqueUsers.toLocaleString()}</p>
        </div>
        <div className="metric-card">
          <h3>Avg Response Time</h3>
          <p className="metric-value">{data.performanceMetrics.avgResponseTime}ms</p>
        </div>
        <div className="metric-card">
          <h3>Error Rate</h3>
          <p className="metric-value">{(data.performanceMetrics.errorRate * 100).toFixed(2)}%</p>
        </div>
      </div>

      <div className="charts-section">
        <div className="chart-container">
          <h3>Top Pages</h3>
          {/* Render top pages chart */}
        </div>
        <div className="chart-container">
          <h3>Real-time Events</h3>
          {/* Render real-time events */}
        </div>
      </div>
    </div>
  )
}

Testing

Unit Tests

import { EntrolyticsVercel } from '@entrolytics/vercel-plugin'
import { VercelRequest, VercelResponse } from '@vercel/node'

describe('EntrolyticsVercel', () => {
  let entrolytics: EntrolyticsVercel
  let mockReq: Partial<VercelRequest>
  let mockRes: Partial<VercelResponse>

  beforeEach(() => {
    entrolytics = new EntrolyticsVercel({
      websiteId: 'test-website-id',
      apiKey: 'test-api-key',
      debug: true
    })

    mockReq = {
      method: 'GET',
      url: '/api/test',
      headers: {
        'user-agent': 'test-agent',
        'x-forwarded-for': '192.168.1.1'
      }
    }

    mockRes = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn().mockReturnThis()
    }
  })

  test('should track API requests', async () => {
    await entrolytics.trackRequest(mockReq as VercelRequest, {
      custom: 'property'
    })

    // Verify tracking was called
    expect(entrolytics.track).toHaveBeenCalledWith('api_request', {
      method: 'GET',
      path: '/api/test',
      userAgent: 'test-agent',
      ip: '192.168.1.1',
      custom: 'property'
    })
  })

  test('should track performance metrics', async () => {
    await entrolytics.trackPerformance('response_time', 150, {
      endpoint: '/api/test'
    })

    expect(entrolytics.track).toHaveBeenCalledWith('performance_metric', {
      metric: 'response_time',
      value: 150,
      endpoint: '/api/test'
    })
  })

  test('should track errors with context', async () => {
    const error = new Error('Test error')

    await entrolytics.trackError(error, {
      endpoint: '/api/test',
      method: 'GET'
    })

    expect(entrolytics.track).toHaveBeenCalledWith('error_occurred', {
      error: 'Test error',
      stack: error.stack,
      endpoint: '/api/test',
      method: 'GET'
    })
  })
})

Integration Tests

describe('Vercel Integration', () => {
  let app: Express
  let entrolytics: EntrolyticsVercel

  beforeAll(async () => {
    entrolytics = new EntrolyticsVercel({
      websiteId: 'integration-test-id',
      apiKey: 'integration-test-key'
    })

    app = express()
    app.use(express.json())

    // Analytics middleware
    app.use(async (req, res, next) => {
      await entrolytics.trackRequest(req, {
        middleware: 'express'
      })
      next()
    })

    app.get('/api/test', (req, res) => {
      res.json({ message: 'Success' })
    })
  })

  test('should track API requests through middleware', async () => {
    const response = await request(app).get('/api/test')

    expect(response.status).toBe(200)
    expect(response.body.message).toBe('Success')

    // Verify analytics were tracked
    expect(entrolytics.track).toHaveBeenCalledWith('api_request', {
      method: 'GET',
      path: '/api/test',
      middleware: 'express'
    })
  })
})

Performance Optimization

Edge Function Optimization

// Optimized edge function
export default async function handler(req: VercelRequest, res: VercelResponse) {
  // Early return for health checks
  if (req.url === '/api/health') {
    return res.status(200).json({ status: 'ok' })
  }

  // Track asynchronously to avoid blocking
  entrolytics.trackRequest(req).catch(console.error)

  // Your business logic
  const result = await processRequest(req)

  res.status(200).json(result)
}

// Warm-up function for edge functions
export async function warmUp() {
  // Pre-warm the analytics client
  await entrolytics.warmUp()

  // Pre-warm database connections
  await warmUpDatabase()
}

Caching Strategy

// Analytics with caching
class CachedAnalytics {
  private cache = new Map<string, { data: any; timestamp: number }>()
  private ttl = 60000 // 1 minute

  async getDashboardData(timeRange: string): Promise<any> {
    const cacheKey = `dashboard:${timeRange}`
    const cached = this.cache.get(cacheKey)

    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.data
    }

    const data = await entrolytics.getDashboardData({ timeRange })
    this.cache.set(cacheKey, { data, timestamp: Date.now() })

    return data
  }

  clearCache(): void {
    this.cache.clear()
  }
}

Troubleshooting

Best Practices

Migration Guide

From Manual Vercel Setup

// Before - Manual setup
import fetch from 'node-fetch'

await fetch('https://entrolytics.click/track', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    websiteId: process.env.WEBSITE_ID,
    event: 'test_event',
    properties: {}
  })
})

// After - Entrolytics Vercel Plugin
import { EntrolyticsVercel } from '@entrolytics/vercel-plugin'

const entrolytics = new EntrolyticsVercel({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!
})

await entrolytics.track('test_event', {})

From Other Analytics Plugins

// Before - Other Vercel analytics
import { Analytics } from 'other-vercel-analytics'

const analytics = new Analytics({
  apiKey: process.env.ANALYTICS_API_KEY
})

analytics.track('event', properties)

// After - Entrolytics Vercel Plugin
import { EntrolyticsVercel } from '@entrolytics/vercel-plugin'

const entrolytics = new EntrolyticsVercel({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!
})

await entrolytics.track('event', properties)

// Additional Vercel-specific features
await entrolytics.trackBuildComplete(buildMetrics)
await entrolytics.trackDeployment(deploymentInfo)

Vercel plugin for Entrolytics - First-party growth analytics for the edge