Netlify Plugin

Netlify integration with functions and build analytics

Netlify Plugin

The @entrolytics/netlify-plugin provides seamless Netlify integration with serverless functions, build-time analytics, and deployment tracking.

Installation

npm install @entrolytics/netlify-plugin

Quick Start

Configure netlify.toml

[build]
  command = "npm run build"
  publish = "dist"

[build.environment]
  ENTROLYTICS_WEBSITE_ID = "your-website-id"
  ENTROLYTICS_API_KEY = "your-api-key"

[[plugins]]
  package = "@entrolytics/netlify-plugin"

  [plugins.inputs]
    website_id = "your-website-id"
    api_key = "your-api-key"
    track_builds = true
    track_functions = true
    track_forms = true

[functions]
  directory = "netlify/functions"

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/:splat"
  status = 200

Environment Variables

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

Serverless Function Integration

// netlify/functions/analytics.ts
import { Handler } from '@netlify/functions'
import { EntrolyticsNetlify } from '@entrolytics/netlify-plugin'

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

export const handler: Handler = async (event, context) => {
  // Track function invocations
  await entrolytics.trackFunction(event, {
    functionName: 'analytics',
    region: process.env.NETLIFY_REGION || 'unknown',
    coldStart: !context.clientContext?.custom?.warmed
  })

  // Track API requests
  await entrolytics.track('api_request', {
    method: event.httpMethod,
    path: event.path,
    userAgent: event.headers['user-agent'],
    ip: event.headers['x-forwarded-for'] || event.headers['x-real-ip']
  })

  // Your function logic here
  const response = {
    statusCode: 200,
    body: JSON.stringify({ message: 'Success' })
  }

  return response
}

Form Tracking

// netlify/functions/submit-form.ts
import { Handler } from '@netlify/functions'
import { EntrolyticsNetlify } from '@entrolytics/netlify-plugin'

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

export const handler: Handler = async (event) => {
  if (event.httpMethod !== 'POST') {
    return {
      statusCode: 405,
      body: JSON.stringify({ error: 'Method not allowed' })
    }
  }

  try {
    const data = JSON.parse(event.body || '{}')

    // Track form submission
    await entrolytics.track('form_submission', {
      formName: data.formName,
      formType: data.formType,
      source: data.source || 'direct',
      userAgent: event.headers['user-agent']
    })

    // Process form data
    await processFormData(data)

    return {
      statusCode: 200,
      body: JSON.stringify({ success: true })
    }
  } catch (error) {
    // Track form errors
    await entrolytics.track('form_error', {
      error: (error as Error).message,
      formName: data?.formName
    })

    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Failed to process form' })
    }
  }
}

Configuration

Plugin Configuration

# netlify.toml
[[plugins]]
  package = "@entrolytics/netlify-plugin"

  [plugins.inputs]
    website_id = "your-website-id"
    api_key = "your-api-key"
    host = "https://entrolytics.click"

    # Build analytics
    track_builds = true
    track_bundle_size = true
    track_dependencies = true

    # Function analytics
    track_functions = true
    track_performance = true
    track_errors = true
    exclude_functions = ["health-check"]

    # Form analytics
    track_forms = true
    track_conversions = true
    track_form_performance = true

    # Edge analytics
    track_edge_functions = true
    track_geo_data = true

    # Debug mode
    debug = false

Environment 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',

  // Build configuration
  build: {
    enabled: true,
    trackBuildTime: true,
    trackBundleSize: true,
    trackDependencies: true,
    trackOptimization: true
  },

  // Function configuration
  functions: {
    enabled: true,
    trackInvocations: true,
    trackPerformance: true,
    trackErrors: true,
    trackColdStarts: true,
    excludePatterns: ['/_netlify/*', '/health']
  },

  // Form configuration
  forms: {
    enabled: true,
    trackSubmissions: true,
    trackConversions: true,
    trackValidationErrors: true,
    trackFormAbandonment: true
  },

  // Edge configuration
  edge: {
    enabled: true,
    trackGeoData: true,
    trackPerformance: true,
    trackCacheHits: true
  },

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

API Reference

Core Classes

class EntrolyticsNetlify {
  constructor(config: NetlifyConfig)

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

  // Function tracking
  trackFunction(
    event: HandlerEvent,
    properties?: Record<string, any>
  ): Promise<void>
  trackFunctionInvocation(
    name: string,
    duration: number,
    properties?: Record<string, any>
  ): Promise<void>
  trackFunctionError(error: Error, context: FunctionContext): Promise<void>

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

  // Form tracking
  trackFormSubmission(formData: FormData): Promise<void>
  trackFormConversion(
    formData: FormData,
    conversionValue?: number
  ): Promise<void>
  trackFormError(error: Error, formData: FormData): Promise<void>

  // Edge analytics
  trackEdgeRequest(
    event: HandlerEvent,
    properties?: Record<string, any>
  ): Promise<void>
  trackGeoData(geoData: GeoData): Promise<void>
  trackCacheHit(cacheKey: string, hit: boolean): Promise<void>

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

class EntrolyticsNetlifyMiddleware {
  constructor(config: NetlifyConfig)

  trackRequest(
    event: HandlerEvent,
    properties?: Record<string, any>
  ): Promise<void>
  trackPerformance(
    event: HandlerEvent,
    metrics: PerformanceMetrics
  ): Promise<void>
  trackError(error: Error, context: HandlerContext): Promise<void>
}

Type Definitions

interface NetlifyConfig {
  websiteId: string
  apiKey?: string
  host?: string
  build?: {
    enabled?: boolean
    trackBuildTime?: boolean
    trackBundleSize?: boolean
    trackDependencies?: boolean
    trackOptimization?: boolean
  }
  functions?: {
    enabled?: boolean
    trackInvocations?: boolean
    trackPerformance?: boolean
    trackErrors?: boolean
    trackColdStarts?: boolean
    excludePatterns?: string[]
  }
  forms?: {
    enabled?: boolean
    trackSubmissions?: boolean
    trackConversions?: boolean
    trackValidationErrors?: boolean
    trackFormAbandonment?: boolean
  }
  edge?: {
    enabled?: boolean
    trackGeoData?: boolean
    trackPerformance?: boolean
    trackCacheHits?: boolean
  }
  debug?: boolean
}

interface BuildMetrics {
  buildTime: number
  bundleSize: number
  dependencies: string[]
  optimization: {
    minified: boolean
    compressed: boolean
    treeshaken: boolean
  }
  environment: string
}

interface DeploymentInfo {
  url: string
  environment: string
  commit: string
  branch: string
  timestamp: Date
  netlifyConfig: Record<string, any>
}

interface FormData {
  formName: string
  formType: string
  fields: Record<string, any>
  source: string
  timestamp: Date
}

interface GeoData {
  country: string
  region: string
  city: string
  latitude: number
  longitude: number
}

Advanced Usage

Advanced Function Analytics

// netlify/functions/advanced-analytics.ts
import { Handler, HandlerEvent, HandlerContext } from '@netlify/functions'
import { EntrolyticsNetlify } from '@entrolytics/netlify-plugin'

const entrolytics = new EntrolyticsNetlify({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
  apiKey: process.env.ENTROLYTICS_API_KEY!,
  functions: {
    enabled: true,
    trackPerformance: true,
    trackColdStarts: true
  }
})

export const handler: Handler = async (
  event: HandlerEvent,
  context: HandlerContext
) => {
  const startTime = Date.now()

  try {
    // Track function start
    await entrolytics.trackFunction(event, {
      functionName: 'advanced-analytics',
      stage: 'start',
      region: process.env.NETLIFY_REGION
    })

    // Detect cold start
    const isColdStart = !globalThis.warmed
    if (isColdStart) {
      await entrolytics.track('function_cold_start', {
        functionName: 'advanced-analytics',
        region: process.env.NETLIFY_REGION
      })
      globalThis.warmed = true
    }

    // Your business logic
    const result = await processAdvancedAnalytics(event)

    // Track performance
    const duration = Date.now() - startTime
    await entrolytics.trackFunctionInvocation('advanced-analytics', duration, {
      success: true,
      resultSize: JSON.stringify(result).length
    })

    return {
      statusCode: 200,
      body: JSON.stringify(result),
      headers: {
        'X-Response-Time': `${duration}ms`,
        'X-Cold-Start': String(isColdStart)
      }
    }
  } catch (error) {
    // Track function error
    await entrolytics.trackFunctionError(error as Error, context)

    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Internal server error' })
    }
  }
}

Form Analytics with Conversion Tracking

// netlify/functions/contact-form.ts
import { Handler } from '@netlify/functions'
import { EntrolyticsNetlify } from '@entrolytics/netlify-plugin'

const entrolytics = new EntrolyticsNetlify({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
  apiKey: process.env.ENTROLYTICS_API_KEY!,
  forms: {
    enabled: true,
    trackConversions: true,
    trackFormAbandonment: true
  }
})

export const handler: Handler = async (event) => {
  if (event.httpMethod !== 'POST') {
    return {
      statusCode: 405,
      body: JSON.stringify({ error: 'Method not allowed' })
    }
  }

  try {
    const formData = JSON.parse(event.body || '{}')

    // Track form submission
    await entrolytics.trackFormSubmission({
      formName: formData.formName,
      formType: 'contact',
      fields: {
        name: formData.name,
        email: formData.email,
        subject: formData.subject
      },
      source: event.headers['referer'] || 'direct',
      timestamp: new Date()
    })

    // Process form
    const result = await processContactForm(formData)

    // Track conversion if successful
    if (result.success) {
      await entrolytics.trackFormConversion(
        {
          formName: formData.formName,
          formType: 'contact',
          fields: formData,
          source: event.headers['referer'] || 'direct',
          timestamp: new Date()
        },
        1
      ) // Conversion value of 1
    }

    return {
      statusCode: 200,
      body: JSON.stringify(result)
    }
  } catch (error) {
    // Track form error
    await entrolytics.trackFormError(error as Error, {
      formName: 'contact',
      formType: 'contact',
      fields: {},
      source: 'direct',
      timestamp: new Date()
    })

    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Failed to process form' })
    }
  }
}

// Client-side form tracking with abandonment detection
// scripts/form-analytics.js
class FormAnalytics {
  constructor() {
    this.setupFormTracking()
    this.setupAbandonmentTracking()
  }

  setupFormTracking() {
    document.querySelectorAll('form').forEach((form) => {
      form.addEventListener('submit', (event) => {
        const formData = new FormData(form)
        const formName = form.dataset.name || 'unknown'

        // Track form interaction
        fetch('/api/track-form-interaction', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            formName,
            action: 'submit',
            fields: Object.fromEntries(formData.entries())
          })
        })
      })

      // Track field interactions
      form.addEventListener(
        'focus',
        (event) => {
          const fieldName = event.target.name
          this.trackFieldInteraction(form.dataset.name, fieldName, 'focus')
        },
        true
      )

      form.addEventListener(
        'blur',
        (event) => {
          const fieldName = event.target.name
          this.trackFieldInteraction(form.dataset.name, fieldName, 'blur')
        },
        true
      )
    })
  }

  setupAbandonmentTracking() {
    let formStartTime = null
    let currentForm = null

    document.addEventListener('focusin', (event) => {
      const form = event.target.closest('form')
      if (form && !formStartTime) {
        formStartTime = Date.now()
        currentForm = form
      }
    })

    document.addEventListener('focusout', (event) => {
      if (formStartTime && currentForm) {
        // Check if user left the form without submitting
        setTimeout(() => {
          if (document.activeElement?.closest('form') !== currentForm) {
            const timeSpent = Date.now() - formStartTime

            // Track form abandonment
            fetch('/api/track-form-abandonment', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                formName: currentForm.dataset.name,
                timeSpent,
                fields: this.getFilledFields(currentForm)
              })
            })

            formStartTime = null
            currentForm = null
          }
        }, 1000)
      }
    })
  }

  trackFieldInteraction(formName, fieldName, action) {
    fetch('/api/track-field-interaction', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        formName,
        fieldName,
        action,
        timestamp: Date.now()
      })
    })
  }

  getFilledFields(form) {
    const fields = {}
    form.querySelectorAll('input, select, textarea').forEach((field) => {
      if (field.value) {
        fields[field.name] = field.type === 'password' ? '***' : field.value
      }
    })
    return fields
  }
}

// Initialize form analytics
new FormAnalytics()

Build and Deployment Analytics

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

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

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

  await entrolytics.trackBuildStart()

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

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

  await entrolytics.trackBuildComplete({
    buildTime: buildMetrics.duration,
    bundleSize: buildMetrics.bundleSize,
    dependencies: buildMetrics.dependencies,
    optimization: {
      minified: buildMetrics.minified,
      compressed: buildMetrics.compressed,
      treeshaken: buildMetrics.treeshaken
    },
    environment: process.env.NODE_ENV,
    netlifyEnvironment: process.env.NETLIFY_ENV,
    netlifyRegion: process.env.NETLIFY_REGION
  })

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

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

  await entrolytics.trackDeployment({
    url: deploymentInfo.url,
    environment: deploymentInfo.environment,
    commit: deploymentInfo.commit,
    branch: deploymentInfo.branch,
    timestamp: new Date(),
    netlifyConfig: deploymentInfo.netlifyConfig,
    buildTime: deploymentInfo.buildTime,
    deployTime: deploymentInfo.deployTime
  })

  console.log('✅ Deployment tracked')
}

// Export for use in build scripts
module.exports = {
  trackBuildStart,
  trackBuildComplete,
  trackDeployment
}
// package.json scripts
{
  "scripts": {
    "build": "node scripts/track-build-start.js && npm run build:site && node scripts/track-build-complete.js",
    "build:site": "vite build",
    "postbuild": "entrolytics-netlify-build"
  }
}

Edge Function Analytics

// netlify/edge-functions/analytics.ts
import { Context } from '@netlify/edge-functions'
import { EntrolyticsNetlifyEdge } from '@entrolytics/netlify-plugin'

const entrolytics = new EntrolyticsNetlifyEdge({
  websiteId: process.env.ENTROLYTICS_WEBSITE_ID!,
  apiKey: process.env.ENTROLYTICS_API_KEY!,
  edge: {
    enabled: true,
    trackGeoData: true,
    trackPerformance: true,
    trackCacheHits: true
  }
})

export default async (request: Request, context: Context) => {
  const startTime = Date.now()

  try {
    // Track edge request
    await entrolytics.trackEdgeRequest(request, {
      functionName: 'analytics-edge',
      edgeRegion: context.geo?.country || 'unknown',
      city: context.geo?.city || 'unknown'
    })

    // Track geo data
    if (context.geo) {
      await entrolytics.trackGeoData({
        country: context.geo.country,
        region: context.geo.subdivision,
        city: context.geo.city,
        latitude: context.geo.latitude || 0,
        longitude: context.geo.longitude || 0
      })
    }

    // Your edge function logic
    const response = await processRequest(request, context)

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

    return new Response(JSON.stringify(response), {
      headers: {
        'Content-Type': 'application/json',
        'X-Edge-Region': context.geo?.country || 'unknown',
        'X-Response-Time': `${duration}ms`
      }
    })
  } catch (error) {
    // Track edge error
    await entrolytics.track('edge_function_error', {
      functionName: 'analytics-edge',
      error: (error as Error).message,
      edgeRegion: context.geo?.country || 'unknown'
    })

    return new Response(JSON.stringify({ error: 'Internal server error' }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    })
  }
}

Testing

Unit Tests

import { EntrolyticsNetlify } from '@entrolytics/netlify-plugin'
import { HandlerEvent, HandlerContext } from '@netlify/functions'

describe('EntrolyticsNetlify', () => {
  let entrolytics: EntrolyticsNetlify
  let mockEvent: Partial<HandlerEvent>
  let mockContext: Partial<HandlerContext>

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

    mockEvent = {
      httpMethod: 'GET',
      path: '/api/test',
      headers: {
        'user-agent': 'test-agent',
        'x-forwarded-for': '192.168.1.1'
      }
    }

    mockContext = {
      functionName: 'test-function',
      functionVersion: '1.0'
    }
  })

  test('should track function invocations', async () => {
    await entrolytics.trackFunction(mockEvent as HandlerEvent, {
      custom: 'property'
    })

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

  test('should track form submissions', async () => {
    const formData = {
      formName: 'contact',
      formType: 'contact',
      fields: {
        name: 'John Doe',
        email: 'john@example.com'
      },
      source: 'direct',
      timestamp: new Date()
    }

    await entrolytics.trackFormSubmission(formData)

    expect(entrolytics.track).toHaveBeenCalledWith('form_submission', {
      formName: 'contact',
      formType: 'contact',
      fields: formData.fields,
      source: 'direct'
    })
  })

  test('should track build metrics', async () => {
    const buildMetrics = {
      buildTime: 120000,
      bundleSize: 1024000,
      dependencies: ['react', 'netlify-cli'],
      optimization: {
        minified: true,
        compressed: true,
        treeshaken: true
      },
      environment: 'production'
    }

    await entrolytics.trackBuildComplete(buildMetrics)

    expect(entrolytics.track).toHaveBeenCalledWith('build_completed', {
      buildTime: 120000,
      bundleSize: 1024000,
      dependenciesCount: 2,
      optimization: buildMetrics.optimization,
      environment: 'production'
    })
  })
})

Integration Tests

describe('Netlify Integration', () => {
  let app: any
  let entrolytics: EntrolyticsNetlify

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

    // Mock Netlify function handler
    app = {
      handler: async (event: HandlerEvent) => {
        await entrolytics.trackFunction(event)
        return {
          statusCode: 200,
          body: JSON.stringify({ message: 'Success' })
        }
      }
    }
  })

  test('should track function calls', async () => {
    const mockEvent = {
      httpMethod: 'GET',
      path: '/api/test',
      headers: { 'user-agent': 'test-agent' }
    }

    const response = await app.handler(mockEvent)

    expect(response.statusCode).toBe(200)
    expect(entrolytics.track).toHaveBeenCalledWith('function_invocation', {
      method: 'GET',
      path: '/api/test',
      userAgent: 'test-agent'
    })
  })
})

Performance Optimization

Function Optimization

// Optimized Netlify function
export const handler: Handler = async (event, context) => {
  // Early return for health checks
  if (event.path === '/health') {
    return {
      statusCode: 200,
      body: JSON.stringify({ status: 'ok' })
    }
  }

  // Track asynchronously to avoid blocking
  entrolytics.trackFunction(event).catch(console.error)

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

  return {
    statusCode: 200,
    body: JSON.stringify(result)
  }
}

// Warm-up function for cold starts
export const warmUp: Handler = async () => {
  // Pre-warm the analytics client
  await entrolytics.warmUp()

  // Pre-warm database connections
  await warmUpDatabase()

  return {
    statusCode: 200,
    body: JSON.stringify({ warmed: true })
  }
}

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 Netlify 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 Netlify Plugin
import { EntrolyticsNetlify } from '@entrolytics/netlify-plugin'

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

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

From Other Netlify Analytics

// Before - Other Netlify analytics
import { NetlifyAnalytics } from 'other-netlify-analytics'

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

analytics.track('event', properties)

// After - Entrolytics Netlify Plugin
import { EntrolyticsNetlify } from '@entrolytics/netlify-plugin'

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

await entrolytics.track('event', properties)

// Additional Netlify-specific features
await entrolytics.trackBuildComplete(buildMetrics)
await entrolytics.trackDeployment(deploymentInfo)
await entrolytics.trackFormSubmission(formData)

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