Features

Events Package (BETA)

Complete reference for @friendlyinternet/nuxt-crouton-events package - Event tracking and audit trails
Status: Experimental - This package is in active development (v0.3.0). APIs and features may change. Use in production with caution and expect potential breaking changes in future releases.

The @friendlyinternet/nuxt-crouton-events package provides comprehensive event tracking and audit trails for Nuxt Crouton applications. Automatically tracks all CREATE, UPDATE, and DELETE operations across collections with smart diff tracking and zero-configuration setup.

Package Overview

Package: @friendlyinternet/nuxt-crouton-events
Version: 0.3.0 (BETA)
Type: Nuxt Layer (Addon)
Dependencies: @friendlyinternet/nuxt-crouton

Key Features

  • Zero Configuration: Auto-tracks all collection mutations via hooks
  • 🎯 Smart Diff Tracking: Stores only changed fields to minimize storage
  • 👤 User Attribution: Captures user ID and username at event time
  • 📸 Historical Snapshots: Preserves user data for accurate audit trails
  • 🗑️ Auto-Cleanup: Configurable retention policy prevents database bloat
  • 🔍 Rich Querying: Filter by collection, operation, user, or date
  • 🚨 Error Handling: Development-friendly toasts with production safety
  • 📊 Health Monitoring: Track success/failure rates in real-time
  • 🛠️ Standard Collection: Generated UI components for viewing events

Installation

Prerequisites

You must have the base @friendlyinternet/nuxt-crouton package installed first.

# Install both base and events addon
pnpm add @friendlyinternet/nuxt-crouton @friendlyinternet/nuxt-crouton-events

Basic Setup

Add both layers to your nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    '@friendlyinternet/nuxt-crouton',         // Base layer (required)
    '@friendlyinternet/nuxt-crouton-events'   // Events addon
  ]
})

That's it! Events are now automatically tracked for all collection mutations.

Important: The events addon is a layer extension, not a standalone package. You must extend both the base layer and the events addon.

Configuration

Customize event tracking behavior in your nuxt.config.ts:

export default defineNuxtConfig({
  extends: [
    '@friendlyinternet/nuxt-crouton',
    '@friendlyinternet/nuxt-crouton-events'
  ],

  runtimeConfig: {
    public: {
      croutonEvents: {
        // Enable/disable tracking globally
        enabled: true,

        // Store username snapshot for audit trail
        snapshotUserName: true,

        // Error handling configuration
        errorHandling: {
          mode: 'toast',        // 'silent' | 'toast' | 'throw'
          logToConsole: true    // Log errors to console
        },

        // Automatic cleanup of old events
        retention: {
          enabled: true,        // Enable auto-cleanup
          days: 90,            // Keep events for 90 days
          maxEvents: 100000    // Or maximum number of events
        }
      }
    }
  }
})

Configuration Options

OptionTypeDefaultDescription
enabledbooleantrueEnable/disable event tracking globally
snapshotUserNamebooleantrueStore username at time of event
errorHandling.modestring'toast'How to handle tracking errors: 'silent', 'toast', or 'throw'
errorHandling.logToConsolebooleantrueLog errors to console
retention.enabledbooleantrueEnable automatic cleanup
retention.daysnumber90Keep events for N days
retention.maxEventsnumber100000Maximum number of events to keep

Event Schema

Database Structure

Each tracked event is stored with the following structure:

interface CroutonEvent {
  // Core identification
  id: string
  timestamp: Date
  teamId: string

  // Operation details
  operation: 'create' | 'update' | 'delete'
  collectionName: string
  itemId: string

  // User attribution
  userId: string
  userName: string  // Snapshot at time of event

  // Field-level changes
  changes: EventChange[]

  // Optional metadata
  metadata?: {
    ipAddress?: string
    userAgent?: string
    duration?: number
  }
}

interface EventChange {
  fieldName: string
  oldValue: string | null  // JSON stringified
  newValue: string | null  // JSON stringified
}

Schema Fields

FieldTypeRequiredDescription
timestampDateWhen the event occurred
operationstringType of operation: create, update, delete
collectionNamestringName of the collection modified
itemIdstringID of the item created/updated/deleted
userIdstringID of the user who performed the action
userNamestringName of user at time of event (historical snapshot)
changesJSONArray of field-level changes
metadataJSONAdditional context (IP address, user agent, etc.)

Smart Diff Logic

The package intelligently tracks only changed fields to minimize storage:

CREATE Operation:

  • Stores all fields as "new" (oldValue = null)
  • Excludes internal fields: id, createdAt, updatedAt, createdBy, updatedBy, teamId, owner

UPDATE Operation:

  • Compares before and after states
  • Stores only fields where values changed
  • Uses JSON stringify comparison for deep equality

DELETE Operation:

  • Stores all fields as "removed" (newValue = null)
  • Excludes internal fields

Composables

useCroutonEventTracker

Core composable for manual event tracking with smart diff calculation.

const { track, trackInBackground } = useCroutonEventTracker()

track(options)

Track an event synchronously (awaitable).

Parameters:

interface TrackEventOptions {
  operation: 'create' | 'update' | 'delete'
  collection: string
  itemId?: string
  itemIds?: string[]
  data?: any           // For create: new item data
  updates?: any        // For update: fields being updated
  result?: any         // Result after operation
  beforeData?: any     // State before operation (for update/delete)
}

Usage:

// Manual tracking (usually not needed - auto-tracking via plugin)
try {
  await track({
    operation: 'update',
    collection: 'users',
    itemId: 'user-123',
    beforeData: { name: 'John', email: 'john@example.com' },
    result: { name: 'Jane', email: 'john@example.com' }
  })
} catch (error) {
  console.error('Tracking failed:', error)
}

trackInBackground(options)

Track an event asynchronously (fire and forget with error handling).

Usage:

// Non-blocking tracking
trackInBackground({
  operation: 'create',
  collection: 'posts',
  data: { title: 'New Post', content: '...' }
})
Note: You rarely need to use this composable directly. The plugin automatically tracks all collection mutations via the crouton:mutation hook.

useCroutonEvents

Composable for querying events with filtering and enrichment options.

const { data, pending, error, refresh } = useCroutonEvents(options)

Options

interface UseCroutonEventsOptions {
  teamId?: string              // Override team context
  enrichUserData?: boolean     // Join with users table (future)
  
  filters?: {
    collectionName?: string    // Filter by collection
    operation?: 'create' | 'update' | 'delete'
    userId?: string            // Filter by user
    dateFrom?: Date            // Events after this date
    dateTo?: Date              // Events before this date
  }
  
  pagination?: {
    page?: number              // Page number (default: 1)
    pageSize?: number          // Items per page (default: 50)
  }
}

Basic Usage

// Get all events for current team
const { data: events, pending } = useCroutonEvents()

// Filter by collection
const { data: userEvents } = useCroutonEvents({
  filters: {
    collectionName: 'users'
  }
})

Filter by Operation

// Get only UPDATE operations
const { data: updates } = useCroutonEvents({
  filters: {
    operation: 'update'
  }
})

Filter by Date Range

// Get events from last 7 days
const sevenDaysAgo = new Date()
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)

const { data: recentEvents } = useCroutonEvents({
  filters: {
    dateFrom: sevenDaysAgo
  }
})

Combined Filters

// Complex query: user updates in posts collection, last 30 days
const { data: events } = useCroutonEvents({
  filters: {
    collectionName: 'posts',
    operation: 'update',
    userId: 'user-123',
    dateFrom: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
  },
  pagination: {
    page: 1,
    pageSize: 100
  }
})

User Data Enrichment (Future)

// Join with users table to get current user data
const { data: enrichedEvents } = useCroutonEvents({
  enrichUserData: true
})

// enrichedEvents[0].userName = "John Smith" (at time of event)
// enrichedEvents[0].user.currentName = "Jane Doe" (current)
// enrichedEvents[0].user.email = "jane@example.com"
BETA Note: The enrichUserData option is planned but not yet fully implemented. Currently returns events without user JOIN.

useCroutonEventsHealth

Composable for monitoring event tracking health and failure rates.

const { health, failureRate, isHealthy } = useCroutonEventsHealth()

Return Values

interface CroutonEventsHealth {
  health: {
    total: number           // Total tracking attempts
    failed: number          // Failed tracking attempts
    lastError: string | null
    lastErrorTime: Date | null
  }
  failureRate: ComputedRef<number>  // Failure percentage (0-100)
  isHealthy: ComputedRef<boolean>   // true if < 10% failure rate
}

Usage

<script setup lang="ts">
const { health, failureRate, isHealthy } = useCroutonEventsHealth()
</script>

<template>
  <div>
    <h3>Event Tracking Health</h3>
    <div>Total Events: {{ health.total }}</div>
    <div>Failed: {{ health.failed }}</div>
    <div>Failure Rate: {{ failureRate.toFixed(2) }}%</div>
    <div>Status: {{ isHealthy ? '✅ Healthy' : '⚠️ Degraded' }}</div>
    
    <div v-if="health.lastError">
      <p>Last Error: {{ health.lastError }}</p>
      <p>Time: {{ health.lastErrorTime }}</p>
    </div>
  </div>
</template>

Generated Components

The package automatically generates standard Crouton collection components for viewing events:

CroutonEventsCollectionEventsList

Pre-built list component for viewing events with filtering and pagination.

<template>
  <CroutonEventsCollectionEventsList />
</template>

This component includes:

  • Sortable columns (timestamp, operation, collection, user)
  • Search and filtering
  • Pagination
  • Detail view for individual events
  • Change history display

Query Patterns

Using Standard Collection Query

Since events are a Crouton collection, you can use the standard useCollectionQuery composable:

Query Patterns: For complete useCollectionQuery patterns, see Querying Data.
// Events can be queried like any other collection
const { data: events, pending } = await useCollectionQuery('collectionEvents', {
  teamId: currentTeam.id,
  filters: { collectionName: 'users', operation: 'create' }
})

Direct API Access

Events are accessible via the standard Crouton API endpoints:

// GET /api/teams/:teamId/crouton-collection-events
const response = await $fetch(`/api/teams/${teamId}/crouton-collection-events`)

// With filters
const response = await $fetch(`/api/teams/${teamId}/crouton-collection-events`, {
  params: {
    collectionName: 'users',
    operation: 'update',
    page: 1,
    pageSize: 50
  }
})

Architecture

How It Works

The event tracking system uses a hook-based architecture:

  1. Core Hooks: nuxt-crouton emits crouton:mutation hooks after successful CRUD operations
  2. Event Listener Plugin: This package subscribes to those hooks via a Nuxt plugin
  3. Smart Diff: Calculates field-level changes (oldValue → newValue)
  4. Async Tracking: Events tracked in background without blocking user operations
  5. Storage: Events stored in same database as collections (NuxtHub D1/SQLite)

Auto-Tracking Plugin

The event-listener.ts plugin automatically subscribes to collection mutations:

// Runs automatically - no configuration needed
nuxtApp.hooks.hook('crouton:mutation', async (event) => {
  // Track event in background
  await track({
    operation: event.operation,
    collection: event.collection,
    itemId: event.itemId,
    data: event.data,
    result: event.result,
    beforeData: event.beforeData
  })
})

Performance Characteristics

  • Non-blocking: Events tracked asynchronously after mutation completes
  • Minimal overhead: Smart diff stores only changed fields
  • Indexed queries: Fast filtering by collection, user, date
  • Auto-cleanup: Configurable retention prevents database bloat

Storage Estimates

OperationTypical SizeDescription
CREATE~500 bytesAll fields stored as new
UPDATE~200-400 bytesOnly changed fields
DELETE~150 bytesMinimal metadata
10,000 events≈ 3-5 MBTotal database impact

Error Handling

The package uses environment-aware error handling:

Development Mode

  • ⚠️ Toast Notifications: Failed tracking shows visible toast
  • 📝 Console Logging: Full error details logged to console
  • 🎯 Error Details: Stack traces and context included
⚠️ Event tracking failed
Description: Network error or validation failure

Production Mode

  • 🔇 Silent Logging: Errors logged to console only
  • 🚫 No User Disruption: Failed tracking never blocks operations
  • 📊 Health Monitoring: Use useCroutonEventsHealth() to detect issues

Error Handling Configuration

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      croutonEvents: {
        errorHandling: {
          mode: 'toast',        // Development-friendly
          logToConsole: true
        }
      }
    }
  }
})

// Options for mode:
// 'silent'  - No UI feedback, console logging only
// 'toast'   - Show toast in dev, silent in prod
// 'throw'   - Throw errors (not recommended - blocks operations)

Data Retention & Cleanup

Automatic Cleanup

The package includes a cleanup utility to prevent database bloat:

// Server-side utility
import { cleanupOldEvents } from '#crouton-events/server/utils/cleanup'

// Run cleanup (respects config settings)
const result = await cleanupOldEvents()

console.log(result)
// {
//   deletedCount: 5234,
//   oldestRemaining: Date('2025-08-18T...'),
//   totalRemaining: 94766
// }

Cleanup Options

interface CleanupOptions {
  retentionDays?: number   // Override config setting
  maxEvents?: number       // Override config setting
  dryRun?: boolean        // Preview without deleting
}

// Dry run to see what would be deleted
const preview = await cleanupOldEvents({ dryRun: true })

// Custom retention (keep only 30 days)
const result = await cleanupOldEvents({ retentionDays: 30 })

// Limit by count (keep max 50k events)
const result = await cleanupOldEvents({ maxEvents: 50000 })

Cleanup Strategy

The cleanup utility uses a two-phase approach:

  1. Phase 1: Age-based deletion
    • Deletes events older than retentionDays
    • Example: 90 days (default)
  2. Phase 2: Count-based deletion
    • If total still exceeds maxEvents, delete oldest events
    • Deletes in batches of 1000 to avoid query limits
// Cleanup process
1. Count total events: 125,000
2. Delete events > 90 days: -20,000 (105,000 remaining)
3. Check max limit: 105,000 > 100,000
4. Delete 5,000 oldest events: -5,000 (100,000 remaining)
5. Result: 25,000 deleted, 100,000 remaining

Scheduled Cleanup (NuxtHub)

You can schedule automatic cleanup using NuxtHub's scheduled tasks:

// server/tasks/cleanup-events.ts
export default defineTask({
  meta: {
    name: 'cleanup-old-events',
    description: 'Remove old event tracking data'
  },
  
  // Run daily at 3 AM
  run: async () => {
    const result = await cleanupOldEvents()
    
    return {
      result: 'success',
      deletedCount: result.deletedCount,
      remaining: result.totalRemaining
    }
  }
})

Migration & Stability

Beta Stability Warning

This package is in active development (v0.3.0 BETA). Be aware of:

What's Stable:

  • ✅ Core event tracking and storage
  • ✅ Smart diff calculation
  • ✅ Auto-tracking plugin
  • ✅ Basic querying and filtering
  • ✅ Cleanup utilities

What May Change:

  • ⚠️ User data enrichment API
  • ⚠️ Advanced query filters
  • ⚠️ Event schema (additional fields)
  • ⚠️ Configuration structure
  • ⚠️ Component APIs

Migration Expectations

When upgrading between beta versions:

  1. Schema Changes: May require database migrations
  2. Config Changes: Runtime config structure may evolve
  3. API Changes: Composable signatures may change
  4. Breaking Changes: Expect breaking changes until v1.0

Recommended Approach:

  • Pin to specific version in package.json
  • Test thoroughly before upgrading
  • Review changelog for breaking changes
  • Consider event data as audit logs (preserve on migration)

Best Practices

When to Use Events

Good Use Cases:

  • ✅ Audit trails for compliance
  • ✅ User activity monitoring
  • ✅ Change history for important records
  • ✅ Debugging data issues
  • ✅ Analytics and reporting

Not Recommended For:

  • ❌ Real-time notifications (use WebSockets)
  • ❌ Undo/redo functionality (too expensive)
  • ❌ Version control (consider separate versioning system)
  • ❌ High-frequency events (> 1000/sec)

Performance Tips

  1. Configure Retention Aggressively
    retention: {
      days: 30,        // Keep only recent data
      maxEvents: 50000 // Limit total events
    }
    
  2. Use Specific Filters
    // Good: Specific filters reduce result set
    filters: {
      collectionName: 'users',
      operation: 'update',
      dateFrom: last7Days
    }
    

// Bad: Fetching all events const { data } = useCroutonEvents()


3. **Paginate Results**
```typescript
pagination: {
  page: 1,
  pageSize: 50  // Don't fetch thousands at once
}
  1. Index Frequently Queried Fields
    • collectionName
    • userId
    • timestamp
    • operation

Security Considerations

  1. Sensitive Data
    // Avoid tracking sensitive fields
    // Consider excluding fields like passwords, tokens, etc.
    // The smart diff will store field values as JSON
    
  2. User Attribution
    // userName snapshot helps with audit trails
    // But consider privacy implications
    snapshotUserName: true  // Default
    
  3. Access Control
    // Events inherit team-based access control
    // Only users in the team can view events
    // Enforce via middleware in custom endpoints
    

Troubleshooting

Events Not Being Tracked

Check 1: Verify configuration

const config = useRuntimeConfig()
console.log(config.public.croutonEvents?.enabled)  // Should be true

Check 2: Verify user session

const { user } = useUserSession()
console.log(user.value)  // Should exist

Check 3: Check health monitoring

<script setup lang="ts">
const { health, failureRate } = useCroutonEventsHealth()
</script>

<template>
  <div>
    Failed: {{ health.failed }} / {{ health.total }}
    Last error: {{ health.lastError }}
  </div>
</template>

High Failure Rate

Possible Causes:

  • Network issues
  • Database connection problems
  • Invalid user session
  • Validation errors

Debug Steps:

// Enable console logging
croutonEvents: {
  errorHandling: {
    logToConsole: true
  }
}

// Check browser console for errors
// Check network tab for failed API calls

Events Not Appearing in List

Check 1: Verify team context

const { getTeamId } = useTeamContext()
console.log(getTeamId())  // Should match event teamId

Check 2: Check filters

// Remove filters to see all events
const { data } = useCroutonEvents({
  filters: {}  // No filters
})

Database Growing Too Large

Solution 1: Run manual cleanup

// In server endpoint or task
const result = await cleanupOldEvents()

Solution 2: Adjust retention

croutonEvents: {
  retention: {
    days: 30,         // Shorter retention
    maxEvents: 10000  // Lower limit
  }
}

API Reference

Types

// Core event type
interface CroutonEvent {
  id: string
  timestamp: Date
  operation: 'create' | 'update' | 'delete'
  collectionName: string
  itemId: string
  teamId: string
  userId: string
  userName: string
  changes: EventChange[]
  metadata?: Record<string, any>
}

// Change record
interface EventChange {
  fieldName: string
  oldValue: string | null  // JSON stringified
  newValue: string | null  // JSON stringified
}

// Tracking options
interface TrackEventOptions {
  operation: 'create' | 'update' | 'delete'
  collection: string
  itemId?: string
  itemIds?: string[]
  data?: any
  updates?: any
  result?: any
  beforeData?: any
}

// Query options
interface UseCroutonEventsOptions {
  teamId?: string
  enrichUserData?: boolean
  filters?: {
    collectionName?: string
    operation?: 'create' | 'update' | 'delete'
    userId?: string
    dateFrom?: Date
    dateTo?: Date
  }
  pagination?: {
    page?: number
    pageSize?: number
  }
}

// Cleanup options
interface CleanupOptions {
  retentionDays?: number
  maxEvents?: number
  dryRun?: boolean
}

// Cleanup result
interface CleanupResult {
  deletedCount: number
  oldestRemaining: Date | null
  totalRemaining: number
}

// Health monitoring
interface CroutonEventsHealth {
  total: number
  failed: number
  lastError: string | null
  lastErrorTime: Date | null
}

Support & Feedback

This is a beta package. We welcome feedback and bug reports:

When reporting issues, include:

  • Package version (v0.3.0)
  • Nuxt Crouton version
  • Error messages from console
  • Health monitoring stats
  • Steps to reproduce