Features

Events Package (BETA)

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

The @fyit/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: @fyit/crouton-events
Version: 0.1.0 (BETA) Type: Nuxt Layer (Addon)
Dependencies: @fyit/crouton-core

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 @fyit/crouton-core package installed first.

# Install both base and events addon
pnpm add @fyit/crouton-core @fyit/crouton-events

Basic Setup

Add both layers to your nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    '@fyit/crouton-core',    // Base layer (required)
    '@fyit/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 @fyit/crouton-core layer and the events addon.

Configuration

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

export default defineNuxtConfig({
  extends: [
    '@fyit/crouton-core',
    '@fyit/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: string | Date

  // 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?: Record<string, unknown>
}

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

Schema Fields

FieldTypeRequiredDescription
timestampstring | DateWhen 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 (Record<string, unknown>)

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.

Event Tracking Health State

The event-listener plugin tracks health internally via useState('crouton-events-health'). There is no standalone composable — access the state directly.

State Shape

interface CroutonEventsHealth {
  total: number           // Total tracking attempts
  failed: number          // Failed tracking attempts
  lastError: string | null
  lastErrorTime: Date | null
}

Usage

<script setup lang="ts">
const health = useState<{
  total: number
  failed: number
  lastError: string | null
  lastErrorTime: Date | null
}>('crouton-events-health')

const failureRate = computed(() => {
  if (!health.value?.total) return 0
  return (health.value.failed / health.value.total) * 100
})

const isHealthy = computed(() => failureRate.value < 10)
</script>

<template>
  <div v-if="health">
    <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>

Components

The package provides purpose-built components for displaying event data:

CroutonActivityLog

Full activity log page with stats, filters, pagination, and export.

Props

PropTypeDefaultDescription
collectionstringFilter to specific collection
userIdstringFilter to specific user
pageSizenumber50Items per page
showFiltersbooleantrueShow filters bar
showPaginationbooleantrueShow pagination
emptyMessagestring'No activity found'Empty state message
<template>
  <CroutonActivityLog />
</template>

CroutonActivityTimeline

Timeline visualization that groups events by date (Today, Yesterday, etc.).

Props

PropTypeDefaultDescription
eventsCroutonEvent[][]Events to display
loadingbooleanfalseShow loading state
emptyMessagestring'No activity found'Empty state message
<template>
  <CroutonActivityTimeline :events="events" @event-click="handleClick" />
</template>

CroutonActivityTimelineItem

Individual event row with operation badge, timestamp, user, and expandable changes.

Props

PropTypeDefaultDescription
eventCroutonEventEvent to display
expandedbooleanfalseWhether changes are expanded

CroutonActivityFilters

Filter controls for collection, operation, user, and date range presets.

Props

PropTypeDefaultDescription
modelValueFilterStateFilter state (v-model)
collectionsstring[][]Available collection names
usersArray<{ id: string, name: string }>[]Available users

CroutonEventDetail

Modal showing full event details with metadata and a changes diff table.

Props

PropTypeDefaultDescription
modelValuebooleanModal open state (v-model)
eventCroutonEventEvent to display

CroutonEventChangesTable

Before/after diff table for field-level changes.

Props

PropTypeDefaultDescription
changesEventChange[]Array of field changes
operationEventOperationOperation type (affects column visibility)

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-events — query tracked events
const response = await $fetch(`/api/teams/${teamId}/crouton-events`, {
  params: {
    collectionName: 'users',
    operation: 'update',
    page: 1,
    pageSize: 50
  }
})

// POST /api/teams/:teamId/crouton-collection-events — manual event tracking only
const response = await $fetch(`/api/teams/${teamId}/crouton-collection-events`, {
  method: 'POST',
  body: {
    operation: 'update',
    collection: 'users',
    itemId: 'user-123'
  }
})

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 useState('crouton-events-health') 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 (auto-imported in Nuxt server context)
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
    • Capped at 5000 deletions per run to avoid full-table scans on large tables. If excess remains, the next scheduled cleanup run continues where this one left off.
// 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.1.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
    pagination: {
      page: 1,
      pageSize: 50  // Don't fetch thousands at once
    }
    
  4. 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 = useState('crouton-events-health')
</script>

<template>
  <div v-if="health">
    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: string | Date
  operation: 'create' | 'update' | 'delete'
  collectionName: string
  itemId: string
  userId: string
  userName: string
  changes: EventChange[]
  metadata?: Record<string, unknown>
}

// 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.1.0)
  • Nuxt Crouton version
  • Error messages from console
  • Health monitoring stats
  • Steps to reproduce