Events Package (BETA)
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.
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
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable/disable event tracking globally |
snapshotUserName | boolean | true | Store username at time of event |
errorHandling.mode | string | 'toast' | How to handle tracking errors: 'silent', 'toast', or 'throw' |
errorHandling.logToConsole | boolean | true | Log errors to console |
retention.enabled | boolean | true | Enable automatic cleanup |
retention.days | number | 90 | Keep events for N days |
retention.maxEvents | number | 100000 | Maximum 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
| Field | Type | Required | Description |
|---|---|---|---|
timestamp | Date | ✅ | When the event occurred |
operation | string | ✅ | Type of operation: create, update, delete |
collectionName | string | ✅ | Name of the collection modified |
itemId | string | ✅ | ID of the item created/updated/deleted |
userId | string | ✅ | ID of the user who performed the action |
userName | string | ✅ | Name of user at time of event (historical snapshot) |
changes | JSON | ✅ | Array of field-level changes |
metadata | JSON | ❌ | Additional 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: '...' }
})
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"
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:
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:
- Core Hooks:
nuxt-croutonemitscrouton:mutationhooks after successful CRUD operations - Event Listener Plugin: This package subscribes to those hooks via a Nuxt plugin
- Smart Diff: Calculates field-level changes (oldValue → newValue)
- Async Tracking: Events tracked in background without blocking user operations
- 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
| Operation | Typical Size | Description |
|---|---|---|
| CREATE | ~500 bytes | All fields stored as new |
| UPDATE | ~200-400 bytes | Only changed fields |
| DELETE | ~150 bytes | Minimal metadata |
| 10,000 events | ≈ 3-5 MB | Total 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:
- Phase 1: Age-based deletion
- Deletes events older than
retentionDays - Example: 90 days (default)
- Deletes events older than
- Phase 2: Count-based deletion
- If total still exceeds
maxEvents, delete oldest events - Deletes in batches of 1000 to avoid query limits
- If total still exceeds
// 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:
- Schema Changes: May require database migrations
- Config Changes: Runtime config structure may evolve
- API Changes: Composable signatures may change
- 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
- Configure Retention Aggressively
retention: { days: 30, // Keep only recent data maxEvents: 50000 // Limit total events } - 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
}
- Index Frequently Queried Fields
collectionNameuserIdtimestampoperation
Security Considerations
- Sensitive Data
// Avoid tracking sensitive fields // Consider excluding fields like passwords, tokens, etc. // The smart diff will store field values as JSON - User Attribution
// userName snapshot helps with audit trails // But consider privacy implications snapshotUserName: true // Default - 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
}
Related Resources
Support & Feedback
This is a beta package. We welcome feedback and bug reports:
- GitHub Issues: nuxt-crouton/issues
- Discussions: nuxt-crouton/discussions
When reporting issues, include:
- Package version (
v0.3.0) - Nuxt Crouton version
- Error messages from console
- Health monitoring stats
- Steps to reproduce