diff --git a/README.md b/README.md index a24bbf9..a618cac 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > **High-fidelity process tracking, deterministic replay, and AI-powered debugging for Laravel β€” production & enterprise ready.** -[![Latest Version](https://img.shields.io/packagist/v/iazaran/tracereplay)](https://packagist.org/packages/iazaran/tracereplay) +[![Latest Version](https://img.shields.io/packagist/v/iazaran/trace-replay)](https://packagist.org/packages/iazaran/trace-replay) [![PHP](https://img.shields.io/badge/PHP-8.2%2B-blue)](https://php.net) [![Laravel](https://img.shields.io/badge/Laravel-10%20|%2011%20|%2012%20|%2013-red)](https://laravel.com) [![License: MIT](https://img.shields.io/badge/License-MIT-green)](LICENSE) @@ -10,7 +10,7 @@ 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. -![TraceReplay Dashboard](https://raw.githubusercontent.com/iazaran/tracereplay/refs/heads/main/art/preview.png) +![TraceReplay Dashboard](https://raw.githubusercontent.com/iazaran/trace-replay/refs/heads/main/art/preview.png) --- @@ -40,14 +40,14 @@ TraceReplay is not a standard error logger. It is a full-fledged **execution tra ## πŸ›  Installation ```bash -composer require iazaran/tracereplay +composer require iazaran/trace-replay ``` Publish the config and migrations: ```bash -php artisan vendor:publish --tag=tracereplay-config -php artisan vendor:publish --tag=tracereplay-migrations +php artisan vendor:publish --tag=trace-replay-config +php artisan vendor:publish --tag=trace-replay-migrations ``` Run migrations: @@ -63,79 +63,79 @@ php artisan migrate TraceReplay ships with a polished, dark-themed dashboard featuring a waterfall timeline, syntax-highlighted JSON inspector, and live stats β€” all styled and ready to use out of the box. Publishing the views lets you customise the layout, colours, or add your own branding: ```bash -php artisan vendor:publish --tag=tracereplay-views +php artisan vendor:publish --tag=trace-replay-views ``` -This copies the Blade templates to `resources/views/vendor/tracereplay/` where you can edit them freely. The package will automatically use your published versions instead of its built-in views. +This copies the Blade templates to `resources/views/vendor/trace-replay/` where you can edit them freely. The package will automatically use your published versions instead of its built-in views. --- ## βš™οΈ Configuration -Open `config/tracereplay.php`. Every option is documented inline; the key ones are: +Open `config/trace-replay.php`. Every option is documented inline; the key ones are: ```php return [ // Globally enable or disable tracing (useful for CI) - 'enabled' => env('TRACEREPLAY_ENABLED', true), + 'enabled' => env('TRACE_REPLAY_ENABLED', true), // Probabilistic sampling β€” 1.0 = always trace, 0.1 = trace 10% of requests - 'sample_rate' => env('TRACEREPLAY_SAMPLE_RATE', 1.0), + 'sample_rate' => env('TRACE_REPLAY_SAMPLE_RATE', 1.0), // Multi-tenant project ID (optional) - 'project_id' => env('TRACEREPLAY_PROJECT_ID', null), + 'project_id' => env('TRACE_REPLAY_PROJECT_ID', null), // Automatically mask these keys in request/response payloads 'mask_fields' => ['password', 'password_confirmation', 'token', 'api_key', 'authorization', 'secret', 'credit_card', 'cvv', 'ssn', 'private_key'], // Track DB queries inside each step - 'track_db_queries' => env('TRACEREPLAY_TRACK_DB', true), + 'track_db_queries' => env('TRACE_REPLAY_TRACK_DB', true), // Dashboard route middleware (add 'auth' or custom gate middleware for production) 'middleware' => ['web'], 'api_middleware' => ['api'], // IP allowlist for the dashboard (exact match; empty = allow all) - 'allowed_ips' => array_filter(explode(',', env('TRACEREPLAY_ALLOWED_IPS', ''))), + 'allowed_ips' => array_filter(explode(',', env('TRACE_REPLAY_ALLOWED_IPS', ''))), // Async step persistence via a queue 'queue' => [ - 'enabled' => env('TRACEREPLAY_QUEUE_ENABLED', false), - 'connection' => env('TRACEREPLAY_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')), - 'queue' => env('TRACEREPLAY_QUEUE_NAME', 'default'), + 'enabled' => env('TRACE_REPLAY_QUEUE_ENABLED', false), + 'connection' => env('TRACE_REPLAY_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')), + 'queue' => env('TRACE_REPLAY_QUEUE_NAME', 'default'), ], // Replay engine 'replay' => [ - 'default_base_url' => env('TRACEREPLAY_REPLAY_URL', env('APP_URL', 'http://localhost')), - 'timeout' => env('TRACEREPLAY_REPLAY_TIMEOUT', 30), + 'default_base_url' => env('TRACE_REPLAY_REPLAY_URL', env('APP_URL', 'http://localhost')), + 'timeout' => env('TRACE_REPLAY_REPLAY_TIMEOUT', 30), ], // Auto-pruning retention period (days) - 'retention_days' => env('TRACEREPLAY_RETENTION_DAYS', 30), + 'retention_days' => env('TRACE_REPLAY_RETENTION_DAYS', 30), // Failure notifications (email / Slack webhook) 'notifications' => [ - 'on_failure' => env('TRACEREPLAY_NOTIFY_ON_FAILURE', false), + 'on_failure' => env('TRACE_REPLAY_NOTIFY_ON_FAILURE', false), 'channels' => ['mail'], - 'mail' => ['to' => env('TRACEREPLAY_NOTIFY_EMAIL')], - 'slack' => ['webhook_url' => env('TRACEREPLAY_SLACK_WEBHOOK')], + 'mail' => ['to' => env('TRACE_REPLAY_NOTIFY_EMAIL')], + 'slack' => ['webhook_url' => env('TRACE_REPLAY_SLACK_WEBHOOK')], ], // OpenAI integration for in-dashboard AI responses 'ai' => [ - 'openai_api_key' => env('TRACEREPLAY_OPENAI_KEY'), - 'model' => env('TRACEREPLAY_OPENAI_MODEL', 'gpt-4o'), + 'openai_api_key' => env('TRACE_REPLAY_OPENAI_KEY'), + 'model' => env('TRACE_REPLAY_OPENAI_MODEL', 'gpt-4o'), ], // Auto-tracing for jobs and artisan commands (registered automatically) 'auto_trace' => [ - 'jobs' => env('TRACEREPLAY_AUTO_TRACE_JOBS', true), - 'commands' => env('TRACEREPLAY_AUTO_TRACE_COMMANDS', false), + 'jobs' => env('TRACE_REPLAY_AUTO_TRACE_JOBS', true), + 'commands' => env('TRACE_REPLAY_AUTO_TRACE_COMMANDS', false), 'exclude_commands' => [ 'queue:work', 'queue:listen', 'horizon', 'schedule:run', - 'schedule:work', 'tracereplay:prune', 'tracereplay:export', + 'schedule:work', 'trace-replay:prune', 'trace-replay:export', ], ], ]; @@ -222,7 +222,7 @@ For Laravel 11+ (using `bootstrap/app.php`): 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 `TRACEREPLAY_AUTO_TRACE_JOBS=false` in your `.env`. +To disable, set `TRACE_REPLAY_AUTO_TRACE_JOBS=false` in your `.env`. --- @@ -231,21 +231,21 @@ To disable, set `TRACEREPLAY_AUTO_TRACE_JOBS=false` in your `.env`. Artisan commands can be auto-traced by enabling `auto_trace.commands`: ```env -TRACEREPLAY_AUTO_TRACE_COMMANDS=true +TRACE_REPLAY_AUTO_TRACE_COMMANDS=true ``` -Internal commands like `queue:work`, `horizon`, and `tracereplay:prune` are excluded by default (see `auto_trace.exclude_commands` in the config). +Internal commands like `queue:work`, `horizon`, and `trace-replay:prune` are excluded by default (see `auto_trace.exclude_commands` in the config). --- ### Debug Bar Component -Drop the `` Blade component into your layout for instant in-page trace inspection: +Drop the `` Blade component into your layout for instant in-page trace inspection: ```blade {{-- resources/views/layouts/app.blade.php --}} @if(config('app.debug')) - + @endif ``` @@ -253,7 +253,7 @@ Drop the `` Blade component into your layout for inst ## 🎨 The Dashboard -Access the built-in dashboard at `https://your-app.com/tracereplay`. +Access the built-in dashboard at `https://your-app.com/trace-replay`. **Features:** - **Waterfall timeline** β€” visual bars show each step's exact duration relative to the total trace @@ -265,25 +265,25 @@ Access the built-in dashboard at `https://your-app.com/tracereplay`. ### Securing the Dashboard -Add authentication or authorization middleware in `config/tracereplay.php`: +Add authentication or authorization middleware in `config/trace-replay.php`: ```php -'middleware' => ['web', 'auth', 'can:view-tracereplay'], +'middleware' => ['web', 'auth', 'can:view-trace-replay'], ``` Then define the gate: ```php // app/Providers/AuthServiceProvider.php -Gate::define('view-tracereplay', function ($user) { - return in_array($user->email, config('tracereplay.admin_emails', [])); +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): ```env -TRACEREPLAY_ALLOWED_IPS=203.0.113.5,10.0.0.1 +TRACE_REPLAY_ALLOWED_IPS=203.0.113.5,10.0.0.1 ``` --- @@ -303,7 +303,7 @@ Paste this into any LLM. Optionally configure your OpenAI key and click **"Ask A ## πŸ€– MCP / AI-Agent JSON-RPC API -TraceReplay exposes a JSON-RPC 2.0 endpoint at `POST /api/tracereplay/mcp` for autonomous AI agents. +TraceReplay exposes a JSON-RPC 2.0 endpoint at `POST /api/trace-replay/mcp` for autonomous AI agents. **Available methods:** @@ -333,15 +333,15 @@ Automatically prune old traces with the built-in Artisan command. Add to your sc ```php // app/Console/Kernel.php -$schedule->command('tracereplay:prune --days=30')->daily(); +$schedule->command('trace-replay:prune --days=30')->daily(); ``` Options: ```bash -php artisan tracereplay:prune --days=30 # Delete traces older than 30 days -php artisan tracereplay:prune --days=30 --dry-run # Preview what would be deleted -php artisan tracereplay:prune --days=7 --status=error # Only prune error traces +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 ``` --- @@ -351,10 +351,10 @@ php artisan tracereplay:prune --days=7 --status=error # Only prune error traces Export a trace to JSON or CSV for archiving or external analysis: ```bash -php artisan tracereplay:export {id} --format=json -php artisan tracereplay:export {id} --format=csv -php artisan tracereplay:export {id} --format=json --output=/tmp/trace.json -php artisan tracereplay:export --status=error --format=json # Export all error traces +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 ``` --- @@ -378,8 +378,8 @@ composer install - Dashboard β€” index, filters, search, show, stats, export, replay, AI prompt - MCP API β€” REST endpoints and JSON-RPC (all methods + error handling) - Middleware β€” TraceMiddleware (route skipping, disabled config), AuthMiddleware (IP allow/block) -- Artisan `tracereplay:prune` (delete, dry-run, status filter, validation) -- Artisan `tracereplay:export` (JSON, CSV, file output, status filter, validation) +- Artisan `trace-replay:prune` (delete, dry-run, status filter, validation) +- Artisan `trace-replay:export` (JSON, CSV, file output, status filter, validation) - Blade components β€” TraceBar rendering with enabled/disabled states --- diff --git a/composer.json b/composer.json index b7be60d..15cdbab 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "iazaran/tracereplay", + "name": "iazaran/trace-replay", "description": "A high-fidelity process tracking, replay, and AI debugging package for Laravel.", "type": "library", "license": "MIT", @@ -18,7 +18,7 @@ "monitoring", "observability" ], - "homepage": "https://iazaran.github.io/tracereplay/", + "homepage": "https://iazaran.github.io/trace-replay/", "require": { "php": "^8.2", "illuminate/support": "^10.0|^11.0|^12.0|^13.0", diff --git a/config/tracereplay.php b/config/trace-replay.php similarity index 75% rename from config/tracereplay.php rename to config/trace-replay.php index 765351c..1c1083d 100644 --- a/config/tracereplay.php +++ b/config/trace-replay.php @@ -4,12 +4,12 @@ /* |-------------------------------------------------------------------------- - | Enable / Disable TraceReplay + | Enable / Disable Trace-Replay |-------------------------------------------------------------------------- - | Set TRACEREPLAY_ENABLED=false in production .env to completely disable + | Set TRACE_REPLAY_ENABLED=false in production .env to completely disable | all tracing with zero overhead. */ - 'enabled' => env('TRACEREPLAY_ENABLED', true), + 'enabled' => env('TRACE_REPLAY_ENABLED', true), /* |-------------------------------------------------------------------------- @@ -17,9 +17,9 @@ |-------------------------------------------------------------------------- | A float between 0.0 and 1.0 controlling what fraction of HTTP requests | are traced. 1.0 = trace every request, 0.1 = trace 10% at random. - | Manual TraceReplay::start() calls are never sampled. + | Manual Trace-Replay::start() calls are never sampled. */ - 'sample_rate' => env('TRACEREPLAY_SAMPLE_RATE', 1.0), + 'sample_rate' => env('TRACE_REPLAY_SAMPLE_RATE', 1.0), /* |-------------------------------------------------------------------------- @@ -28,7 +28,7 @@ | Optionally set a static project UUID, or override determineProjectId() | in a custom TraceReplayManager binding for dynamic multi-tenancy. */ - 'project_id' => env('TRACEREPLAY_PROJECT_ID', null), + 'project_id' => env('TRACE_REPLAY_PROJECT_ID', null), /* |-------------------------------------------------------------------------- @@ -38,9 +38,9 @@ | worker to avoid adding latency to the request lifecycle. */ 'queue' => [ - 'enabled' => env('TRACEREPLAY_QUEUE_ENABLED', false), - 'connection' => env('TRACEREPLAY_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')), - 'queue' => env('TRACEREPLAY_QUEUE_NAME', 'default'), + 'enabled' => env('TRACE_REPLAY_QUEUE_ENABLED', false), + 'connection' => env('TRACE_REPLAY_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')), + 'queue' => env('TRACE_REPLAY_QUEUE_NAME', 'default'), ], /* @@ -50,7 +50,7 @@ | When enabled, each step records the number and total time of DB queries | executed within the step closure. */ - 'track_db_queries' => env('TRACEREPLAY_TRACK_DB', true), + 'track_db_queries' => env('TRACE_REPLAY_TRACK_DB', true), /* |-------------------------------------------------------------------------- @@ -78,8 +78,8 @@ |-------------------------------------------------------------------------- */ 'replay' => [ - 'default_base_url' => env('TRACEREPLAY_REPLAY_URL', env('APP_URL', 'http://localhost')), - 'timeout' => env('TRACEREPLAY_REPLAY_TIMEOUT', 30), + 'default_base_url' => env('TRACE_REPLAY_REPLAY_URL', env('APP_URL', 'http://localhost')), + 'timeout' => env('TRACE_REPLAY_REPLAY_TIMEOUT', 30), ], /* @@ -87,17 +87,17 @@ | Retention / Auto-Pruning |-------------------------------------------------------------------------- | Traces older than `retention_days` will be deleted by the artisan command: - | php artisan tracereplay:prune + | php artisan trace-replay:prune | Set to null to disable pruning. */ - 'retention_days' => env('TRACEREPLAY_RETENTION_DAYS', 30), + 'retention_days' => env('TRACE_REPLAY_RETENTION_DAYS', 30), /* |-------------------------------------------------------------------------- | Dashboard Route Middleware |-------------------------------------------------------------------------- - | Protect the TraceReplay dashboard. For production use, add 'auth' or a - | custom gate middleware, e.g. ['web', 'auth', 'can:view-tracereplay']. + | Protect the Trace-Replay dashboard. For production use, add 'auth' or a + | custom gate middleware, e.g. ['web', 'auth', 'can:view-trace-replay']. */ 'middleware' => ['web'], 'api_middleware' => ['api'], @@ -110,7 +110,7 @@ | dashboard. CIDR notation is not evaluated β€” exact match only. | Leave empty to allow all IPs (rely on middleware for auth instead). */ - 'allowed_ips' => array_filter(explode(',', env('TRACEREPLAY_ALLOWED_IPS', ''))), + 'allowed_ips' => array_filter(explode(',', env('TRACE_REPLAY_ALLOWED_IPS', ''))), /* |-------------------------------------------------------------------------- @@ -120,13 +120,13 @@ | notification is dispatched via the configured channels. */ 'notifications' => [ - 'on_failure' => env('TRACEREPLAY_NOTIFY_ON_FAILURE', false), + 'on_failure' => env('TRACE_REPLAY_NOTIFY_ON_FAILURE', false), 'channels' => ['mail'], // 'mail', 'slack' 'mail' => [ - 'to' => env('TRACEREPLAY_NOTIFY_EMAIL', null), + 'to' => env('TRACE_REPLAY_NOTIFY_EMAIL', null), ], 'slack' => [ - 'webhook_url' => env('TRACEREPLAY_SLACK_WEBHOOK', null), + 'webhook_url' => env('TRACE_REPLAY_SLACK_WEBHOOK', null), ], ], @@ -139,8 +139,8 @@ | receive a copyable prompt instead (no external call is made). */ 'ai' => [ - 'openai_api_key' => env('TRACEREPLAY_OPENAI_KEY', null), - 'model' => env('TRACEREPLAY_OPENAI_MODEL', 'gpt-4o'), + 'openai_api_key' => env('TRACE_REPLAY_OPENAI_KEY', null), + 'model' => env('TRACE_REPLAY_OPENAI_MODEL', 'gpt-4o'), ], /* @@ -151,12 +151,12 @@ | in traces without any manual instrumentation. */ 'auto_trace' => [ - 'jobs' => env('TRACEREPLAY_AUTO_TRACE_JOBS', true), - 'commands' => env('TRACEREPLAY_AUTO_TRACE_COMMANDS', false), + 'jobs' => env('TRACE_REPLAY_AUTO_TRACE_JOBS', true), + 'commands' => env('TRACE_REPLAY_AUTO_TRACE_COMMANDS', false), // Artisan commands to exclude from auto-tracing (exact names) 'exclude_commands' => [ 'queue:work', 'queue:listen', 'horizon', 'schedule:run', - 'schedule:work', 'tracereplay:prune', 'tracereplay:export', + 'schedule:work', 'trace-replay:prune', 'trace-replay:export', ], ], ]; diff --git a/database/migrations/2024_01_01_000000_create_tracereplay_tables.php b/database/migrations/2024_01_01_000000_create_trace_replay_tables.php similarity index 100% rename from database/migrations/2024_01_01_000000_create_tracereplay_tables.php rename to database/migrations/2024_01_01_000000_create_trace_replay_tables.php diff --git a/docs/index.html b/docs/index.html index 75edd2e..51ba077 100644 --- a/docs/index.html +++ b/docs/index.html @@ -113,7 +113,7 @@ Docs Install API - GitHub + GitHub @@ -157,55 +157,55 @@

TraceReplay Documentation

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. It supports Laravel 10, 11, 12, and 13.

Installation

-
composer require iazaran/tracereplay
+
composer require iazaran/trace-replay

Publish config and migrations:

-
php artisan vendor:publish --tag=tracereplay-config
-php artisan vendor:publish --tag=tracereplay-migrations
+
php artisan vendor:publish --tag=trace-replay-config
+php artisan vendor:publish --tag=trace-replay-migrations
php artisan migrate

Migrations use json columns for full MySQL, MariaDB, PostgreSQL, and SQLite compatibility.

Publishing Views (Recommended)

TraceReplay ships with a polished, dark-themed dashboard featuring a waterfall timeline, syntax-highlighted JSON inspector, and live stats β€” all styled and ready to use out of the box. Publishing the views lets you customise the layout, colours, or add your own branding:

-
php artisan vendor:publish --tag=tracereplay-views
-

This copies the Blade templates to resources/views/vendor/tracereplay/ where you can edit them freely. The package will automatically use your published versions instead of its built-in views.

+
php artisan vendor:publish --tag=trace-replay-views
+

This copies the Blade templates to resources/views/vendor/trace-replay/ where you can edit them freely. The package will automatically use your published versions instead of its built-in views.

Configuration

-

Key options in config/tracereplay.php:

+

Key options in config/trace-replay.php:

return [
-    'enabled'          => env('TRACEREPLAY_ENABLED', true),
-    'sample_rate'      => env('TRACEREPLAY_SAMPLE_RATE', 1.0), // 0.1 = 10% of requests
-    'project_id'       => env('TRACEREPLAY_PROJECT_ID', null),
+    'enabled'          => env('TRACE_REPLAY_ENABLED', true),
+    'sample_rate'      => env('TRACE_REPLAY_SAMPLE_RATE', 1.0), // 0.1 = 10% of requests
+    'project_id'       => env('TRACE_REPLAY_PROJECT_ID', null),
     'mask_fields'      => ['password', 'password_confirmation', 'token', 'api_key',
                            'authorization', 'secret', 'credit_card', 'cvv', 'ssn', 'private_key'],
-    'track_db_queries' => env('TRACEREPLAY_TRACK_DB', true),
+    'track_db_queries' => env('TRACE_REPLAY_TRACK_DB', true),
     'middleware'        => ['web'],           // add 'auth', 'can:...' for production
     'api_middleware'    => ['api'],
-    'allowed_ips'      => array_filter(explode(',', env('TRACEREPLAY_ALLOWED_IPS', ''))),
+    'allowed_ips'      => array_filter(explode(',', env('TRACE_REPLAY_ALLOWED_IPS', ''))),
     'queue' => [
-        'enabled'    => env('TRACEREPLAY_QUEUE_ENABLED', false),
-        'connection' => env('TRACEREPLAY_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')),
-        'queue'      => env('TRACEREPLAY_QUEUE_NAME', 'default'),
+        'enabled'    => env('TRACE_REPLAY_QUEUE_ENABLED', false),
+        'connection' => env('TRACE_REPLAY_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')),
+        'queue'      => env('TRACE_REPLAY_QUEUE_NAME', 'default'),
     ],
     'replay' => [
-        'default_base_url' => env('TRACEREPLAY_REPLAY_URL', env('APP_URL')),
-        'timeout'          => env('TRACEREPLAY_REPLAY_TIMEOUT', 30),
+        'default_base_url' => env('TRACE_REPLAY_REPLAY_URL', env('APP_URL')),
+        'timeout'          => env('TRACE_REPLAY_REPLAY_TIMEOUT', 30),
     ],
-    'retention_days' => env('TRACEREPLAY_RETENTION_DAYS', 30),
+    'retention_days' => env('TRACE_REPLAY_RETENTION_DAYS', 30),
     'notifications' => [
-        'on_failure'  => env('TRACEREPLAY_NOTIFY_ON_FAILURE', false),
+        'on_failure'  => env('TRACE_REPLAY_NOTIFY_ON_FAILURE', false),
         'channels'    => ['mail'],
-        'mail'        => ['to' => env('TRACEREPLAY_NOTIFY_EMAIL')],
-        'slack'       => ['webhook_url' => env('TRACEREPLAY_SLACK_WEBHOOK')],
+        'mail'        => ['to' => env('TRACE_REPLAY_NOTIFY_EMAIL')],
+        'slack'       => ['webhook_url' => env('TRACE_REPLAY_SLACK_WEBHOOK')],
     ],
     'ai' => [
-        'openai_api_key' => env('TRACEREPLAY_OPENAI_KEY'),
-        'model'          => env('TRACEREPLAY_OPENAI_MODEL', 'gpt-4o'),
+        'openai_api_key' => env('TRACE_REPLAY_OPENAI_KEY'),
+        'model'          => env('TRACE_REPLAY_OPENAI_MODEL', 'gpt-4o'),
     ],
     'auto_trace' => [
-        'jobs'     => env('TRACEREPLAY_AUTO_TRACE_JOBS', true),
-        'commands' => env('TRACEREPLAY_AUTO_TRACE_COMMANDS', false),
+        'jobs'     => env('TRACE_REPLAY_AUTO_TRACE_JOBS', true),
+        'commands' => env('TRACE_REPLAY_AUTO_TRACE_COMMANDS', false),
         'exclude_commands' => ['queue:work', 'queue:listen', 'horizon', 'schedule:run',
-                               'schedule:work', 'tracereplay:prune', 'tracereplay:export'],
+                               'schedule:work', 'trace-replay:prune', 'trace-replay:export'],
     ],
 ];
@@ -270,11 +270,11 @@

HTTP Auto Ingestion

Auto Tracing β€” Jobs & Commands

Queue jobs are automatically traced when auto_trace.jobs is enabled (default: true). No manual listener registration needed β€” the service provider wires everything up.

Artisan commands can be auto-traced by enabling auto_trace.commands:

-
TRACEREPLAY_AUTO_TRACE_COMMANDS=true
-

Internal commands like queue:work, horizon, and tracereplay:prune are excluded by default (see auto_trace.exclude_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).

The Dashboard

-

Navigate to https://your-app.com/tracereplay.

+

Navigate to https://your-app.com/trace-replay.

  • Waterfall timeline β€” visual duration bars relative to total trace time
  • Live stats β€” auto-refreshing counters (total, failed, avg duration)
  • @@ -285,20 +285,20 @@

    The Dashboard

Security

-

Add authentication or authorization middleware in config/tracereplay.php:

-
// config/tracereplay.php
-'middleware' => ['web', 'auth', 'can:view-tracereplay'],
+            

Add authentication or authorization middleware in config/trace-replay.php:

+
// config/trace-replay.php
+'middleware' => ['web', 'auth', 'can:view-trace-replay'],
 
 // AuthServiceProvider
-Gate::define('view-tracereplay', fn ($user) =>
+Gate::define('view-trace-replay', fn ($user) =>
     in_array($user->email, ['admin@example.com'])
 );

Or restrict by IP (exact match, comma-separated via env):

-
TRACEREPLAY_ALLOWED_IPS=203.0.113.5,10.0.0.1
+
TRACE_REPLAY_ALLOWED_IPS=203.0.113.5,10.0.0.1

Data Masking

TraceReplay automatically redacts configured keys from all request/response payloads before they touch the database β€” including deeply nested values:

-
// config/tracereplay.php
+            
// config/trace-replay.php
 'mask_fields' => ['password', 'token', 'secret', 'api_key', 'credit_card'],
 
 // Input:  ['username' => 'alice', 'password' => 'hunter2']
@@ -318,7 +318,7 @@ 

AI Debugging

Configure OPENAI_API_KEY and click "Ask AI" to get an answer directly in the dashboard.

MCP / AI-Agent JSON-RPC API

-

Autonomous agents can query TraceReplay over JSON-RPC 2.0 at POST /api/tracereplay/mcp.

+

Autonomous agents can query TraceReplay over JSON-RPC 2.0 at POST /api/trace-replay/mcp.

@@ -338,14 +338,14 @@

MCP / AI-Agent JSON-RPC API

Data Retention

Prune old traces automatically via the scheduler:

// app/Console/Kernel.php
-$schedule->command('tracereplay:prune --days=30')->daily();
-
php artisan tracereplay:prune --days=30 --dry-run       # preview
-php artisan tracereplay:prune --days=7 --status=error    # prune only error traces
+$schedule->command('trace-replay:prune --days=30')->daily(); +
php artisan trace-replay:prune --days=30 --dry-run       # preview
+php artisan trace-replay:prune --days=7 --status=error    # prune only error traces

Export a trace for archiving:

-
php artisan tracereplay:export {id} --format=json
-php artisan tracereplay:export {id} --format=csv
-php artisan tracereplay:export {id} --format=json --output=/tmp/trace.json
-php artisan tracereplay:export --status=error --format=json  # all error traces
+
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  # all error traces

Testing

./vendor/bin/pest
diff --git a/docs/robots.txt b/docs/robots.txt index 77e19f7..aaf55be 100644 --- a/docs/robots.txt +++ b/docs/robots.txt @@ -1,3 +1,3 @@ User-agent: * Allow: / -Sitemap: https://iazaran.github.io/tracereplay/sitemap.xml +Sitemap: https://iazaran.github.io/trace-replay/sitemap.xml diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 651a20f..4216178 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -1,7 +1,7 @@ - https://iazaran.github.io/tracereplay/ + https://iazaran.github.io/trace-replay/ 2024-05-01T00:00:00+00:00 weekly 1.0 diff --git a/phpunit.xml b/phpunit.xml index ff1499b..b244bc1 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,7 +22,7 @@ - + diff --git a/resources/views/components/trace-bar.blade.php b/resources/views/components/trace-bar.blade.php index 1af6589..d4e525a 100644 --- a/resources/views/components/trace-bar.blade.php +++ b/resources/views/components/trace-bar.blade.php @@ -1,5 +1,5 @@ -@if(config('tracereplay.enabled') && $trace) -
@@ -28,13 +28,13 @@ {{ substr($trace->id, 0, 8) }} - View in Dashboard β†’ -
@endif diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php index b7e5c4c..e562c27 100644 --- a/resources/views/index.blade.php +++ b/resources/views/index.blade.php @@ -1,4 +1,4 @@ -@extends('tracereplay::layout') +@extends('trace-replay::layout') @section('title', 'Traces β€” TraceReplay') @section('content') @@ -21,7 +21,7 @@
-
+ @@ -39,7 +39,7 @@ class="px-3 py-2 text-sm rounded-lg bg-dark-700 border border-gray-700 text-gray Filter @if(request()->hasAny(['search','status'])) - + βœ• Clear @endif @@ -57,7 +57,7 @@ class="px-3 py-2 text-sm rounded-lg border transition flex items-center gap-1.5"
r.json()).then(d=>{ document.getElementById('stat-total').textContent = d.total; document.getElementById('stat-failed').textContent = d.failed + ' (' + d.failure_rate + '%)'; document.getElementById('stat-avg').textContent = d.avg_duration + ' ms'; @@ -132,11 +132,11 @@ class="px-3 py-2 text-sm rounded-lg border transition flex items-center gap-1.5" @endif
diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php index 63ce232..731bfeb 100644 --- a/resources/views/layout.blade.php +++ b/resources/views/layout.blade.php @@ -75,10 +75,10 @@ diff --git a/resources/views/show.blade.php b/resources/views/show.blade.php index 0582551..704043e 100644 --- a/resources/views/show.blade.php +++ b/resources/views/show.blade.php @@ -1,4 +1,4 @@ -@extends('tracereplay::layout') +@extends('trace-replay::layout') @section('title', ($trace->name ?? 'Trace') . ' β€” TraceReplay') @section('content') @@ -277,7 +277,7 @@ class="font-mono font-bold"> this.aiPromptContent = 'Generating expert debugging prompt…'; try { - const response = await fetch(`{{ route('tracereplay.ai.prompt', $trace->id) }}`, { + const response = await fetch(`{{ route('trace-replay.ai.prompt', $trace->id) }}`, { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, @@ -297,7 +297,7 @@ class="font-mono font-bold"> this.replayData = { original: 'Running replay...', replay: 'Waiting...' }; try { - const response = await fetch(`{{ route('tracereplay.replay', $trace->id) }}`, { + const response = await fetch(`{{ route('trace-replay.replay', $trace->id) }}`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/routes/api.php b/routes/api.php index 045cd4e..14e4cc9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,9 +4,9 @@ use TraceReplay\Http\Controllers\Api\McpController; Route::group([ - 'prefix' => 'api/tracereplay/mcp', - 'as' => 'tracereplay.api.mcp.', - 'middleware' => config('tracereplay.api_middleware', ['api']), + 'prefix' => 'api/trace-replay/mcp', + 'as' => 'trace-replay.api.mcp.', + 'middleware' => config('trace-replay.api_middleware', ['api']), ], function () { Route::post('/', [McpController::class, 'handleRpc'])->name('rpc'); diff --git a/routes/web.php b/routes/web.php index 0883156..2a00851 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,13 +5,13 @@ use TraceReplay\Http\Middleware\TraceReplayAuthMiddleware; $middleware = array_merge( - config('tracereplay.middleware', ['web']), + config('trace-replay.middleware', ['web']), [TraceReplayAuthMiddleware::class] ); Route::group([ - 'prefix' => 'tracereplay', - 'as' => 'tracereplay.', + 'prefix' => 'trace-replay', + 'as' => 'trace-replay.', 'middleware' => $middleware, ], function () { Route::get('/', [DashboardController::class, 'index'])->name('index'); diff --git a/src/Console/Commands/ExportTraceCommand.php b/src/Console/Commands/ExportTraceCommand.php index 6461895..f65d715 100644 --- a/src/Console/Commands/ExportTraceCommand.php +++ b/src/Console/Commands/ExportTraceCommand.php @@ -7,7 +7,7 @@ class ExportTraceCommand extends Command { - protected $signature = 'tracereplay:export + protected $signature = 'trace-replay:export {id? : UUID of the trace to export (omit to export all)} {--format=json : Export format: json or csv} {--output= : File path to write the output (defaults to stdout)} diff --git a/src/Console/Commands/PruneTracesCommand.php b/src/Console/Commands/PruneTracesCommand.php index 0729761..f875645 100644 --- a/src/Console/Commands/PruneTracesCommand.php +++ b/src/Console/Commands/PruneTracesCommand.php @@ -7,7 +7,7 @@ class PruneTracesCommand extends Command { - protected $signature = 'tracereplay:prune + protected $signature = 'trace-replay:prune {--days= : Override the retention_days config value} {--status= : Only prune traces with this status (success|error|processing)} {--dry-run : Show what would be deleted without deleting}'; @@ -16,7 +16,7 @@ class PruneTracesCommand extends Command public function handle(): int { - $days = (int) ($this->option('days') ?? config('tracereplay.retention_days', 30)); + $days = (int) ($this->option('days') ?? config('trace-replay.retention_days', 30)); $status = $this->option('status'); $dryRun = $this->option('dry-run'); diff --git a/src/Facades/TraceReplay.php b/src/Facades/TraceReplay.php index d5a431d..1dafde1 100644 --- a/src/Facades/TraceReplay.php +++ b/src/Facades/TraceReplay.php @@ -22,6 +22,6 @@ class TraceReplay extends Facade { protected static function getFacadeAccessor(): string { - return 'tracereplay'; + return 'trace-replay'; } } diff --git a/src/Http/Controllers/DashboardController.php b/src/Http/Controllers/DashboardController.php index 26ce35e..415091a 100644 --- a/src/Http/Controllers/DashboardController.php +++ b/src/Http/Controllers/DashboardController.php @@ -26,14 +26,14 @@ public function index(Request $request) $traces = $query->paginate(25)->withQueryString(); - return view('tracereplay::index', compact('traces')); + return view('trace-replay::index', compact('traces')); } public function show(string $id) { $trace = Trace::with('steps')->findOrFail($id); - return view('tracereplay::show', compact('trace')); + return view('trace-replay::show', compact('trace')); } public function replay(Request $request, string $id, ReplayService $replayService): JsonResponse diff --git a/src/Http/Middleware/TraceMiddleware.php b/src/Http/Middleware/TraceMiddleware.php index 59ebfd6..bc489f9 100644 --- a/src/Http/Middleware/TraceMiddleware.php +++ b/src/Http/Middleware/TraceMiddleware.php @@ -12,18 +12,18 @@ class TraceMiddleware { public function handle(Request $request, Closure $next): SymfonyResponse { - if (! config('tracereplay.enabled')) { + if (! config('trace-replay.enabled')) { return $next($request); } // Respect sampling rate - $sampleRate = (float) config('tracereplay.sample_rate', 1.0); + $sampleRate = (float) config('trace-replay.sample_rate', 1.0); if ($sampleRate < 1.0 && mt_rand() / mt_getrandmax() > $sampleRate) { return $next($request); } - // Skip tracereplay dashboard routes to avoid recursive tracing - if (str_starts_with($request->path(), 'tracereplay') || str_starts_with($request->path(), 'api/tracereplay')) { + // Skip trace-replay dashboard routes to avoid recursive tracing + if (str_starts_with($request->path(), 'trace-replay') || str_starts_with($request->path(), 'api/trace-replay')) { return $next($request); } @@ -58,7 +58,7 @@ public function handle(Request $request, Closure $next): SymfonyResponse public function terminate(Request $request, SymfonyResponse $response): void { - if (! config('tracereplay.enabled')) { + if (! config('trace-replay.enabled')) { return; } diff --git a/src/Http/Middleware/TraceReplayAuthMiddleware.php b/src/Http/Middleware/TraceReplayAuthMiddleware.php index 7979dee..6c88435 100644 --- a/src/Http/Middleware/TraceReplayAuthMiddleware.php +++ b/src/Http/Middleware/TraceReplayAuthMiddleware.php @@ -9,14 +9,14 @@ /** * Guards the TraceReplay dashboard routes. * - * - IP allowlist: if `tracereplay.allowed_ips` is non-empty, only those IPs can access the dashboard. - * - Can be combined with any Laravel auth middleware via the `tracereplay.middleware` config key. + * - IP allowlist: if `trace-replay.allowed_ips` is non-empty, only those IPs can access the dashboard. + * - Can be combined with any Laravel auth middleware via the `trace-replay.middleware` config key. */ class TraceReplayAuthMiddleware { public function handle(Request $request, Closure $next): Response { - $allowedIps = config('tracereplay.allowed_ips', []); + $allowedIps = config('trace-replay.allowed_ips', []); if (! empty($allowedIps) && ! \in_array($request->ip(), $allowedIps, true)) { abort(403, 'Access to TraceReplay dashboard is restricted.'); diff --git a/src/Listeners/CommandTraceListener.php b/src/Listeners/CommandTraceListener.php index a0df112..9a85cfb 100644 --- a/src/Listeners/CommandTraceListener.php +++ b/src/Listeners/CommandTraceListener.php @@ -10,7 +10,7 @@ class CommandTraceListener { public function onCommandStarting(CommandStarting $event): void { - if (! config('tracereplay.auto_trace.commands', false)) { + if (! config('trace-replay.auto_trace.commands', false)) { return; } @@ -19,7 +19,7 @@ public function onCommandStarting(CommandStarting $event): void return; } - $excluded = config('tracereplay.auto_trace.exclude_commands', []); + $excluded = config('trace-replay.auto_trace.exclude_commands', []); if (\in_array($event->command, $excluded, true)) { return; } @@ -34,7 +34,7 @@ public function onCommandStarting(CommandStarting $event): void public function onCommandFinished(CommandFinished $event): void { - if (! config('tracereplay.auto_trace.commands', false)) { + if (! config('trace-replay.auto_trace.commands', false)) { return; } @@ -42,7 +42,7 @@ public function onCommandFinished(CommandFinished $event): void return; } - $excluded = config('tracereplay.auto_trace.exclude_commands', []); + $excluded = config('trace-replay.auto_trace.exclude_commands', []); if (\in_array($event->command, $excluded, true)) { return; } diff --git a/src/Listeners/JobTraceListener.php b/src/Listeners/JobTraceListener.php index 34b94a9..5a70c65 100644 --- a/src/Listeners/JobTraceListener.php +++ b/src/Listeners/JobTraceListener.php @@ -11,7 +11,7 @@ class JobTraceListener { public function onJobProcessing(JobProcessing $event): void { - if (! config('tracereplay.auto_trace.jobs', true)) { + if (! config('trace-replay.auto_trace.jobs', true)) { return; } @@ -30,7 +30,7 @@ public function onJobProcessing(JobProcessing $event): void public function onJobProcessed(JobProcessed $_event): void { - if (! config('tracereplay.auto_trace.jobs', true)) { + if (! config('trace-replay.auto_trace.jobs', true)) { return; } @@ -40,7 +40,7 @@ public function onJobProcessed(JobProcessed $_event): void public function onJobFailed(JobFailed $event): void { - if (! config('tracereplay.auto_trace.jobs', true)) { + if (! config('trace-replay.auto_trace.jobs', true)) { return; } diff --git a/src/Services/AiPromptService.php b/src/Services/AiPromptService.php index 40ab598..75292c9 100644 --- a/src/Services/AiPromptService.php +++ b/src/Services/AiPromptService.php @@ -71,7 +71,7 @@ public function generateFixPrompt(Trace $trace): string $prompt .= "2. **Hypothesis:** State your best hypothesis about the underlying issue.\n"; $prompt .= "3. **Fix:** Provide a concrete, minimal code fix (PHP/Laravel) or configuration change.\n"; $prompt .= "4. **Prevention:** Suggest how to prevent this class of error in the future (tests, validation, guards).\n\n"; - $prompt .= "_Context generated by TraceReplay β€” https://github.com/iazaran/tracereplay_\n"; + $prompt .= "_Context generated by TraceReplay β€” https://github.com/iazaran/trace-replay_\n"; return $prompt; } @@ -82,8 +82,8 @@ public function generateFixPrompt(Trace $trace): string */ public function callOpenAI(string $prompt): ?string { - $apiKey = config('tracereplay.ai.openai_api_key'); - $model = config('tracereplay.ai.model', 'gpt-4o'); + $apiKey = config('trace-replay.ai.openai_api_key'); + $model = config('trace-replay.ai.model', 'gpt-4o'); if (! $apiKey) { return null; diff --git a/src/Services/NotificationService.php b/src/Services/NotificationService.php index e28ec9a..ff337a7 100644 --- a/src/Services/NotificationService.php +++ b/src/Services/NotificationService.php @@ -10,7 +10,7 @@ class NotificationService { public function notifyFailure(Trace $trace): void { - $channels = config('tracereplay.notifications.channels', []); + $channels = config('trace-replay.notifications.channels', []); foreach ($channels as $channel) { match ($channel) { @@ -23,13 +23,13 @@ public function notifyFailure(Trace $trace): void protected function sendMail(Trace $trace): void { - $to = config('tracereplay.notifications.mail.to'); + $to = config('trace-replay.notifications.mail.to'); if (! $to) { return; } $errorStep = $trace->error_step; - $dashboardUrl = rtrim(config('app.url', ''), '/').'/tracereplay/traces/'.$trace->id; + $dashboardUrl = rtrim(config('app.url', ''), '/').'/trace-replay/traces/'.$trace->id; $subject = "[TraceReplay] Trace Failed: {$trace->name}"; $body = $this->buildEmailBody($trace, $errorStep, $dashboardUrl); @@ -41,13 +41,13 @@ protected function sendMail(Trace $trace): void protected function sendSlack(Trace $trace): void { - $webhookUrl = config('tracereplay.notifications.slack.webhook_url'); + $webhookUrl = config('trace-replay.notifications.slack.webhook_url'); if (! $webhookUrl) { return; } $errorStep = $trace->error_step; - $dashboardUrl = rtrim(config('app.url', ''), '/').'/tracereplay/traces/'.$trace->id; + $dashboardUrl = rtrim(config('app.url', ''), '/').'/trace-replay/traces/'.$trace->id; $payload = [ 'text' => ':red_circle: *TraceReplay β€” Trace Failed*', diff --git a/src/Services/PayloadMasker.php b/src/Services/PayloadMasker.php index d2ccedb..70d2ead 100644 --- a/src/Services/PayloadMasker.php +++ b/src/Services/PayloadMasker.php @@ -11,7 +11,7 @@ public function __construct() { $this->fields = array_map( 'strtolower', - config('tracereplay.mask_fields', [ + config('trace-replay.mask_fields', [ 'password', 'password_confirmation', 'token', 'api_key', 'authorization', 'secret', 'credit_card', ]) diff --git a/src/Services/ReplayService.php b/src/Services/ReplayService.php index 9f6d9ac..dfbeb03 100644 --- a/src/Services/ReplayService.php +++ b/src/Services/ReplayService.php @@ -32,7 +32,7 @@ public function replay(Trace $trace, ?string $overrideUrl = null): array // Remove host headers so they don't interfere with the target unset($headers['host'], $headers['Host']); - $baseUrl = $overrideUrl ?? config('tracereplay.replay.default_base_url'); + $baseUrl = $overrideUrl ?? config('trace-replay.replay.default_base_url'); $targetUrl = rtrim($baseUrl, '/').'/'.ltrim($uri, '/'); // Symfony normalises all header names to lowercase, so 'Content-Type' never exists @@ -40,7 +40,7 @@ public function replay(Trace $trace, ?string $overrideUrl = null): array $isJson = str_contains($headers['content-type'][0] ?? '', 'json'); $response = Http::withHeaders($headers) - ->timeout((int) config('tracereplay.replay.timeout', 30)) + ->timeout((int) config('trace-replay.replay.timeout', 30)) ->withQueryParameters($query) ->send($method, $targetUrl, $isJson ? ['json' => $body] : ['form_params' => $body]); diff --git a/src/TraceReplayManager.php b/src/TraceReplayManager.php index be94c90..30278ce 100644 --- a/src/TraceReplayManager.php +++ b/src/TraceReplayManager.php @@ -36,7 +36,7 @@ public function __construct($app) public function start(string $name, array $tags = []): ?Trace { - if (! config('tracereplay.enabled', true)) { + if (! config('trace-replay.enabled', true)) { return null; } @@ -81,7 +81,7 @@ public function step(string $label, callable $callback, array $extra = []): mixe $start = microtime(true); $status = 'success'; $errorReason = null; - $trackDb = config('tracereplay.track_db_queries', true); + $trackDb = config('trace-replay.track_db_queries', true); // Use Laravel's built-in query log rather than DB::listen() to avoid // listener accumulation: each additional step() call would register @@ -232,7 +232,7 @@ public function end(string $status = 'success'): void ]); // Fire notification if configured and trace failed - if ($status === 'error' && config('tracereplay.notifications.on_failure', false)) { + if ($status === 'error' && config('trace-replay.notifications.on_failure', false)) { try { app(NotificationService::class)->notifyFailure($this->currentTrace->fresh(['steps'])); } catch (Throwable) { @@ -259,10 +259,10 @@ public function getCurrentTrace(): ?Trace protected function persistStep(TraceStep $step): void { try { - if (config('tracereplay.queue.enabled') && class_exists(PersistTraceStepJob::class)) { + if (config('trace-replay.queue.enabled') && class_exists(PersistTraceStepJob::class)) { dispatch(new PersistTraceStepJob($step->toArray())) - ->onConnection(config('tracereplay.queue.connection')) - ->onQueue(config('tracereplay.queue.queue')); + ->onConnection(config('trace-replay.queue.connection')) + ->onQueue(config('trace-replay.queue.queue')); } else { $step->save(); } @@ -273,7 +273,7 @@ protected function persistStep(TraceStep $step): void protected function determineProjectId(): ?string { - return config('tracereplay.project_id'); + return config('trace-replay.project_id'); } /** diff --git a/src/TraceReplayServiceProvider.php b/src/TraceReplayServiceProvider.php index ae01c85..f4e771d 100644 --- a/src/TraceReplayServiceProvider.php +++ b/src/TraceReplayServiceProvider.php @@ -23,9 +23,9 @@ class TraceReplayServiceProvider extends ServiceProvider { public function register(): void { - $this->mergeConfigFrom(__DIR__.'/../config/tracereplay.php', 'tracereplay'); + $this->mergeConfigFrom(__DIR__.'/../config/trace-replay.php', 'trace-replay'); - $this->app->singleton('tracereplay', fn ($app) => new TraceReplayManager($app)); + $this->app->singleton('trace-replay', fn ($app) => new TraceReplayManager($app)); $this->app->singleton(PayloadMasker::class); $this->app->singleton(AiPromptService::class); @@ -39,16 +39,16 @@ public function boot(): void { if ($this->app->runningInConsole()) { $this->publishes([ - __DIR__.'/../config/tracereplay.php' => config_path('tracereplay.php'), - ], 'tracereplay-config'); + __DIR__.'/../config/trace-replay.php' => config_path('trace-replay.php'), + ], 'trace-replay-config'); $this->publishes([ __DIR__.'/../database/migrations/' => database_path('migrations'), - ], 'tracereplay-migrations'); + ], 'trace-replay-migrations'); $this->publishes([ - __DIR__.'/../resources/views' => resource_path('views/vendor/tracereplay'), - ], 'tracereplay-views'); + __DIR__.'/../resources/views' => resource_path('views/vendor/trace-replay'), + ], 'trace-replay-views'); $this->commands([ PruneTracesCommand::class, @@ -57,8 +57,8 @@ public function boot(): void } $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); - $this->loadViewsFrom(__DIR__.'/../resources/views', 'tracereplay'); - $this->loadViewComponentsAs('tracereplay', [ + $this->loadViewsFrom(__DIR__.'/../resources/views', 'trace-replay'); + $this->loadViewComponentsAs('trace-replay', [ TraceBar::class, ]); @@ -66,14 +66,14 @@ public function boot(): void $this->loadRoutesFrom(__DIR__.'/../routes/api.php'); // Auto-trace queue jobs - if (config('tracereplay.auto_trace.jobs', true)) { + if (config('trace-replay.auto_trace.jobs', true)) { Event::listen(JobProcessing::class, fn (JobProcessing $e) => $this->app->make(JobTraceListener::class)->onJobProcessing($e)); Event::listen(JobProcessed::class, fn (JobProcessed $e) => $this->app->make(JobTraceListener::class)->onJobProcessed($e)); Event::listen(JobFailed::class, fn (JobFailed $e) => $this->app->make(JobTraceListener::class)->onJobFailed($e)); } // Auto-trace artisan commands - if (config('tracereplay.auto_trace.commands', false)) { + if (config('trace-replay.auto_trace.commands', false)) { Event::listen(CommandStarting::class, fn (CommandStarting $e) => $this->app->make(CommandTraceListener::class)->onCommandStarting($e)); Event::listen(CommandFinished::class, fn (CommandFinished $e) => $this->app->make(CommandTraceListener::class)->onCommandFinished($e)); } diff --git a/src/View/Components/TraceBar.php b/src/View/Components/TraceBar.php index 0e53c73..702d141 100644 --- a/src/View/Components/TraceBar.php +++ b/src/View/Components/TraceBar.php @@ -13,13 +13,13 @@ public function __construct( public function render() { - if (! $this->show || ! config('tracereplay.enabled', true)) { + if (! $this->show || ! config('trace-replay.enabled', true)) { return ''; } $trace = TraceReplay::getCurrentTrace(); - return view('tracereplay::components.trace-bar', [ + return view('trace-replay::components.trace-bar', [ 'trace' => $trace, ]); } diff --git a/tests/Feature/TraceReplayTest.php b/tests/Feature/TraceReplayTest.php index b71cfc5..7c95deb 100644 --- a/tests/Feature/TraceReplayTest.php +++ b/tests/Feature/TraceReplayTest.php @@ -32,7 +32,7 @@ }); it('returns null from start() when tracing is disabled', function () { - config(['tracereplay.enabled' => false]); + config(['trace-replay.enabled' => false]); $trace = TraceReplay::start('Disabled Trace'); @@ -188,7 +188,7 @@ // ── PayloadMasker ───────────────────────────────────────────────────────────── it('PayloadMasker masks configured sensitive fields', function () { - config(['tracereplay.mask_fields' => ['password', 'token']]); + config(['trace-replay.mask_fields' => ['password', 'token']]); $masker = new PayloadMasker; $result = $masker->mask([ @@ -236,83 +236,83 @@ // ── Artisan Commands ────────────────────────────────────────────────────────── -it('tracereplay:prune deletes old traces', function () { +it('trace-replay:prune deletes old traces', function () { Trace::factory()->create(['started_at' => now()->subDays(60)]); Trace::factory()->create(['started_at' => now()->subDays(60)]); Trace::factory()->create(['started_at' => now()->subDays(1)]); - $this->artisan('tracereplay:prune', ['--days' => 30]) + $this->artisan('trace-replay:prune', ['--days' => 30]) ->expectsOutput('Deleted 2 trace(s) older than 30 day(s).') ->assertExitCode(0); expect(Trace::count())->toBe(1); }); -it('tracereplay:prune dry-run does not delete traces', function () { +it('trace-replay:prune dry-run does not delete traces', function () { Trace::factory()->create(['started_at' => now()->subDays(60)]); - $this->artisan('tracereplay:prune', ['--days' => 30, '--dry-run' => true]) + $this->artisan('trace-replay:prune', ['--days' => 30, '--dry-run' => true]) ->assertExitCode(0); expect(Trace::count())->toBe(1); }); -it('tracereplay:export outputs JSON for a trace', function () { +it('trace-replay:export outputs JSON for a trace', function () { $trace = Trace::factory()->create(['name' => 'Exportable Trace']); - $this->artisan('tracereplay:export', ['id' => $trace->id, '--format' => 'json']) + $this->artisan('trace-replay:export', ['id' => $trace->id, '--format' => 'json']) ->assertExitCode(0); }); // ── Export CSV ──────────────────────────────────────────────────────────────── -it('tracereplay:export outputs CSV for a trace', function () { +it('trace-replay:export outputs CSV for a trace', function () { $trace = Trace::factory()->create(['name' => 'CSV, with "commas"']); - $this->artisan('tracereplay:export', ['id' => $trace->id, '--format' => 'csv']) + $this->artisan('trace-replay:export', ['id' => $trace->id, '--format' => 'csv']) ->assertExitCode(0); }); -it('tracereplay:export rejects unsupported format', function () { +it('trace-replay:export rejects unsupported format', function () { $trace = Trace::factory()->create(); - $this->artisan('tracereplay:export', ['id' => $trace->id, '--format' => 'xml']) + $this->artisan('trace-replay:export', ['id' => $trace->id, '--format' => 'xml']) ->assertExitCode(1); }); -it('tracereplay:export returns failure for nonexistent trace', function () { - $this->artisan('tracereplay:export', ['id' => 'nonexistent-id']) +it('trace-replay:export returns failure for nonexistent trace', function () { + $this->artisan('trace-replay:export', ['id' => 'nonexistent-id']) ->assertExitCode(1); }); -it('tracereplay:export filters by status', function () { +it('trace-replay:export filters by status', function () { Trace::factory()->create(['status' => 'success']); Trace::factory()->create(['status' => 'error']); - $this->artisan('tracereplay:export', ['--status' => 'error', '--format' => 'json']) + $this->artisan('trace-replay:export', ['--status' => 'error', '--format' => 'json']) ->assertExitCode(0); }); // ── Prune Command Extra ────────────────────────────────────────────────────── -it('tracereplay:prune rejects zero days', function () { - $this->artisan('tracereplay:prune', ['--days' => 0]) +it('trace-replay:prune rejects zero days', function () { + $this->artisan('trace-replay:prune', ['--days' => 0]) ->assertExitCode(1); }); -it('tracereplay:prune filters by status', function () { +it('trace-replay:prune filters by status', function () { Trace::factory()->create(['started_at' => now()->subDays(60), 'status' => 'success']); Trace::factory()->create(['started_at' => now()->subDays(60), 'status' => 'error']); - $this->artisan('tracereplay:prune', ['--days' => 30, '--status' => 'success']) + $this->artisan('trace-replay:prune', ['--days' => 30, '--status' => 'success']) ->assertExitCode(0); expect(Trace::count())->toBe(1) ->and(Trace::first()->status)->toBe('error'); }); -it('tracereplay:prune shows no-op when no traces match', function () { - $this->artisan('tracereplay:prune', ['--days' => 30]) +it('trace-replay:prune shows no-op when no traces match', function () { + $this->artisan('trace-replay:prune', ['--days' => 30]) ->expectsOutput('No traces found matching the criteria.') ->assertExitCode(0); }); @@ -322,7 +322,7 @@ it('dashboard index page loads', function () { Trace::factory()->count(3)->create(); - $response = $this->get('/tracereplay'); + $response = $this->get('/trace-replay'); $response->assertOk(); $response->assertSee('Traces'); @@ -332,7 +332,7 @@ Trace::factory()->create(['status' => 'error', 'name' => 'Error Trace']); Trace::factory()->create(['status' => 'success', 'name' => 'Good Trace']); - $response = $this->get('/tracereplay?status=error'); + $response = $this->get('/trace-replay?status=error'); $response->assertOk(); $response->assertSee('Error Trace'); @@ -342,7 +342,7 @@ Trace::factory()->create(['name' => 'Login Flow']); Trace::factory()->create(['name' => 'Checkout Process']); - $response = $this->get('/tracereplay?search=Login'); + $response = $this->get('/trace-replay?search=Login'); $response->assertOk(); $response->assertSee('Login Flow'); @@ -351,14 +351,14 @@ it('dashboard show page loads for a valid trace', function () { $trace = Trace::factory()->create(['name' => 'Detail Test']); - $response = $this->get("/tracereplay/traces/{$trace->id}"); + $response = $this->get("/trace-replay/traces/{$trace->id}"); $response->assertOk(); $response->assertSee('Detail Test'); }); it('dashboard show returns 404 for nonexistent trace', function () { - $response = $this->get('/tracereplay/traces/nonexistent-uuid'); + $response = $this->get('/trace-replay/traces/nonexistent-uuid'); $response->assertNotFound(); }); @@ -367,7 +367,7 @@ Trace::factory()->create(['status' => 'success', 'duration_ms' => 100]); Trace::factory()->create(['status' => 'error', 'duration_ms' => 200]); - $response = $this->getJson('/tracereplay/stats'); + $response = $this->getJson('/trace-replay/stats'); $response->assertOk(); $response->assertJsonStructure(['total', 'success', 'failed', 'today', 'failure_rate', 'avg_duration', 'slowest']); @@ -377,7 +377,7 @@ it('dashboard export downloads JSON file', function () { $trace = Trace::factory()->create(['name' => 'Export Me']); - $response = $this->get("/tracereplay/traces/{$trace->id}/export"); + $response = $this->get("/trace-replay/traces/{$trace->id}/export"); $response->assertOk(); $response->assertHeader('Content-Type', 'application/json'); @@ -389,7 +389,7 @@ it('MCP list traces returns paginated results', function () { Trace::factory()->count(3)->create(); - $response = $this->getJson('/api/tracereplay/mcp/traces'); + $response = $this->getJson('/api/trace-replay/mcp/traces'); $response->assertOk(); $response->assertJsonPath('status', 'success'); @@ -399,7 +399,7 @@ Trace::factory()->create(['status' => 'success']); Trace::factory()->create(['status' => 'error']); - $response = $this->getJson('/api/tracereplay/mcp/traces?filter_by_error=1'); + $response = $this->getJson('/api/trace-replay/mcp/traces?filter_by_error=1'); $response->assertOk(); $response->assertJsonPath('data.total', 1); @@ -408,7 +408,7 @@ it('MCP get context returns trace details', function () { $trace = Trace::factory()->create(['status' => 'success']); - $response = $this->getJson("/api/tracereplay/mcp/traces/{$trace->id}/context"); + $response = $this->getJson("/api/trace-replay/mcp/traces/{$trace->id}/context"); $response->assertOk(); $response->assertJsonPath('status', 'success'); @@ -425,7 +425,7 @@ $trace = Trace::latest()->first(); - $response = $this->getJson("/api/tracereplay/mcp/traces/{$trace->id}/fix-prompt"); + $response = $this->getJson("/api/trace-replay/mcp/traces/{$trace->id}/fix-prompt"); $response->assertOk(); $response->assertJsonPath('status', 'success'); @@ -434,7 +434,7 @@ it('MCP RPC list_traces method works', function () { Trace::factory()->count(2)->create(); - $response = $this->postJson('/api/tracereplay/mcp', [ + $response = $this->postJson('/api/trace-replay/mcp', [ 'method' => 'list_traces', 'params' => [], 'id' => 1, @@ -446,7 +446,7 @@ }); it('MCP RPC returns error for unknown method', function () { - $response = $this->postJson('/api/tracereplay/mcp', [ + $response = $this->postJson('/api/trace-replay/mcp', [ 'method' => 'unknown_method', 'params' => [], 'id' => 42, @@ -461,7 +461,7 @@ it('MCP RPC get_trace_context method works', function () { $trace = Trace::factory()->create(['status' => 'error']); - $response = $this->postJson('/api/tracereplay/mcp', [ + $response = $this->postJson('/api/trace-replay/mcp', [ 'method' => 'get_trace_context', 'params' => ['trace_id' => $trace->id], 'id' => 2, @@ -481,7 +481,7 @@ $trace = Trace::latest()->first(); - $response = $this->postJson('/api/tracereplay/mcp', [ + $response = $this->postJson('/api/trace-replay/mcp', [ 'method' => 'generate_fix_prompt', 'params' => ['trace_id' => $trace->id], 'id' => 3, @@ -493,20 +493,20 @@ // ── TraceMiddleware ────────────────────────────────────────────────────────── -it('TraceMiddleware skips tracereplay dashboard routes', function () { +it('TraceMiddleware skips trace-replay dashboard routes', function () { // Dashboard route should not create a trace - $this->get('/tracereplay'); + $this->get('/trace-replay'); - // The middleware skips routes starting with 'tracereplay' + // The middleware skips routes starting with 'trace-replay' // Traces created should only be for the dashboard itself, not from middleware // Since dashboard creates no traces itself, count should be 0 from middleware - expect(Trace::where('name', 'like', 'HTTP GET /tracereplay%')->count())->toBe(0); + expect(Trace::where('name', 'like', 'HTTP GET /trace-replay%')->count())->toBe(0); }); it('TraceMiddleware respects disabled config', function () { - config(['tracereplay.enabled' => false]); + config(['trace-replay.enabled' => false]); - $this->get('/tracereplay'); + $this->get('/trace-replay'); expect(Trace::count())->toBe(0); }); @@ -514,17 +514,17 @@ // ── Auth Middleware ────────────────────────────────────────────────────────── it('TraceReplayAuthMiddleware allows access when no IPs configured', function () { - config(['tracereplay.allowed_ips' => []]); + config(['trace-replay.allowed_ips' => []]); - $response = $this->get('/tracereplay'); + $response = $this->get('/trace-replay'); $response->assertOk(); }); it('TraceReplayAuthMiddleware blocks access from unauthorized IPs', function () { - config(['tracereplay.allowed_ips' => ['192.168.1.100']]); + config(['trace-replay.allowed_ips' => ['192.168.1.100']]); - $response = $this->get('/tracereplay'); + $response = $this->get('/trace-replay'); $response->assertForbidden(); }); @@ -699,7 +699,7 @@ }); it('PayloadMasker is case-insensitive', function () { - config(['tracereplay.mask_fields' => ['Authorization']]); + config(['trace-replay.mask_fields' => ['Authorization']]); $masker = new PayloadMasker; $result = $masker->mask(['AUTHORIZATION' => 'Bearer xyz', 'name' => 'test']); @@ -721,9 +721,9 @@ }); config([ - 'tracereplay.notifications.on_failure' => true, - 'tracereplay.notifications.channels' => ['mail'], - 'tracereplay.notifications.mail.to' => 'test@example.com', + 'trace-replay.notifications.on_failure' => true, + 'trace-replay.notifications.channels' => ['mail'], + 'trace-replay.notifications.mail.to' => 'test@example.com', ]); $trace = Trace::factory()->create(['status' => 'error', 'name' => 'Notified Trace']); @@ -736,8 +736,8 @@ Mail::fake(); config([ - 'tracereplay.notifications.channels' => ['mail'], - 'tracereplay.notifications.mail.to' => null, + 'trace-replay.notifications.channels' => ['mail'], + 'trace-replay.notifications.mail.to' => null, ]); $trace = Trace::factory()->create(['status' => 'error']); @@ -750,8 +750,8 @@ Http::fake(['*' => Http::response([], 200)]); config([ - 'tracereplay.notifications.channels' => ['slack'], - 'tracereplay.notifications.slack.webhook_url' => 'https://hooks.slack.test/webhook', + 'trace-replay.notifications.channels' => ['slack'], + 'trace-replay.notifications.slack.webhook_url' => 'https://hooks.slack.test/webhook', ]); $trace = Trace::factory()->create(['status' => 'error', 'name' => 'Slack Trace']); @@ -764,8 +764,8 @@ Http::fake(); config([ - 'tracereplay.notifications.channels' => ['slack'], - 'tracereplay.notifications.slack.webhook_url' => null, + 'trace-replay.notifications.channels' => ['slack'], + 'trace-replay.notifications.slack.webhook_url' => null, ]); $trace = Trace::factory()->create(['status' => 'error']); @@ -777,7 +777,7 @@ // ── Step DB query tracking ─────────────────────────────────────────────────── it('step records db query count when tracking is enabled', function () { - config(['tracereplay.track_db_queries' => true]); + config(['trace-replay.track_db_queries' => true]); TraceReplay::start('DB Tracking'); TraceReplay::step('Query Step', function () { @@ -791,7 +791,7 @@ }); it('step does not track queries when disabled', function () { - config(['tracereplay.track_db_queries' => false]); + config(['trace-replay.track_db_queries' => false]); TraceReplay::start('No DB Tracking'); TraceReplay::step('Silent Step', function () { @@ -831,7 +831,7 @@ it('context() returns the manager for chaining', function () { TraceReplay::start('Chain Test'); - $result = app('tracereplay')->context(['x' => 1]); + $result = app('trace-replay')->context(['x' => 1]); expect($result)->toBeInstanceOf(TraceReplayManager::class); }); @@ -853,10 +853,10 @@ // ── Export Command β€” invalid directory ────────────────────────────────────── -it('tracereplay:export fails on invalid output directory', function () { +it('trace-replay:export fails on invalid output directory', function () { $trace = Trace::factory()->create(); - $this->artisan('tracereplay:export', [ + $this->artisan('trace-replay:export', [ 'id' => $trace->id, '--format' => 'json', '--output' => '/nonexistent/dir/trace.json', @@ -865,8 +865,8 @@ // ── Prune Command β€” invalid status ───────────────────────────────────────── -it('tracereplay:prune rejects invalid status', function () { - $this->artisan('tracereplay:prune', ['--days' => 30, '--status' => 'invalid']) +it('trace-replay:prune rejects invalid status', function () { + $this->artisan('trace-replay:prune', ['--days' => 30, '--status' => 'invalid']) ->assertExitCode(1); }); @@ -875,7 +875,7 @@ it('dashboard index ignores invalid status filter', function () { Trace::factory()->create(['status' => 'success', 'name' => 'Valid Trace']); - $response = $this->get('/tracereplay?status=nonexistent'); + $response = $this->get('/trace-replay?status=nonexistent'); $response->assertOk(); // Invalid status is ignored β€” all traces should be shown @@ -887,7 +887,7 @@ it('dashboard replay endpoint returns error for trace without request payload', function () { $trace = Trace::factory()->create(); - $response = $this->postJson("/tracereplay/traces/{$trace->id}/replay"); + $response = $this->postJson("/trace-replay/traces/{$trace->id}/replay"); $response->assertStatus(400); $response->assertJsonPath('status', 'error'); @@ -906,7 +906,7 @@ $trace = Trace::latest()->first(); // No OpenAI key configured, so ai_response will be null - $response = $this->postJson("/tracereplay/traces/{$trace->id}/ai-prompt"); + $response = $this->postJson("/trace-replay/traces/{$trace->id}/ai-prompt"); $response->assertOk(); $response->assertJsonPath('status', 'success'); @@ -916,7 +916,7 @@ // ── AiPromptService::callOpenAI ──────────────────────────────────────────── it('AiPromptService callOpenAI returns null when no key configured', function () { - config(['tracereplay.ai.openai_api_key' => null]); + config(['trace-replay.ai.openai_api_key' => null]); $result = app(AiPromptService::class)->callOpenAI('test prompt'); @@ -924,7 +924,7 @@ }); it('AiPromptService callOpenAI returns response when key configured', function () { - config(['tracereplay.ai.openai_api_key' => 'test-key']); + config(['trace-replay.ai.openai_api_key' => 'test-key']); Http::fake([ 'api.openai.com/*' => Http::response([ @@ -938,7 +938,7 @@ }); it('AiPromptService callOpenAI returns null on API failure', function () { - config(['tracereplay.ai.openai_api_key' => 'test-key']); + config(['trace-replay.ai.openai_api_key' => 'test-key']); Http::fake([ 'api.openai.com/*' => Http::response([], 500), @@ -952,7 +952,7 @@ // ── TraceBar Component ────────────────────────────────────────────────────── it('TraceBar renders empty when disabled', function () { - config(['tracereplay.enabled' => false]); + config(['trace-replay.enabled' => false]); $component = new TraceBar; $result = $component->render(); @@ -969,11 +969,11 @@ // ── Export Command β€” output to file ───────────────────────────────────────── -it('tracereplay:export writes to output file', function () { +it('trace-replay:export writes to output file', function () { $trace = Trace::factory()->create(['name' => 'File Export']); $tmpFile = tempnam(sys_get_temp_dir(), 'tr_export_'); - $this->artisan('tracereplay:export', [ + $this->artisan('trace-replay:export', [ 'id' => $trace->id, '--format' => 'json', '--output' => $tmpFile, @@ -987,8 +987,8 @@ // ── ExportTraceCommand β€” status validation ───────────────────────────────── -it('tracereplay:export rejects invalid status', function () { - $this->artisan('tracereplay:export', ['--status' => 'invalid', '--format' => 'json']) +it('trace-replay:export rejects invalid status', function () { + $this->artisan('trace-replay:export', ['--status' => 'invalid', '--format' => 'json']) ->assertExitCode(1); }); @@ -1028,8 +1028,8 @@ }); config([ - 'tracereplay.notifications.channels' => ['mail'], - 'tracereplay.notifications.mail.to' => 'test@example.com', + 'trace-replay.notifications.channels' => ['mail'], + 'trace-replay.notifications.mail.to' => 'test@example.com', ]); $trace = Trace::factory()->create([ @@ -1045,8 +1045,8 @@ Http::fake(['*' => Http::response([], 200)]); config([ - 'tracereplay.notifications.channels' => ['slack'], - 'tracereplay.notifications.slack.webhook_url' => 'https://hooks.slack.test/webhook', + 'trace-replay.notifications.channels' => ['slack'], + 'trace-replay.notifications.slack.webhook_url' => 'https://hooks.slack.test/webhook', ]); $trace = Trace::factory()->create([ @@ -1065,7 +1065,7 @@ it('MCP RPC trigger_replay returns error for trace without payload', function () { $trace = Trace::factory()->create(); - $response = $this->postJson('/api/tracereplay/mcp', [ + $response = $this->postJson('/api/trace-replay/mcp', [ 'method' => 'trigger_replay', 'params' => ['trace_id' => $trace->id], 'id' => 10, diff --git a/tests/TestCase.php b/tests/TestCase.php index bbef4a5..363abc7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -31,7 +31,7 @@ protected function getEnvironmentSetUp($app) 'prefix' => '', ]); - $app['config']->set('tracereplay.enabled', true); + $app['config']->set('trace-replay.enabled', true); $app['config']->set('app.key', 'base64:'.base64_encode(str_repeat('a', 32))); } }
MethodParamsReturns
- View β†’ - ↓