iazaran/trace-replay
TraceReplay is an execution tracer for Laravel: instrument steps, view a waterfall timeline, auto-trace jobs/commands, track DB/cache/mail, and deterministically replay HTTP with JSON diffs. Includes PII masking, sampling, multi-tenant scoping, and AI debug prompts.
Production-conscious step-level tracing, deterministic HTTP replay, and AI-ready debugging for Laravel.
TraceReplay is not a standard error logger. It is a full-fledged execution tracer that captures every step of your complex workflows, reconstructs them with a waterfall timeline, and offers one-click AI debugging when things go wrong.

| Feature | TraceReplay | Telescope | Debugbar | Clockwork |
|---|---|---|---|---|
| Manual step instrumentation | ✅ | ❌ | ❌ | ❌ |
| Waterfall timeline UI | ✅ | ❌ | ✅ | ✅ |
| Deterministic HTTP replay | ✅ | ❌ | ❌ | ❌ |
| Visual JSON diff on replay | ✅ | ❌ | ❌ | ❌ |
| AI fix-prompt generator | ✅ | ❌ | ❌ | ❌ |
| OpenAI / Anthropic / Ollama | ✅ | ❌ | ❌ | ❌ |
| Cache & HTTP tracking | ✅ | ✅ | ✅ | ✅ |
| Mail & Notification tracking | ✅ | ❌ | ❌ | ❌ |
| DB query tracking per step | ✅ | ✅ | ✅ | ✅ |
| Memory tracking per step | ✅ | ❌ | ✅ | ✅ |
| Peak memory tracking | ✅ | ✅ | ❌ | ❌ |
| Livewire Hydrate Tracing | ✅ | ❌ | ❌ | ❌ |
| PII / sensitive-field masking | ✅ | ❌ | ❌ | ❌ |
| Queue-job auto-tracing | ✅ | ✅ | ❌ | ❌ |
| Artisan-command auto-tracing | ✅ | ✅ | ❌ | ❌ |
| Probabilistic Sampling | ✅ | ❌ | ❌ | ❌ |
| Dashboard auth & Gate gate | ✅ | ✅ | ❌ | N/A |
| Multi-tenant scoping | ✅ | ❌ | ❌ | ❌ |
| W3C Traceparent support | ✅ | ❌ | ❌ | ❌ |
| Octane Compatible | ✅ | ✅ | ❌ | ❌ |
composer require iazaran/trace-replay
Quick install:
php artisan trace-replay:install
php artisan migrate
Or publish the config manually:
php artisan vendor:publish --tag=trace-replay-config
Run migrations:
php artisan migrate
Note: Migrations run automatically without publishing. They use
jsoncolumns anddecimalprecision for timings, compatible with MySQL 5.7+, MariaDB, PostgreSQL, and SQLite.⚠️ Important When Updating: Package migrations do not execute automatically upon
composer update. Whenever you update TraceReplay to a newer version, you must runphp artisan migrateto ensure any newly introduced schema changes are applied. ReviewUPGRADE.mdfor behavioral changes between versions, especially around replay safety, job payload capture, and workspace ID scoping.
To customize the dashboard UI:
php artisan vendor:publish --tag=trace-replay-views
This copies the Blade templates to resources/views/vendor/trace-replay/ where you can customize the layout, colors, or add your own branding.
To automatically trace all HTTP requests, add the TraceMiddleware to your application. This provides instant visibility into every request without manual instrumentation.
For Laravel 10 (app/Http/Kernel.php):
protected $middlewareGroups = [
'web' => [
// ...
\TraceReplay\Http\Middleware\TraceMiddleware::class,
],
];
For Laravel 11+ (bootstrap/app.php):
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\TraceReplay\Http\Middleware\TraceMiddleware::class);
})
Open config/trace-replay.php. Key options include:
return [
'enabled' => env('TRACE_REPLAY_ENABLED', true),
// 0.1 = trace 10% of requests/jobs/commands
'sample_rate' => env('TRACE_REPLAY_SAMPLE_RATE', 1.0),
// Disable on low-cost production servers if query-log overhead is too high
'track_db_queries' => env('TRACE_REPLAY_TRACK_DB', true),
// Truncate captured JSON fields after 64 KB by default
'max_payload_size' => env('TRACE_REPLAY_MAX_PAYLOAD_SIZE', 65536),
// Automatically mask these keys in payloads, headers, and URL query strings
'mask_fields' => ['password', 'token', 'api_key', 'authorization', 'cookie', 'secret'],
// Query bindings can contain PII; keep disabled in production unless needed
'track_db_query_bindings' => env('TRACE_REPLAY_TRACK_DB_BINDINGS', false),
// Dashboard security: add a Gate for tighter production access
'middleware' => ['web', 'auth'],
'route_prefix' => env('TRACE_REPLAY_ROUTE_PREFIX', 'trace-replay'),
// Replay safety: mutating methods are blocked and override hosts are restricted
'replay' => [
'allow_mutating_methods' => env('TRACE_REPLAY_REPLAY_MUTATING', false),
'allowed_hosts' => array_filter(explode(',', env('TRACE_REPLAY_REPLAY_ALLOWED_HOSTS', ''))),
],
// Agent/MCP API is disabled until this token is set
'api' => [
'token' => env('TRACE_REPLAY_API_TOKEN'),
'middleware' => ['api'],
'route_prefix' => env('TRACE_REPLAY_API_ROUTE_PREFIX', 'api/trace-replay'),
'max_steps' => env('TRACE_REPLAY_API_MAX_STEPS', 500),
],
// AI Troubleshooting (Drivers: openai, anthropic, ollama)
'ai' => [
'driver' => env('TRACE_REPLAY_AI_DRIVER', 'openai'),
'api_key' => env('TRACE_REPLAY_AI_KEY'),
'model' => env('TRACE_REPLAY_AI_MODEL', 'gpt-4o'),
'base_url' => env('TRACE_REPLAY_AI_BASE_URL'),
],
// Async batch persistence via queue (Reduces overhead)
'queue' => [
'enabled' => env('TRACE_REPLAY_QUEUE_ENABLED', false),
],
// Auto-tracing
'auto_trace' => [
'jobs' => true,
'commands' => false,
'livewire' => true,
'capture_job_payload' => false,
],
];
For low-cost production servers, start with sampling instead of tracing every request:
TRACE_REPLAY_SAMPLE_RATE=0.05
TRACE_REPLAY_TRACK_DB=false
TRACE_REPLAY_MAX_PAYLOAD_SIZE=16384
Laravel 11+ applications do not always define an api middleware group. If your
app does not, replace trace-replay.api.middleware with middleware that exists
in your project, such as ['throttle:api'].
Wrap any complex logic in TraceReplay::step() — each callback's return value is passed through transparently.
use TraceReplay\Facades\TraceReplay;
class BookingService
{
public function handleBooking(array $payload): void
{
TraceReplay::start('Flight Booking', ['channel' => 'web']);
try {
$inventory = TraceReplay::step('Validate Inventory', function () use ($payload) {
return Inventory::check($payload['flight_id']);
});
TraceReplay::checkpoint('Inventory validated', ['seats_left' => $inventory->seats]);
TraceReplay::context(['user_tier' => auth()->user()->tier]);
TraceReplay::step('Charge Credit Card', function () use ($payload) {
return PaymentGateway::charge($payload['amount']);
});
TraceReplay::end('success');
} catch (\Exception $e) {
TraceReplay::end('error');
throw $e;
}
}
}
API Reference:
| Method | Description |
|---|---|
TraceReplay::start(name, tags[]) |
Start a new trace; returns Trace or null if disabled/sampled-out |
TraceReplay::step(label, callable, extra[]) |
Wrap callable, record timing, memory, DB queries, errors |
TraceReplay::measure(label, callable) |
Alias for step() — semantic clarity for benchmarks |
TraceReplay::checkpoint(label, state[]) |
Record a zero-overhead breadcrumb (no callable) |
TraceReplay::context(array) |
Merge data into the next step's state_snapshot |
TraceReplay::end(status) |
Finalise trace; status: success or error |
TraceReplay::getCurrentTrace() |
Returns the active Trace model (or null) |
TraceReplay::setWorkspaceId(id) |
Scope subsequent traces to a workspace |
TraceReplay::setProjectId(id) |
Scope subsequent traces to a project |
Use TraceReplay::fake() to verify your instrumentation in tests without hitting the database:
use TraceReplay\Facades\TraceReplay;
public function test_booking_records_steps()
{
$fake = TraceReplay::fake();
$this->post('/book', ['flight_id' => 123]);
$fake->assertTraceStarted('Flight Booking');
$fake->assertStepRecorded('Validate Inventory');
$fake->assertStepRecorded('Charge Credit Card');
$fake->assertCheckpointRecorded('Inventory validated');
$fake->assertTraceEnded('success');
$fake->assertTraceCount(1);
}
Available assertions:
| Assertion | Description |
|---|---|
assertTraceStarted(name) |
Assert a trace with the given name was started |
assertNoTraceStarted() |
Assert no trace was started at all |
assertTraceCount(n) |
Assert exactly n traces were started |
assertStepRecorded(label) |
Assert a step with the given label was recorded |
assertCheckpointRecorded(label) |
Assert a checkpoint with the given label was recorded |
assertStepCount(n, traceName?) |
Assert exactly n steps in total (or in a named trace) |
assertTraceEnded(status) |
Assert a trace with the given final status exists |
Queue jobs are automatically traced when auto_trace.jobs is enabled (default: true). No manual listener registration is needed — the service provider wires everything up.
To disable, set TRACE_REPLAY_AUTO_TRACE_JOBS=false in your .env.
Job payloads are not captured by default because queue payloads often contain sensitive application data. To opt in:
TRACE_REPLAY_CAPTURE_JOB_PAYLOAD=true
Artisan commands can be auto-traced by enabling auto_trace.commands:
TRACE_REPLAY_AUTO_TRACE_COMMANDS=true
Internal commands like queue:work, horizon, and trace-replay:prune are excluded by default (see auto_trace.exclude_commands in the config).
Drop the <x-trace-replay-trace-bar /> Blade component into your layout for instant in-page trace inspection:
{{-- resources/views/layouts/app.blade.php --}}
@if(config('app.debug'))
<x-trace-replay-trace-bar />
@endif
Access the built-in dashboard at https://your-app.com/trace-replay.

Features:

Add authentication or authorization middleware in config/trace-replay.php:
'middleware' => ['web', 'auth', 'can:view-trace-replay'],
Then define the gate:
// app/Providers/AuthServiceProvider.php
Gate::define('view-trace-replay', function ($user) {
return in_array($user->email, config('trace-replay.admin_emails', []));
});
Or use IP allowlisting (exact match, comma-separated via env):
TRACE_REPLAY_ALLOWED_IPS=203.0.113.5,10.0.0.1
HTTP replay is intentionally conservative:
TRACE_REPLAY_REPLAY_MUTATING=true.override_url can only target the originally recorded host by default.TRACE_REPLAY_REPLAY_ALLOWED_HOSTS=staging.example.com,*.internal.test to allow explicit replay targets.Authorization, cookies, CSRF tokens, and forwarded headers are stripped before replay.When trace-replay.notifications.on_failure is enabled, mail/Slack notifications are dispatched through a queued job after the response so slow webhooks do not add latency to failed requests.
For any failed trace the dashboard shows an AI Fix Prompt button that generates a structured markdown prompt including:
The AI prompt feature works without any API key. Copy the generated prompt and paste it into ChatGPT, Claude, or any other AI assistant.
For a seamless experience, configure an AI driver to get answers directly in the dashboard:
# OpenAI (default)
TRACE_REPLAY_AI_DRIVER=openai
TRACE_REPLAY_AI_KEY=sk-your-openai-key
TRACE_REPLAY_AI_MODEL=gpt-4o
# Or Anthropic Claude
TRACE_REPLAY_AI_DRIVER=anthropic
TRACE_REPLAY_AI_KEY=sk-ant-your-key
TRACE_REPLAY_AI_MODEL=claude-3-5-sonnet-latest
# Or Ollama (local, no API key needed)
TRACE_REPLAY_AI_DRIVER=ollama
TRACE_REPLAY_AI_MODEL=llama3
TRACE_REPLAY_AI_BASE_URL=http://localhost:11434/api/generate
With a key configured, clicking "Ask AI" sends the prompt to your chosen AI provider and displays the response in the dashboard.
TraceReplay exposes a JSON-RPC 2.0 endpoint at POST /api/trace-replay/mcp for autonomous AI agents.
The API is disabled until TRACE_REPLAY_API_TOKEN is configured. Send requests with:
Authorization: Bearer your-token
Available methods:
| Method | Params | Returns |
|---|---|---|
list_traces |
limit, status, filter_by_error |
Paginated trace summaries |
get_trace_context |
trace_id, step_limit |
Trace context with capped steps |
generate_fix_prompt |
trace_id |
Markdown debugging prompt |
trigger_replay |
trace_id |
Replay result + JSON diff |
Example request:
{
"jsonrpc": "2.0",
"method": "generate_fix_prompt",
"params": { "trace_id": "9b12f7e4-..." },
"id": 1
}
step_limit defaults to trace-replay.api.max_steps (500) and is capped at
that value to keep large traces from producing oversized API responses.
Automatically prune old traces with the built-in Artisan command. Add to your scheduler:
// app/Console/Kernel.php
$schedule->command('trace-replay:prune --days=30')->daily();
Options:
php artisan trace-replay:prune --days=30 # Delete traces older than 30 days
php artisan trace-replay:prune --days=30 --dry-run # Preview what would be deleted
php artisan trace-replay:prune --days=7 --status=error # Only prune error traces
Set TRACE_REPLAY_RETENTION_DAYS to choose the default window. If retention_days is null, the prune command exits without deleting data unless --days is passed.
Check production-readiness settings with:
php artisan trace-replay:doctor
The command reports dashboard protection, API token status, sample rate, retention, replay safety, query binding capture, and whether the trace tables exist.
Export a trace to JSON or CSV for archiving or external analysis:
php artisan trace-replay:export {id} --format=json
php artisan trace-replay:export {id} --format=csv
php artisan trace-replay:export {id} --format=json --output=/tmp/trace.json
php artisan trace-replay:export --status=error --format=json # Export all error traces
composer install
composer test
composer format:test
The test suite covers:
failed, successful, search)error_step, total_db_queries, total_memory_usage, completion_percentage)PayloadMasker — recursive PII field redaction, case-insensitivityAiPromptService — prompt generation, OpenAI integration (mocked), null-safetyReplayService — HTTP replay and JSON difftrace-replay:prune (delete, dry-run, status filter, validation)trace-replay:export (JSON, CSV, file output, status filter, validation)TraceReplayFake — assertions for started/count/steps/checkpoints/endedNotificationService — error_reason array/string serialisation safetyThe MIT License (MIT). See LICENSE for details.
How can I help you explore Laravel packages today?