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: @fyit/crouton-events
Version: 0.1.0 (BETA)
Type: Nuxt Layer (Addon)
Dependencies: @fyit/crouton-core
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
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.
@fyit/crouton-core layer and the events addon.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
}
}
}
}
})
| 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 |
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
}
| Field | Type | Required | Description |
|---|---|---|---|
timestamp | string | 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 (Record<string, unknown>) |
The package intelligently tracks only changed fields to minimize storage:
CREATE Operation:
id, createdAt, updatedAt, createdBy, updatedBy, teamId, ownerUPDATE Operation:
DELETE Operation:
Core composable for manual event tracking with smart diff calculation.
const { track, trackInBackground } = useCroutonEventTracker()
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)
}
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.Composable for querying events with filtering and enrichment options.
const { data, pending, error, refresh } = useCroutonEvents(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)
}
}
// Get all events for current team
const { data: events, pending } = useCroutonEvents()
// Filter by collection
const { data: userEvents } = useCroutonEvents({
filters: {
collectionName: 'users'
}
})
// Get only UPDATE operations
const { data: updates } = useCroutonEvents({
filters: {
operation: 'update'
}
})
// Get events from last 7 days
const sevenDaysAgo = new Date()
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
const { data: recentEvents } = useCroutonEvents({
filters: {
dateFrom: sevenDaysAgo
}
})
// 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
}
})
// 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.The event-listener plugin tracks health internally via useState('crouton-events-health'). There is no standalone composable — access the state directly.
interface CroutonEventsHealth {
total: number // Total tracking attempts
failed: number // Failed tracking attempts
lastError: string | null
lastErrorTime: Date | null
}
<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>
The package provides purpose-built components for displaying event data:
Full activity log page with stats, filters, pagination, and export.
| Prop | Type | Default | Description |
|---|---|---|---|
collection | string | — | Filter to specific collection |
userId | string | — | Filter to specific user |
pageSize | number | 50 | Items per page |
showFilters | boolean | true | Show filters bar |
showPagination | boolean | true | Show pagination |
emptyMessage | string | 'No activity found' | Empty state message |
<template>
<CroutonActivityLog />
</template>
Timeline visualization that groups events by date (Today, Yesterday, etc.).
| Prop | Type | Default | Description |
|---|---|---|---|
events | CroutonEvent[] | [] | Events to display |
loading | boolean | false | Show loading state |
emptyMessage | string | 'No activity found' | Empty state message |
<template>
<CroutonActivityTimeline :events="events" @event-click="handleClick" />
</template>
Individual event row with operation badge, timestamp, user, and expandable changes.
| Prop | Type | Default | Description |
|---|---|---|---|
event | CroutonEvent | — | Event to display |
expanded | boolean | false | Whether changes are expanded |
Filter controls for collection, operation, user, and date range presets.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | FilterState | — | Filter state (v-model) |
collections | string[] | [] | Available collection names |
users | Array<{ id: string, name: string }> | [] | Available users |
Modal showing full event details with metadata and a changes diff table.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | — | Modal open state (v-model) |
event | CroutonEvent | — | Event to display |
Before/after diff table for field-level changes.
| Prop | Type | Default | Description |
|---|---|---|---|
changes | EventChange[] | — | Array of field changes |
operation | EventOperation | — | Operation type (affects column visibility) |
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' }
})
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'
}
})
The event tracking system uses a hook-based architecture:
nuxt-crouton emits crouton:mutation hooks after successful CRUD operationsThe 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
})
})
| 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 |
The package uses environment-aware error handling:
⚠️ Event tracking failed
Description: Network error or validation failure
useState('crouton-events-health') to detect issuesexport 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)
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
// }
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 })
The cleanup utility uses a two-phase approach:
retentionDaysmaxEvents, delete oldest events// 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
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
}
}
})
This package is in active development (v0.1.0 BETA). Be aware of:
What's Stable:
What May Change:
When upgrading between beta versions:
Recommended Approach:
package.jsonGood Use Cases:
Not Recommended For:
retention: {
days: 30, // Keep only recent data
maxEvents: 50000 // Limit total events
}
// Good: Specific filters reduce result set
filters: {
collectionName: 'users',
operation: 'update',
dateFrom: last7Days
}
// Bad: Fetching all events
const { data } = useCroutonEvents()
pagination: {
page: 1,
pageSize: 50 // Don't fetch thousands at once
}
collectionNameuserIdtimestampoperation// Avoid tracking sensitive fields
// Consider excluding fields like passwords, tokens, etc.
// The smart diff will store field values as JSON
// userName snapshot helps with audit trails
// But consider privacy implications
snapshotUserName: true // Default
// Events inherit team-based access control
// Only users in the team can view events
// Enforce via middleware in custom endpoints
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>
Possible Causes:
Debug Steps:
// Enable console logging
croutonEvents: {
errorHandling: {
logToConsole: true
}
}
// Check browser console for errors
// Check network tab for failed API calls
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
})
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
}
}
// 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
}
This is a beta package. We welcome feedback and bug reports:
When reporting issues, include:
v0.1.0)