Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/0m1n3m/contacts-db/llms.txt

Use this file to discover all available pages before exploring further.

The audit log is Contacts DB’s built-in record of every significant state change in the system. Every time a task moves from one status to another, a new AuditLog entry is written automatically — capturing who made the change, when it happened, what the previous state was, and what the new state became. This gives your team a reliable, tamper-evident trail for compliance reviews, retrospectives, and diagnosing bottlenecks in your workflow.

Audit log data model

The audit_logs table is designed to be generic: it can record changes to any model via entity_type and entity_id. For now, task status transitions are the primary data source.
ColumnTypeNotes
idbigintAuto-incrementing primary key
actor_idforeignIdusers (nullable)The user who triggered the change; null for system-initiated transitions
project_idforeignIdprojects (nullable)The project the changed entity belongs to; set to null if the project is deleted
entity_typestringFully-qualified class name of the changed model (e.g. App\Models\Task)
entity_idunsignedBigIntPrimary key of the changed model instance
actionstring (indexed)The type of change; task status transitions use status_changed
beforejson (nullable)State snapshot before the change
afterjson (nullable)State snapshot after the change
ip_addressstring(45) (nullable)IPv4 or IPv6 address of the request that triggered the change
user_agentstring (nullable)Browser or client user-agent string
created_attimestampWhen the log entry was written
updated_attimestamp
Three composite indexes speed up common queries: by (entity_type, entity_id), by (project_id, created_at), and by (actor_id, created_at).

What is logged

Every task status transition is recorded automatically. The flow is:
  1. A status change is saved on a Task model (via PATCH /tasks/{task} or PATCH /tasks/{task}/change-status).
  2. Laravel’s model observer fires the updating hook via TaskObserver.
  3. TaskObserver detects that the status attribute is dirty and calls TimeTrackingService::recordStateChange().
  4. TimeTrackingService calls its private logStateChange() method, which creates the AuditLog record with the before and after status values, the authenticated user’s ID, the current IP address, and the user-agent header.
No manual calls are required — logging is entirely side-effect driven.

Log entry structure

A typical status_changed entry looks like this:
{
  "id": 128,
  "actor_id": 3,
  "project_id": 7,
  "entity_type": "App\\Models\\Task",
  "entity_id": 42,
  "action": "status_changed",
  "before": { "status": "in_progress" },
  "after": { "status": "in_review" },
  "ip_address": "192.168.1.1",
  "user_agent": "Mozilla/5.0...",
  "created_at": "2026-07-20T14:22:00Z"
}
The before and after columns are cast to arrays by the AuditLog model, so you can read them directly as PHP arrays without manual json_decode calls.

Accessing the status history on a task

The Task model exposes two convenience methods for working with its audit trail.

$task->statusChanges()

Returns a MorphMany query builder scoped to action = 'status_changed' and ordered chronologically by created_at. Use this when you need to apply additional query constraints or eager-load relationships:
// Get the ten most recent status changes for a task
$recent = $task->statusChanges()->latest()->limit(10)->get();

// Eager-load the actor on each entry
$changes = $task->statusChanges()->with('actor')->get();

$task->getStatusHistory()

Returns a ready-to-display array of status transition records, with actor names resolved:
$history = $task->getStatusHistory();

// Returns:
[
    [
        'from'       => 'accepted',
        'to'         => 'in_progress',
        'changed_by' => 'Alice Smith',
        'changed_at' => '2026-07-20 09:00:00',
    ],
    [
        'from'       => 'in_progress',
        'to'         => 'in_review',
        'changed_by' => 'Bob Jones',
        'changed_at' => '2026-07-20 14:22:00',
    ],
    // ...
]
The changed_by value comes from $log->actor?->name, so it is null (not an error) for system-initiated transitions where actor_id is null.

Time tracking integration

TimeTrackingService uses the audit log as its source of truth when recalculating time metrics. The service reads statusChanges() entries in chronological order to recompute dev_time (seconds spent in_progress), review_time (seconds spent in_review), lead_time (total working time from creation to done), and backward_transitions (count of status regressions). If you manually correct audit log entries, or if you suspect a task’s time metrics have drifted out of sync, you can force a full recalculation:
use App\Services\TimeTrackingService;

$service = app(TimeTrackingService::class);
$service->recalculateAllTimesForTask($task);
This method runs inside Task::withoutEvents() to prevent the observer from firing again during the recalculation, and it performs a single $task->save() at the end to persist all updated metrics atomically.
The TimeTrackingService uses a defined status order to detect backward transitions:
StatusOrder
created0
accepted1
in_progress2
in_review3
done4
Any transition from a higher-order status to a lower-order status increments backward_transitions on the task.

Retention

The audit log table grows indefinitely — there is no automatic pruning. In a busy production environment the table can accumulate millions of rows over time. Consider scheduling a periodic cleanup job that deletes entries older than your retention policy (e.g. 12 months):
// Example artisan command or scheduled closure
AuditLog::where('created_at', '<', now()->subYear())->delete();
Retain at least the most recent entry for each task so that getStatusHistory() and time-tracking recalculation remain accurate.
The audit log is an excellent tool for compliance evidence and workflow debugging. If a task is stuck in in_review for an unexpectedly long time, pull its statusChanges() history to see exactly when it arrived in that state and who last touched it. You can also correlate actor_id and ip_address to verify that a specific user performed a status change at a given time.

Build docs developers (and LLMs) love