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-pluginQuick 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 = 200Environment Variables
# Netlify Environment Variables
ENTROLYTICS_WEBSITE_ID=your-website-id
ENTROLYTICS_API_KEY=your-api-key
ENTROLYTICS_HOST=https://entrolytics.click
ENTROLYTICS_DEBUG=falseServerless 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 = falseEnvironment 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