diff --git a/README.md b/README.md
index dacb0c7..9762720 100644
--- a/README.md
+++ b/README.md
@@ -1,289 +1,331 @@
-# Perfbase for Laravel
-
-[](https://github.com/perfbaseorg/laravel/blob/main/LICENSE.txt)
-[](https://packagist.org/packages/perfbase/laravel)
-[](https://github.com/perfbaseorg/laravel/actions/workflows/ci.yml)
-
-Seamless Laravel integration for Perfbase - a comprehensive Application Performance Monitoring (APM) solution that provides real-time insights into your Laravel application's performance, database queries, HTTP requests, queue jobs, and more.
-
-## Features
-
-- 🚀 **Automatic Profiling** - HTTP requests, console commands, and queue jobs
-- 📊 **Multi-span Tracing** - Track nested operations within requests
-- 🔍 **Database Query Monitoring** - Monitor all database operations with timing
-- 🌐 **HTTP Request Tracking** - Monitor outbound API calls and their performance
-- ⚡ **Queue Job Profiling** - Track background job performance and failures
-- 🏷️ **Custom Attributes** - Add contextual metadata to traces
-- 🎯 **Smart Sampling** - Control data collection with configurable sample rates
-- 💾 **Flexible Data Storage** - Sync immediately or buffer locally (file/database)
-- 🔧 **Granular Control** - Include/exclude specific routes, commands, or jobs
-- 🛡️ **Multi-tenant Support** - Organization and project-level data isolation
+
+
+
+
+
+
+Perfbase for Laravel
+
+ Laravel integration for Perfbase.
+
+
+
+
+
+
+
+
+
+
+This package is a thin adapter over [`perfbase/php-sdk`](https://packagist.org/packages/perfbase/php-sdk). It wires Laravel request, console, and queue lifecycles into the SDK and leaves trace transport, submission, and extension handling to the shared SDK.
+
+## What it profiles
+
+- HTTP requests when the Perfbase middleware is installed
+- Artisan commands through Laravel console events
+- Queue jobs through Laravel queue events
+- Manual custom spans through the `Perfbase` facade or injected SDK client
## Requirements
-- **PHP**: 7.4 to 8.4
-- **Laravel**: 8.0, 9.0, 10.0, 11.0, or 12.0
-- **Extensions**:
- - `ext-json` (usually enabled by default)
- - `ext-zlib` (usually enabled by default)
- - `ext-perfbase` (Perfbase PHP extension)
-- **Dependencies**: Guzzle HTTP 7.0+
+- PHP `7.4` to `8.5`
+- Laravel `8.x`, `9.x`, `10.x`, `11.x`, or `12.x`
+- `ext-json`
+- `ext-zlib`
+- `ext-perfbase`
## Installation
-### 1. Install the Package
+Install the package from Packagist:
```bash
-composer require perfbase/laravel
+composer require perfbase/laravel:^1.0
```
-### 2. Install the Perfbase PHP Extension
-
-The `ext-perfbase` PHP extension is required. Install it using:
+Install the native Perfbase extension if it is not already available:
```bash
bash -c "$(curl -fsSL https://cdn.perfbase.com/install.sh)"
```
-**Important**: Restart your web server after installation.
+Restart PHP-FPM, Octane workers, Horizon workers, or your web server after installing the extension.
-### 3. Publish Configuration
+Publish the config file:
```bash
php artisan vendor:publish --tag="perfbase-config"
```
-This creates `config/perfbase.php` with all available options.
-
-### 4. Configure Environment
-
-Add to your `.env` file:
+Add the minimum environment variables:
```env
PERFBASE_ENABLED=true
PERFBASE_API_KEY=your_api_key_here
PERFBASE_SAMPLE_RATE=0.1
-PERFBASE_SENDING_MODE=sync
```
-### 5. Add Middleware (Optional but Recommended)
+### HTTP middleware
+
+HTTP profiling is enabled only when the middleware is present.
-For HTTP request profiling, add the middleware to your HTTP kernel:
+For Laravel 8 to 10, add it to `app/Http/Kernel.php`:
```php
-// app/Http/Kernel.php
protected $middleware = [
- // ... other middleware
+ // ...
\Perfbase\Laravel\Middleware\PerfbaseMiddleware::class,
];
```
-Or apply to specific route groups:
+Or attach it to a middleware group:
```php
-// app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
- // ... other middleware
+ // ...
\Perfbase\Laravel\Middleware\PerfbaseMiddleware::class,
],
];
```
-## Configuration
+For Laravel 11+, register it in `bootstrap/app.php`:
-### Basic Configuration
+```php
+use Illuminate\Foundation\Application;
+use Illuminate\Foundation\Configuration\Middleware;
+use Perfbase\Laravel\Middleware\PerfbaseMiddleware;
+
+return Application::configure(dirname(__DIR__))
+ ->withMiddleware(function (Middleware $middleware) {
+ $middleware->append(PerfbaseMiddleware::class);
+ })
+ ->create();
+```
+
+Console and queue profiling do not need middleware. They are wired through the package service provider.
-The package auto-registers and provides several configuration options:
+## Configuration
+
+Published config lives at `config/perfbase.php`.
```php
-// config/perfbase.php
return [
'enabled' => env('PERFBASE_ENABLED', false),
+ 'debug' => env('PERFBASE_DEBUG', false),
+ 'log_errors' => env('PERFBASE_LOG_ERRORS', true),
'api_key' => env('PERFBASE_API_KEY'),
'sample_rate' => env('PERFBASE_SAMPLE_RATE', 0.1),
-
- 'sending' => [
- 'mode' => env('PERFBASE_SENDING_MODE', 'sync'),
- 'timeout' => env('PERFBASE_TIMEOUT', 5),
- 'proxy' => env('PERFBASE_PROXY'),
- ],
-
+ 'timeout' => env('PERFBASE_TIMEOUT', 5),
+ 'proxy' => env('PERFBASE_PROXY'),
'flags' => env('PERFBASE_FLAGS', \Perfbase\SDK\FeatureFlags::DefaultFlags),
- // ... more options
+ 'include' => [
+ 'http' => ['.*'],
+ 'console' => ['.*'],
+ 'queue' => ['.*'],
+ ],
+ 'exclude' => [
+ 'http' => [],
+ 'console' => ['queue:work'],
+ 'queue' => [],
+ ],
];
```
-### Environment Variables
-
-| Variable | Default | Description |
-|----------|---------|-------------|
-| `PERFBASE_ENABLED` | `false` | Enable/disable profiling |
-| `PERFBASE_API_KEY` | `null` | Your Perfbase API key (required) |
-| `PERFBASE_SAMPLE_RATE` | `0.1` | Sampling rate (0.0 to 1.0) |
-| `PERFBASE_SENDING_MODE` | `sync` | Data sending mode (`sync`, `file`, `database`) |
-| `PERFBASE_TIMEOUT` | `5` | API request timeout in seconds |
-| `PERFBASE_PROXY` | `null` | HTTP proxy URL |
-| `PERFBASE_FLAGS` | Default flags | Profiling feature flags |
-
-### Sending Modes
-
-#### Sync Mode (Default)
-Data is sent immediately to Perfbase:
-```env
-PERFBASE_SENDING_MODE=sync
-```
-
-#### File Buffering
-Data is stored in local files and sent later:
-```env
-PERFBASE_SENDING_MODE=file
-```
-
-#### Database Buffering
-Data is cached in your database and sent later:
-```env
-PERFBASE_SENDING_MODE=database
-```
+### Environment variables
-### Profiling Features Control
+| Variable | Default | Purpose |
+| --- | --- | --- |
+| `PERFBASE_ENABLED` | `false` | Global on/off switch |
+| `PERFBASE_API_KEY` | `null` | Perfbase API key |
+| `PERFBASE_SAMPLE_RATE` | `0.1` | Sampling rate from `0.0` to `1.0` |
+| `PERFBASE_DEBUG` | `false` | Re-throw profiling exceptions |
+| `PERFBASE_LOG_ERRORS` | `true` | Log profiling failures when debug is off |
+| `PERFBASE_TIMEOUT` | `5` | Trace submission timeout in seconds |
+| `PERFBASE_PROXY` | `null` | Optional outbound proxy |
+| `PERFBASE_FLAGS` | `FeatureFlags::DefaultFlags` | Perfbase extension feature flags |
-Control which profiling features are enabled:
+### Feature flags
```php
use Perfbase\SDK\FeatureFlags;
-// In config/perfbase.php
-'flags' => FeatureFlags::DefaultFlags, // Recommended for most apps
-'flags' => FeatureFlags::AllFlags, // All available features
-'flags' => FeatureFlags::TrackCpuTime | FeatureFlags::TrackPdo, // Custom combination
+'flags' => FeatureFlags::DefaultFlags;
+'flags' => FeatureFlags::AllFlags;
+'flags' => FeatureFlags::TrackCpuTime | FeatureFlags::TrackPdo;
```
-Available flags:
-- `UseCoarseClock` - Faster timing (reduced overhead)
-- `TrackCpuTime` - Monitor CPU time usage
-- `TrackMemoryAllocation` - Track memory allocation patterns
-- `TrackPdo` - Monitor database queries
-- `TrackHttp` - Track outbound HTTP requests
-- `TrackCaches` - Monitor cache operations
-- `TrackMongodb` - Track MongoDB operations
-- `TrackElasticsearch` - Monitor Elasticsearch queries
-- `TrackQueues` - Track queue/background jobs
-- `TrackAwsSdk` - Monitor AWS SDK operations
-- `TrackFileOperations` - Track file I/O operations
+Common flags:
+
+- `UseCoarseClock`
+- `TrackCpuTime`
+- `TrackMemoryAllocation`
+- `TrackPdo`
+- `TrackHttp`
+- `TrackCaches`
+- `TrackMongodb`
+- `TrackElasticsearch`
+- `TrackQueues`
+- `TrackAwsSdk`
+- `TrackFileOperations`
+- `TrackFileCompilation`
+- `TrackFileDefinitions`
+- `TrackExceptions`
-### Include/Exclude Filters
+### Include and exclude filters
-Control which routes, commands, and jobs are profiled:
+Filters are split by context: `http`, `console`, and `queue`.
```php
-// config/perfbase.php
'include' => [
- 'http' => [
- 'api/*',
- 'admin/*'
- ],
- 'console' => [
- 'app:*',
- 'queue:*'
- ],
- 'queue' => [
- 'App\\Jobs\\*'
- ]
+ 'http' => ['GET /api/*', 'POST /checkout'],
+ 'console' => ['migrate*', 'app:*'],
+ 'queue' => ['App\\Jobs\\Important*'],
],
'exclude' => [
- 'http' => [
- 'health-check',
- '_debugbar/*'
- ],
- 'console' => [
- 'horizon:*',
- 'telescope:*'
- ],
- 'queue' => [
- 'App\\Jobs\\DebugJob'
- ]
-]
+ 'http' => ['GET /health*', '_debugbar/*'],
+ 'console' => ['queue:work', 'horizon:*'],
+ 'queue' => ['App\\Jobs\\NoisyDebugJob'],
+],
```
-## Usage
+Supported filter styles:
+
+- Wildcards like `GET /api/*`
+- Regex patterns like `/^POST \/checkout/`
+- Command patterns like `queue:*`
+- Job class patterns like `App\\Jobs\\*`
+- Controller or action strings matched through Laravel's string matcher
+
+## How it behaves
+
+### HTTP requests
+
+`PerfbaseMiddleware` creates an `HttpTraceLifecycle` for the current request.
-### Automatic Profiling
+Recorded attributes include:
-Once configured, Perfbase automatically profiles:
+- `source=http`
+- `action`
+- `http_method`
+- `http_url`
+- `http_status_code`
+- `user_ip`
+- `user_agent`
+- `user_id` when available
+- `environment`
+- `app_version`
+- `hostname`
+- `php_version`
-- **HTTP Requests** (when middleware is added)
-- **Console Commands** (all artisan commands)
-- **Queue Jobs** (all queued jobs)
+### Console commands
-### Manual Profiling
+The service provider listens to Laravel console events and creates a `ConsoleTraceLifecycle`.
-Use the facade for custom profiling:
+Recorded attributes include:
+
+- `source=console`
+- `action`
+- `exit_code`
+- `exception` when present
+- `environment`
+- `app_version`
+- `hostname`
+- `php_version`
+
+### Queue jobs
+
+The service provider listens to queue worker events and creates a `QueueTraceLifecycle`.
+
+Recorded attributes include:
+
+- `source=queue`
+- `action`
+- `queue`
+- `connection`
+- `exception` when present
+- `environment`
+- `app_version`
+- `hostname`
+- `php_version`
+
+## Manual spans
+
+Use the facade when you want custom spans inside your own application code:
```php
use Perfbase\Laravel\Facades\Perfbase;
-// Start a custom span
Perfbase::startTraceSpan('custom-operation', [
'operation_type' => 'data_processing',
- 'record_count' => '1000'
+ 'record_count' => '1000',
]);
-// Add attributes during execution
Perfbase::setAttribute('processing_method', 'batch');
-Perfbase::setAttribute('memory_usage', memory_get_usage());
+Perfbase::setAttribute('memory_usage', (string) memory_get_usage());
try {
- // Your custom logic here
processLargeDataset();
-
Perfbase::setAttribute('status', 'success');
-} catch (Exception $e) {
+} catch (\Exception $e) {
Perfbase::setAttribute('status', 'error');
Perfbase::setAttribute('error_message', $e->getMessage());
+ throw $e;
} finally {
- // Always stop the span
Perfbase::stopTraceSpan('custom-operation');
}
-// Submit the trace data
-Perfbase::submitTrace();
+$result = Perfbase::submitTrace();
+
+if (!$result->isSuccess()) {
+ logger()->warning('Perfbase trace submission failed', [
+ 'status' => $result->getStatus(),
+ 'message' => $result->getMessage(),
+ 'status_code' => $result->getStatusCode(),
+ ]);
+}
```
-### Service Injection
+Note that Perfbase trace attributes are string values. Cast integers and booleans before passing them to `setAttribute()`.
+
+## Dependency injection
-Use dependency injection in your services:
+You can inject the SDK client directly:
```php
use Perfbase\SDK\Perfbase;
class DataProcessingService
{
- public function __construct(private Perfbase $perfbase)
+ /** @var Perfbase */
+ private $perfbase;
+
+ public function __construct(Perfbase $perfbase)
{
+ $this->perfbase = $perfbase;
}
-
+
public function processData(array $data): array
{
$this->perfbase->startTraceSpan('data-processing', [
- 'record_count' => count($data),
- 'data_type' => 'user_records'
+ 'record_count' => (string) count($data),
+ 'data_type' => 'user_records',
]);
-
- $result = $this->performProcessing($data);
-
- $this->perfbase->setAttribute('processed_count', count($result));
- $this->perfbase->stopTraceSpan('data-processing');
-
- return $result;
+
+ try {
+ $result = $this->performProcessing($data);
+ $this->perfbase->setAttribute('processed_count', (string) count($result));
+ return $result;
+ } finally {
+ $this->perfbase->stopTraceSpan('data-processing');
+ }
}
}
```
-### User-Specific Profiling
+## User-specific request profiling
-Profile specific users by implementing the `ProfiledUser` interface:
+If your authenticated user model implements `Perfbase\Laravel\Interfaces\ProfiledUser`, HTTP request profiling will respect `shouldBeProfiled()`.
```php
use Perfbase\Laravel\Interfaces\ProfiledUser;
@@ -292,274 +334,79 @@ class User extends Authenticatable implements ProfiledUser
{
public function shouldBeProfiled(): bool
{
- // Profile admin users or users in beta testing
return $this->isAdmin() || $this->isBetaTester();
}
}
```
-## Artisan Commands
-
-### Sync Buffered Data
-
-When using `file` or `database` sending modes, use this command to send buffered data:
-
-```bash
-# Send all buffered trace data to Perfbase
-php artisan perfbase:sync
-
-# Recommended: Set up a cron job
-# * * * * * cd /path-to-your-project && php artisan perfbase:sync >> /dev/null 2>&1
-```
-
-### Clear Buffered Data
-
-Remove all locally buffered traces:
-
-```bash
-# Clear all buffered data (useful for debugging)
-php artisan perfbase:clear
-```
-
-## Advanced Configuration
-
-### Database Strategy Setup
-
-When using `database` sending mode, you may need to run the migration:
-
-```bash
-php artisan migrate
-```
-
-The package includes a migration for the `perfbase_profiles` table.
-
-### Custom Cache Paths
-
-For file-based buffering, customize the storage path:
-
-```php
-// config/perfbase.php
-'sending' => [
- 'mode' => 'file',
- 'config' => [
- 'file' => [
- 'path' => storage_path('app/perfbase-cache'),
- ],
- ],
-],
-```
-
-### Performance Optimization
-
-For high-traffic applications:
+If the authenticated user does not implement `ProfiledUser`, the package falls back to normal request filtering rules.
-```php
-// config/perfbase.php
-'sample_rate' => 0.01, // Profile 1% of requests
-'flags' => \Perfbase\SDK\FeatureFlags::UseCoarseClock |
- \Perfbase\SDK\FeatureFlags::TrackCpuTime |
- \Perfbase\SDK\FeatureFlags::TrackPdo,
-'sending' => ['mode' => 'file'], // Buffer locally
-```
-
-### Multi-Environment Setup
-
-```php
-// config/perfbase.php
-'enabled' => env('PERFBASE_ENABLED', app()->environment('production')),
-'sample_rate' => env('PERFBASE_SAMPLE_RATE', match(app()->environment()) {
- 'production' => 0.1,
- 'staging' => 0.5,
- 'local' => 1.0,
- default => 0.1
-}),
-```
-
-## Facade Methods
-
-The Perfbase facade provides access to all SDK methods:
+## Facade methods
| Method | Description |
-|--------|-------------|
-| `startTraceSpan($name, $attributes = [])` | Start profiling a named span |
-| `stopTraceSpan($name)` | Stop profiling a named span |
-| `setAttribute($key, $value)` | Add attribute to current trace |
-| `setFlags($flags)` | Change profiling feature flags |
-| `submitTrace()` | Submit trace data to Perfbase |
+| --- | --- |
+| `startTraceSpan($name, $attributes = [])` | Start a named span |
+| `stopTraceSpan($name)` | Stop a named span |
+| `setAttribute($key, $value)` | Add a string attribute to the current trace |
+| `setFlags($flags)` | Change extension feature flags |
+| `submitTrace()` | Submit trace data and return a `SubmitResult` |
| `getTraceData($spanName = '')` | Get raw trace data |
-| `reset()` | Clear current trace session |
-| `isExtensionAvailable()` | Check if extension is loaded |
-
-## Error Handling
-
-The package handles errors gracefully:
-
-```php
-// The package won't break your app if Perfbase is unavailable
-try {
- Perfbase::startTraceSpan('critical-operation');
- // Your code here
-} catch (\Perfbase\SDK\Exception\PerfbaseExtensionException $e) {
- // Extension not available - log but continue
- Log::warning('Perfbase extension not available: ' . $e->getMessage());
-}
-```
-
-## Troubleshooting
-
-### Extension Not Found
-
-```bash
-# Check if extension is loaded
-php -m | grep perfbase
+| `reset()` | Clear the current trace session |
+| `isExtensionAvailable()` | Check whether the native extension is loaded |
-# Check PHP configuration
-php --ini
+## Error handling
-# Reinstall extension
-bash -c "$(curl -fsSL https://cdn.perfbase.com/install.sh)"
-```
+The package is designed to fail open in normal operation. When profiling cannot start or trace submission fails, your Laravel request, command, or job should continue running.
-### Permission Issues (File Mode)
+Use `PERFBASE_DEBUG=true` if you want profiling exceptions to surface during local development.
-```bash
-# Ensure storage directory is writable
-chmod -R 755 storage/perfbase
-chown -R www-data:www-data storage/perfbase
-```
-
-### High Memory Usage
-
-```php
-// Reduce profiling overhead
-'flags' => \Perfbase\SDK\FeatureFlags::UseCoarseClock |
- \Perfbase\SDK\FeatureFlags::TrackCpuTime,
-'sample_rate' => 0.01, // Lower sample rate
-```
-
-### Database Issues (Database Mode)
+## Testing
-```bash
-# Ensure migration is run
-php artisan migrate
+In application tests, it is often simplest to disable profiling:
-# Check database connection
-php artisan tinker
->>> DB::connection()->getPdo();
+```xml
+
```
-## Testing
-
-When testing your Laravel application:
+You can also mock the facade:
```php
-// Disable Perfbase in tests
-// phpunit.xml
-
+use Perfbase\Laravel\Facades\Perfbase;
-// Or mock the facade in tests
public function test_something()
{
Perfbase::shouldReceive('startTraceSpan')->once();
Perfbase::shouldReceive('stopTraceSpan')->once();
-
- // Your test code
+
+ // ...
}
```
-## Performance Impact
-
-- **Minimal Overhead**: ~1-3ms per request with default settings
-- **Sampling**: Use sample rates to reduce impact in production
-- **Async Options**: File/database modes reduce request impact
-- **Selective Profiling**: Use include/exclude filters strategically
-
-## Security Considerations
-
-- **API Key Security**: Store API keys in environment variables, not code
-- **Data Privacy**: Configure include/exclude filters to avoid sensitive routes
-- **User Profiling**: Implement `ProfiledUser` interface to control user-specific profiling
-- **Network Security**: Use HTTPS endpoints and configure proxy if needed
-
-## Examples
+## Troubleshooting
-### E-commerce Checkout
+### Extension not loaded
-```php
-class CheckoutController extends Controller
-{
- public function process(Request $request)
- {
- Perfbase::startTraceSpan('checkout-process', [
- 'user_id' => auth()->id(),
- 'cart_items' => $request->items->count(),
- 'payment_method' => $request->payment_method
- ]);
-
- try {
- $order = $this->createOrder($request);
- $payment = $this->processPayment($order);
-
- Perfbase::setAttribute('order_id', $order->id);
- Perfbase::setAttribute('payment_status', $payment->status);
-
- return response()->json(['order' => $order]);
- } finally {
- Perfbase::stopTraceSpan('checkout-process');
- }
- }
-}
+```bash
+php -m | grep perfbase
+php --ini
+bash -c "$(curl -fsSL https://cdn.perfbase.com/install.sh)"
```
-### Background Job Processing
+### High overhead
-```php
-class ProcessEmailCampaignJob implements ShouldQueue
-{
- public function handle()
- {
- // Automatic profiling happens via queue listener
- // But you can add custom spans for detailed tracking
-
- Perfbase::startTraceSpan('email-template-render');
- $template = $this->renderTemplate();
- Perfbase::stopTraceSpan('email-template-render');
-
- Perfbase::startTraceSpan('email-send-batch');
- $this->sendEmails($template);
- Perfbase::stopTraceSpan('email-send-batch');
- }
-}
-```
+- Lower `PERFBASE_SAMPLE_RATE`
+- Use `FeatureFlags::UseCoarseClock`
+- Disable feature flags you do not need
+- Narrow your `include` filters and expand your `exclude` filters
## Documentation
-Comprehensive documentation is available at [https://docs.perfbase.com](https://docs.perfbase.com), including:
-
-- Complete API reference
-- Framework-specific guides
-- Performance optimization tips
-- Data privacy and security policies
-- Troubleshooting guides
-
-## Contributing
-
-We welcome contributions! Please see our [contributing guidelines](CONTRIBUTING.md) and feel free to submit pull requests.
-
-## Security
+Full documentation is available at [perfbase.com/docs](https://perfbase.com/docs).
-If you discover any security-related issues, please email [security@perfbase.com](mailto:security@perfbase.com) instead of using the issue tracker.
-
-## Support
-
-- **Email**: [support@perfbase.com](mailto:support@perfbase.com)
-- **Documentation**: [https://docs.perfbase.com](https://docs.perfbase.com)
-- **Issues**: [GitHub Issues](https://github.com/perfbaseorg/laravel/issues)
+- **Docs**: [perfbase.com/docs](https://perfbase.com/docs)
+- **Issues**: [github.com/perfbaseorg/laravel/issues](https://github.com/perfbaseorg/laravel/issues)
+- **Support**: [support@perfbase.com](mailto:support@perfbase.com)
## License
-This project is licensed under the Apache License 2.0. Please see the [License File](LICENSE.txt) for more information.
-
----
-
-**Made with ❤️ by the Perfbase team**
\ No newline at end of file
+Apache-2.0. See [LICENSE.txt](LICENSE.txt).
diff --git a/composer.json b/composer.json
index 5999492..bbc5dc8 100644
--- a/composer.json
+++ b/composer.json
@@ -24,7 +24,7 @@
"ext-json": "*",
"ext-zlib": "*",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0",
- "perfbase/php-sdk": "^0.3.0",
+ "perfbase/php-sdk": "1.0.0",
"guzzlehttp/guzzle": "^7.0"
},
"require-dev": {
diff --git a/config/perfbase.php b/config/perfbase.php
index 9c2c5e7..d90abe7 100644
--- a/config/perfbase.php
+++ b/config/perfbase.php
@@ -25,6 +25,20 @@
*/
'enabled' => env('PERFBASE_ENABLED', false),
+ /*
+ |--------------------------------------------------------------------------
+ | Debug Mode
+ |--------------------------------------------------------------------------
+ */
+ 'debug' => env('PERFBASE_DEBUG', false),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Log Errors
+ |--------------------------------------------------------------------------
+ */
+ 'log_errors' => env('PERFBASE_LOG_ERRORS', true),
+
/*
|--------------------------------------------------------------------------
| API Key - Required - Used to authenticate your project with Perfbase.
@@ -53,35 +67,17 @@
/*
|--------------------------------------------------------------------------
- | Sending Configuration - Used to control when data is sent to Perfbase.
+ | HTTP Timeout - Timeout in seconds for API requests.
|--------------------------------------------------------------------------
- |
- | If you'd like to buffer data before sending it to Perfbase, you can configure
- | the `mode` option in the transmission settings. The available mode values are:
- | - 'sync': Sends data immediately without buffering.
- | - 'file': Stores data in files before sending it to Perfbase.
- | - 'database': Caches data in a database table before sending.
- |
- | When using modes other than 'sync', data will be collected locally.
- | To process and send this buffered data to Perfbase, use the `perfbase:sync`
- | Artisan command. It's recommended to set up a cron to periodically run it.
- |
*/
- 'sending' => [
- 'mode' => env('PERFBASE_SENDING_MODE', 'sync'),
- 'timeout' => env('PERFBASE_TIMEOUT', 5),
- 'proxy' => env('PERFBASE_PROXY'),
- 'config' => [
- 'sync' => [],
- 'file' => [
- 'path' => storage_path('perfbase'),
- ],
- 'database' => [
- 'connection' => 'default',
- 'table' => 'perfbase_cache',
- ]
- ]
- ],
+ 'timeout' => env('PERFBASE_TIMEOUT', 5),
+
+ /*
+ |--------------------------------------------------------------------------
+ | HTTP Proxy - Optional proxy for API requests.
+ |--------------------------------------------------------------------------
+ */
+ 'proxy' => env('PERFBASE_PROXY'),
/*
|--------------------------------------------------------------------------
@@ -159,10 +155,8 @@
*/
'include' => [
'http' => ['.*'],
- 'artisan' => ['.*'],
- 'jobs' => ['.*'],
- 'schedule' => ['.*'],
- 'exception' => ['.*'],
+ 'console' => ['.*'],
+ 'queue' => ['.*'],
],
/*
@@ -176,10 +170,8 @@
*/
'exclude' => [
'http' => [],
- 'artisan' => ['queue:work'],
- 'jobs' => [],
- 'schedule' => [],
- 'exception' => [],
+ 'console' => ['queue:work'],
+ 'queue' => [],
],
];
diff --git a/database/migrations/2024_11_07_125701_create_perfbase_profiles_table.php b/database/migrations/2024_11_07_125701_create_perfbase_profiles_table.php
deleted file mode 100644
index eae7fc9..0000000
--- a/database/migrations/2024_11_07_125701_create_perfbase_profiles_table.php
+++ /dev/null
@@ -1,26 +0,0 @@
-create($table, function (Blueprint $table) {
- $table->id();
- $table->longText('data');
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- $connection = config('perfbase.cache.config.database.connection');
- $table = config('perfbase.cache.config.database.table');
- Schema::connection($connection)->dropIfExists($table);
- }
-};
diff --git a/phpunit.xml b/phpunit.xml
index 92e2786..2f8b4cf 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -8,6 +8,12 @@
+
+
+ src
+
+
+
diff --git a/src/Caching/CacheStrategy.php b/src/Caching/CacheStrategy.php
deleted file mode 100644
index cfc354b..0000000
--- a/src/Caching/CacheStrategy.php
+++ /dev/null
@@ -1,52 +0,0 @@
- $profileData The profile data to store
- * @return void
- */
- public function store(array $profileData): void;
-
- /**
- * Get profiles that haven't been synced yet.
- *
- * @param int $chunk Maximum number of profiles to retrieve at once
- * @return iterable>>
- */
- public function getUnsentProfiles(int $chunk = 100): iterable;
-
- /**
- * Count the number of unsent profiles in the cache.
- *
- * @return int
- */
- public function countUnsentProfiles(): int;
-
- /**
- * Delete a specific profile from the cache.
- *
- * @param mixed $id The profile identifier
- * @return void
- */
- public function delete($id): void;
-
- /**
- * Delete multiple profiles from the cache.
- *
- * @param array $ids
- * @return void
- */
- public function deleteMass(array $ids): void;
-
- /**
- * Clear all cached profiles.
- *
- * @return void
- */
- public function clear(): void;
-}
\ No newline at end of file
diff --git a/src/Caching/CacheStrategyFactory.php b/src/Caching/CacheStrategyFactory.php
deleted file mode 100644
index 8ca2682..0000000
--- a/src/Caching/CacheStrategyFactory.php
+++ /dev/null
@@ -1,25 +0,0 @@
- $profileData The profile data to store
- * @return void
- */
- public function store(array $profileData): void
- {
- Profile::query()->create([
- 'data' => $profileData['data'] ?? serialize($profileData)
- ]);
- }
-
- /**
- * Get profiles that haven't been synced yet.
- *
- * @param int $chunk Maximum number of profiles to retrieve at once
- * @return iterable>>
- */
- public function getUnsentProfiles(int $chunk = 100): iterable
- {
- $lastId = 0;
- while (true) {
-
- /** @var Collection $profiles */
- $profiles = Profile::query()
- ->where('id', '>', $lastId)
- ->orderBy('id')
- ->limit($chunk)
- ->get();
-
- if (!count($profiles)) {
- break;
- }
-
- /** @var Profile $last */
- $last = $profiles->last();
-
- /** @var int $lastId */
- $lastId = $last->getKey();
-
- /** @var array> $yield */
- $yield = [];
-
- /** @var Profile $profile */
- foreach ($profiles as $profile) {
-
- /** @var int $id */
- $id = $profile->getAttribute('id');
-
- /** @var array $data */
- $data = $profile->getAttribute('data');
-
- /** @var string $created_at */
- $created_at = $profile->getAttribute('created_at');
-
- $yield[] = [
- 'id' => $id,
- 'data' => $data,
- 'created_at' => $created_at
- ];
- }
-
- yield $yield;
- }
- }
-
- /**
- * Count the number of unsent profiles in the cache.
- *
- * @return int
- */
- public function countUnsentProfiles(): int
- {
- return Profile::query()->count();
- }
-
- /**
- * Delete multiple profiles from the cache.
- *
- * @param array $ids
- * @return void
- */
- public function deleteMass(array $ids): void
- {
- Profile::query()->whereIn('id', $ids)
- ->delete();
- }
-
- /**
- * Delete a specific profile from the database.
- *
- * @param int $id The model id
- * @return void
- */
- public function delete($id): void
- {
- Profile::query()->where('id', $id)
- ->delete();
- }
-
- /**
- * Clear all profiles from the database.
- *
- * @return void
- */
- public function clear(): void
- {
- Profile::query()->truncate();
- }
-}
\ No newline at end of file
diff --git a/src/Caching/FileStrategy.php b/src/Caching/FileStrategy.php
deleted file mode 100644
index e9df44d..0000000
--- a/src/Caching/FileStrategy.php
+++ /dev/null
@@ -1,148 +0,0 @@
-path = $path;
- }
-
- /**
- * Store a new profile as a file.
- *
- * @param array $profileData The profile data to store
- * @return void
- */
- public function store(array $profileData): void
- {
- if (!File::exists($this->path)) {
- File::makeDirectory($this->path, 0755, true);
- }
-
- $filename = Str::uuid() . $this->extension;
- File::put($this->path . '/' . $filename, serialize([
- 'id' => $filename,
- 'data' => $profileData,
- 'created_at' => now()->toIso8601String(),
- ]));
- }
-
- /**
- * Get profiles that haven't been synced yet.
- *
- * @param int $chunk Maximum number of profiles to retrieve at once
- * @return iterable>>
- */
- public function getUnsentProfiles(int $chunk = 100): iterable
- {
- $files = collect(File::files($this->path));
-
- /** @var array> $fileChunks */
- $fileChunks = $files->chunk($chunk);
-
- // Yield each profile in the chunk
- foreach ($fileChunks as $fileChunk) {
-
- /** @var array> $yield */
- $yield = [];
-
- foreach ($fileChunk as $file) {
-
- /** @var array $content */
- $content = unserialize(File::get($file));
-
- /** @var string $data - This will be json data */
- $data = $content['data'];
-
- /** @var string $created_at */
- $created_at = $content['created_at'];
-
- $yield[] = [
- 'id' => $file,
- 'data' => $data,
- 'created_at' => $created_at
- ];
- }
-
- yield $yield;
- }
- }
-
- /**
- * Count the number of unsent profiles in the cache.
- *
- * @return int
- */
- public function countUnsentProfiles(): int
- {
- return collect(File::files($this->path))
- ->filter(fn(SplFileInfo $file) => Str::endsWith($file->getFilename(), $this->extension))
- ->count();
- }
-
- /**
- * Delete multiple profiles from the cache.
- *
- * @param array $ids
- * @return void
- */
- public function deleteMass(array $ids): void
- {
- foreach ($ids as $id) {
- $this->delete($id);
- }
- }
-
- /**
- * Delete a specific profile from the filesystem.
- *
- * @param string $id The file path
- * @return void
- */
- public function delete($id): void
- {
- $fullPath = $this->path . '/' . $id;
- if (File::exists($fullPath)) {
- File::delete($fullPath);
- }
- }
-
- /**
- * Clear all profile files from the storage directory.
- *
- * @return void
- */
- public function clear(): void
- {
- collect(File::files($this->path))
- ->filter(fn(SplFileInfo $file) => Str::endsWith($file->getFilename(), $this->extension))
- ->each(fn(SplFileInfo $file) => File::delete($file->getRealPath()));
- }
-}
\ No newline at end of file
diff --git a/src/Commands/PerfbaseClearCommand.php b/src/Commands/PerfbaseClearCommand.php
deleted file mode 100644
index f8c347a..0000000
--- a/src/Commands/PerfbaseClearCommand.php
+++ /dev/null
@@ -1,31 +0,0 @@
-info('Clearing cached profiles...');
- $strategy = CacheStrategyFactory::make();
- $strategy->clear();
- $this->info('All cached profiles have been cleared.');
- return self::SUCCESS;
- }
-}
diff --git a/src/Commands/PerfbaseSyncCommand.php b/src/Commands/PerfbaseSyncCommand.php
deleted file mode 100644
index 0167a8a..0000000
--- a/src/Commands/PerfbaseSyncCommand.php
+++ /dev/null
@@ -1,159 +0,0 @@
-error('Perfbase is not configured to use a cache strategy');
- return self::FAILURE;
- }
-
- $this->info(sprintf('Syncing profiles from %s to Perfbase API...', $strategy));
-
- // Begin transaction if using database strategy
- if ($strategy === 'database') {
- DB::connection($this->getConnectionName())->beginTransaction();
- }
-
- try {
-
- // Get the cache strategy
- $cache = CacheStrategyFactory::make();
-
- // Check for unsent profiles
- $this->info('Checking for unsent profiles...');
- $unsentCount = $cache->countUnsentProfiles();
-
- // If there are no unsent profiles, we can skip the sync
- if ($unsentCount === 0) {
- $this->info('No unsent profiles found, nothing to sync!');
- return self::SUCCESS;
- }
-
- $this->info(sprintf('Found %d unsent profiles, syncing...', $unsentCount));
-
- /** @var Application $app */
- $app = app();
-
- /**
- * Initialize the Perfbase SDK client
- * @var Config $config
- */
- $config = $app->make(Config::class);
-
- $client = new ApiClient($config);
-
- /**
- * IDs of profiles that have been synced.
- * We send in batches to avoid memory issues, so we need to keep track of the IDs.
- * @var array $ids
- */
- $ids = [];
-
- try {
- // Grab a chunk of profiles from the cache and send them to Perfbase
- foreach ($cache->getUnsentProfiles(self::CHUNK_SIZE) as $profileChunk) {
-
- // Foreach profile
- foreach ($profileChunk as $profile) {
-
- /** @var string $traceId */
- $traceId = $profile['id'];
- if (!is_string($traceId)) {
- throw new RuntimeException(sprintf('Found invalid `id` for profile ID: %s', $traceId));
- }
-
- /** @var string $traceData */
- $traceData = $profile['data'];
- if (!is_array($traceData)) {
- throw new RuntimeException(sprintf('Found invalid `data` for profile ID: %s', $traceId));
- }
-
- /** @var string $traceCreatedAt */
- $traceCreatedAt = $profile['created_at'];
- if (!strtotime($traceCreatedAt)) {
- throw new RuntimeException(sprintf('Found invalid `created_at` timestamp for profile ID: %s', $traceId));
- }
-
- // Submit to the API
- $client->submitTrace($traceData);
-
- // Store the ID for deletion
- $ids[] = $profile['id'];
- }
-
- // Delete the chunk of profiles from the cache and clear the IDs array
- $cache->deleteMass($ids);
-
- /** @var string $firstId */
- $firstId = $ids[0];
-
- /** @var string $lastId */
- $lastId = $ids[count($ids) - 1];
-
- $this->info(sprintf('Synced %d profiles, from profile %s to %s', count($ids), $firstId, $lastId));
- $ids = [];
-
- }
- } catch (Throwable $e) {
- $this->error($e->getMessage());
- if (!empty($ids)) {
- $this->warn('An error occurred mid-sync, deleting the IDs that were synced');
- $cache->deleteMass($ids);
- }
- throw new RuntimeException('Error occurred during sync, halting.');
- }
- } catch (Throwable $e) {
- $this->error($e->getMessage());
- return self::FAILURE;
- } finally {
- // Finish up transaction if using database strategy
- if ($strategy === 'database') {
- // Finish up transaction
- DB::connection($this->getConnectionName())->commit();
- }
- }
-
- $this->info('Sync complete');
-
- return self::SUCCESS;
- }
-
- function getConnectionName(): string
- {
- $name = config('database.default');
- if (!is_string($name)) {
- throw new RuntimeException('Invalid connection name');
- }
- return $name;
- }
-
-}
diff --git a/src/Facades/Perfbase.php b/src/Facades/Perfbase.php
index 7765343..19cd823 100644
--- a/src/Facades/Perfbase.php
+++ b/src/Facades/Perfbase.php
@@ -7,7 +7,7 @@
/**
* @method static void startTraceSpan(string $spanName, array $attributes = [])
* @method static bool stopTraceSpan(string $spanName)
- * @method static void submitTrace()
+ * @method static \Perfbase\SDK\SubmitResult submitTrace()
* @method static string getTraceData(string $spanName = '')
* @method static void reset()
* @method static bool isExtensionAvailable()
diff --git a/src/Lifecycle/ConsoleTraceLifecycle.php b/src/Lifecycle/ConsoleTraceLifecycle.php
new file mode 100644
index 0000000..b496974
--- /dev/null
+++ b/src/Lifecycle/ConsoleTraceLifecycle.php
@@ -0,0 +1,33 @@
+command = $command;
+ }
+
+ protected function shouldProfile(): bool
+ {
+ return FilterMatcher::passesConfigFilters([$this->command], 'console');
+ }
+
+ protected function setDefaultAttributes(): void
+ {
+ parent::setDefaultAttributes();
+
+ $this->setAttributes([
+ 'source' => 'console',
+ 'action' => $this->command,
+ ]);
+ }
+}
diff --git a/src/Lifecycle/HttpTraceLifecycle.php b/src/Lifecycle/HttpTraceLifecycle.php
new file mode 100644
index 0000000..c8ebdac
--- /dev/null
+++ b/src/Lifecycle/HttpTraceLifecycle.php
@@ -0,0 +1,107 @@
+request = $request;
+ }
+
+ public function setResponse(Response $response): void
+ {
+ $this->setAttribute('http_status_code', (string) $response->getStatusCode());
+ }
+
+ protected function shouldProfile(): bool
+ {
+ if (!config('perfbase.enabled', false)) {
+ return false;
+ }
+
+ /** @var Authenticatable|null $user */
+ $user = $this->request->user();
+ if ($user instanceof ProfiledUser && !$user->shouldBeProfiled()) {
+ return false;
+ }
+
+ $components = $this->getRequestComponents();
+ if (!FilterMatcher::passesConfigFilters($components, 'http')) {
+ return false;
+ }
+
+ if (!$this->perfbase->isExtensionAvailable()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function setDefaultAttributes(): void
+ {
+ parent::setDefaultAttributes();
+
+ $route = $this->request->route();
+ $action = $route instanceof Route
+ ? sprintf('%s %s', $this->request->method(), $route->uri())
+ : sprintf('%s %s', $this->request->method(), $this->request->path());
+
+ $this->setAttributes([
+ 'source' => 'http',
+ 'action' => $action,
+ 'http_method' => $this->request->method(),
+ 'http_url' => $this->request->fullUrl(),
+ 'user_ip' => EnvironmentUtils::getUserIp() ?? '',
+ 'user_agent' => EnvironmentUtils::getUserUserAgent() ?? '',
+ ]);
+
+ if (Auth::check()) {
+ $this->setAttribute('user_id', (string) Auth::id());
+ }
+ }
+
+ /**
+ * @return array
+ */
+ private function getRequestComponents(): array
+ {
+ $pathWithSlash = '/' . ltrim($this->request->path(), '/');
+ $components = [
+ sprintf('%s %s', $this->request->method(), $pathWithSlash),
+ sprintf('%s %s', $this->request->method(), $this->request->path()),
+ $this->request->path(),
+ $pathWithSlash,
+ ];
+
+ $route = $this->request->route();
+ if ($route instanceof Route) {
+ $explodedAction = explode('@', $route->getActionName());
+ $components[] = $route->getActionName();
+ $components[] = $route->uri();
+ $components[] = '/' . ltrim($route->uri(), '/');
+ $components[] = $explodedAction[0];
+
+ foreach ($route->methods() as $method) {
+ $components[] = sprintf('%s %s', $method, $route->uri());
+ $components[] = sprintf('%s %s', $method, '/' . ltrim($route->uri(), '/'));
+ }
+ }
+
+ return $components;
+ }
+}
diff --git a/src/Lifecycle/QueueTraceLifecycle.php b/src/Lifecycle/QueueTraceLifecycle.php
new file mode 100644
index 0000000..371ea39
--- /dev/null
+++ b/src/Lifecycle/QueueTraceLifecycle.php
@@ -0,0 +1,39 @@
+jobName = $jobName;
+ $this->queue = $queue;
+ $this->connection = $connection;
+ }
+
+ protected function shouldProfile(): bool
+ {
+ return FilterMatcher::passesConfigFilters([$this->jobName], 'queue');
+ }
+
+ protected function setDefaultAttributes(): void
+ {
+ parent::setDefaultAttributes();
+
+ $this->setAttributes([
+ 'source' => 'queue',
+ 'action' => $this->jobName,
+ 'queue' => $this->queue,
+ 'connection' => $this->connection,
+ ]);
+ }
+}
diff --git a/src/Middleware/PerfbaseMiddleware.php b/src/Middleware/PerfbaseMiddleware.php
index 2e0795c..13d6b09 100644
--- a/src/Middleware/PerfbaseMiddleware.php
+++ b/src/Middleware/PerfbaseMiddleware.php
@@ -3,50 +3,27 @@
namespace Perfbase\Laravel\Middleware;
use Closure;
-use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Request;
-use JsonException;
-use Perfbase\Laravel\Profiling\HttpProfiler;
-use Perfbase\SDK\Exception\PerfbaseException;
-use Perfbase\SDK\Exception\PerfbaseExtensionException;
-use Perfbase\SDK\Exception\PerfbaseInvalidSpanException;
+use Perfbase\Laravel\Lifecycle\HttpTraceLifecycle;
use Symfony\Component\HttpFoundation\Response;
-/**
- * Class PerfbaseMiddleware
- *
- * Middleware to handle request profiling using Perfbase.
- */
class PerfbaseMiddleware
{
- /**
- * Handle the incoming request.
- *
- * @param Request $request
- * @param Closure $next
- * @return Response
- * @throws BindingResolutionException
- * @throws JsonException
- * @throws PerfbaseException
- * @throws PerfbaseExtensionException
- * @throws PerfbaseInvalidSpanException
- */
public function handle(Request $request, Closure $next): Response
{
- // Check if profiling is enabled
if (!config('perfbase.enabled')) {
- // No profiling enabled, just pass the request
return $next($request);
}
- // Profiler is enabled, start profiling
- $profiler = new HttpProfiler($request);
- $profiler->startProfiling();
+ $lifecycle = new HttpTraceLifecycle($request);
+ $lifecycle->startProfiling();
+
/** @var Response $response */
$response = $next($request);
- $profiler->setResponse($response);
- $profiler->stopProfiling();
- return $response;
+ $lifecycle->setResponse($response);
+ $lifecycle->stopProfiling();
+
+ return $response;
}
}
diff --git a/src/Models/Profile.php b/src/Models/Profile.php
deleted file mode 100644
index d404978..0000000
--- a/src/Models/Profile.php
+++ /dev/null
@@ -1,75 +0,0 @@
-
- */
- protected $fillable = [
- 'data'
- ];
-
- /**
- * Get the database connection for the model.
- *
- * @return string
- */
- public function getConnectionName()
- {
- $connection = config('perfbase.cache.config.database.connection');
- if (!is_string($connection)) {
- throw new RuntimeException('Invalid connection name');
- }
- return $connection;
- }
-
- /**
- * Get the table associated with the model.
- *
- * @return string
- */
- public function getTable()
- {
- $table = config('perfbase.cache.config.database.table');
- if (!is_string($table)) {
- throw new RuntimeException('Invalid table name');
- }
-
- return $table;
- }
-
- /**
- * Encode the data attribute into a base64 string before saving to the database.
- * This is done because the data is in binary format.
- *
- * @param string $value
- * @return void
- */
- public function setDataAttribute(string $value): void
- {
- $this->attributes['data'] = base64_encode($value);
- }
-
- /**
- * Decode the data attribute back into binary data.
- *
- * @return string
- */
- public function getDataAttribute(): string
- {
- $data = $this->attributes['data'];
- if (!is_string($data)) {
- throw new RuntimeException('Invalid data attribute');
- }
-
- return base64_decode($data);
- }
-
-}
diff --git a/src/PerfbaseServiceProvider.php b/src/PerfbaseServiceProvider.php
index fbd4716..daa2050 100644
--- a/src/PerfbaseServiceProvider.php
+++ b/src/PerfbaseServiceProvider.php
@@ -11,47 +11,45 @@
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
+use Perfbase\Laravel\Lifecycle\ConsoleTraceLifecycle;
+use Perfbase\Laravel\Lifecycle\QueueTraceLifecycle;
use Perfbase\Laravel\Profiling\AbstractProfiler;
-use Perfbase\Laravel\Profiling\ConsoleProfiler;
-use Perfbase\Laravel\Profiling\QueueProfiler;
-use Perfbase\Laravel\Profiling\UniversalProfiler;
use Perfbase\Laravel\Support\PerfbaseConfig;
use Perfbase\Laravel\Support\PerfbaseErrorHandling;
-use Perfbase\Laravel\Support\SpanNaming;
use Perfbase\SDK\Config;
use Perfbase\SDK\Config as SdkConfig;
use Perfbase\SDK\Perfbase;
use Perfbase\SDK\Perfbase as PerfbaseClient;
use Perfbase\SDK\Extension\ExtensionInterface;
-/**
- * Class PerfbaseServiceProvider
- */
class PerfbaseServiceProvider extends ServiceProvider
{
use PerfbaseErrorHandling;
/**
- * Unified span storage with unique IDs
- * @var array
+ * Active profiler instances keyed by span ID.
+ * @var array
*/
private array $spans = [];
/**
- * @return void
+ * Queue job span IDs keyed by job ID.
+ * @var array
*/
- public function boot()
+ private array $queueSpanIds = [];
+
+ /**
+ * Console command span IDs keyed by command name.
+ * @var array
+ */
+ private array $consoleSpanIds = [];
+
+ public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__ . '/../config/perfbase.php' => config_path('perfbase.php'),
], 'perfbase-config');
-
- // Register commands
- $this->commands([
- \Perfbase\Laravel\Commands\PerfbaseClearCommand::class,
- \Perfbase\Laravel\Commands\PerfbaseSyncCommand::class,
- ]);
}
if (PerfbaseConfig::enabled()) {
@@ -59,42 +57,19 @@ public function boot()
}
}
- /**
- * Register the application services.
- * @return void
- */
- public function register()
+ public function register(): void
{
- // Register the config
$this->mergeConfigFrom(__DIR__ . '/../config/perfbase.php', 'perfbase');
- /**
- * Bind the Config class to the container
- */
$this->app->bind(Config::class, function (Application $app) {
-
- /**
- * @var array $config
- */
+ /** @var array $config */
$config = $app['config'];
- /** @var int $flags */
- $flags = $config['perfbase.flags'];
-
- /** @var string|null $proxy */
- $proxy = $config['perfbase.sending.proxy'];
-
- /** @var numeric $timeout */
- $timeout = $config['perfbase.sending.timeout'];
-
- /** @var string $apiKey */
- $apiKey = $config['perfbase.api_key'];
-
return Config::fromArray([
- 'api_key' => $apiKey,
- 'flags' => $flags,
- 'proxy' => $proxy,
- 'timeout' => $timeout,
+ 'api_key' => $config['perfbase.api_key'],
+ 'flags' => $config['perfbase.flags'],
+ 'proxy' => $config['perfbase.proxy'],
+ 'timeout' => $config['perfbase.timeout'],
]);
});
@@ -102,73 +77,61 @@ public function register()
/** @var SdkConfig $config */
$config = $app->make(SdkConfig::class);
- // Check if we have a mocked extension in the container (for testing)
- $extension = $app->bound(ExtensionInterface::class)
- ? $app->make(ExtensionInterface::class)
+ $extension = $app->bound(ExtensionInterface::class)
+ ? $app->make(ExtensionInterface::class)
: null;
- // Start a new perfbase instance
return new PerfbaseClient($config, $extension);
});
}
- /**
- * Register unified event listeners for profiling
- *
- * @return void
- */
private function registerEventListeners(): void
{
+ // Queue: JobProcessing → start, JobProcessed/JobExceptionOccurred → stop
$this->registerEventPair(
JobProcessing::class,
JobProcessed::class,
JobExceptionOccurred::class,
- fn($event) => $this->createQueueProfiler($event)
+ fn($event) => $this->createQueueLifecycle($event)
);
+ // Console: CommandStarting → start, CommandFinished → stop
$this->registerEventPair(
CommandStarting::class,
CommandFinished::class,
null,
- fn($event) => $this->createConsoleProfiler($event)
+ fn($event) => $this->createConsoleLifecycle($event)
);
}
- /**
- * Register a pair of start/stop/error events with unified handling
- *
- * @param string $startEvent
- * @param string $stopEvent
- * @param string|null $errorEvent
- * @param callable $profilerFactory
- * @return void
- */
- private function registerEventPair(string $startEvent, string $stopEvent, ?string $errorEvent, callable $profilerFactory): void
- {
- // Start event handler
- Event::listen($startEvent, function ($event) use ($profilerFactory) {
+ private function registerEventPair(
+ string $startEvent,
+ string $stopEvent,
+ ?string $errorEvent,
+ callable $lifecycleFactory
+ ): void {
+ Event::listen($startEvent, function ($event) use ($lifecycleFactory) {
try {
$spanId = uniqid('span_', true);
- $profiler = $profilerFactory($event);
-
- if ($profiler) {
- $this->spans[$spanId] = $profiler;
+ $lifecycle = $lifecycleFactory($event);
+
+ if ($lifecycle) {
+ $this->spans[$spanId] = $lifecycle;
$this->storeSpanId($event, $spanId);
- $profiler->startProfiling();
+ $lifecycle->startProfiling();
}
} catch (\Throwable $e) {
$this->handleProfilingError($e, 'event_start');
}
});
- // Stop event handler
Event::listen($stopEvent, function ($event) {
try {
$spanId = $this->getSpanId($event);
if ($spanId && isset($this->spans[$spanId])) {
- $profiler = $this->spans[$spanId];
- $this->handleEventData($event, $profiler);
- $profiler->stopProfiling();
+ $lifecycle = $this->spans[$spanId];
+ $this->handleEventData($event, $lifecycle);
+ $lifecycle->stopProfiling();
unset($this->spans[$spanId]);
}
} catch (\Throwable $e) {
@@ -176,15 +139,14 @@ private function registerEventPair(string $startEvent, string $stopEvent, ?strin
}
});
- // Error event handler (if provided)
if ($errorEvent) {
Event::listen($errorEvent, function ($event) {
try {
$spanId = $this->getSpanId($event);
if ($spanId && isset($this->spans[$spanId])) {
- $profiler = $this->spans[$spanId];
- $this->handleEventData($event, $profiler);
- $profiler->stopProfiling();
+ $lifecycle = $this->spans[$spanId];
+ $this->handleEventData($event, $lifecycle);
+ $lifecycle->stopProfiling();
unset($this->spans[$spanId]);
}
} catch (\Throwable $e) {
@@ -194,119 +156,80 @@ private function registerEventPair(string $startEvent, string $stopEvent, ?strin
}
}
- /**
- * Create a queue profiler from event
- *
- * @param JobProcessing $event
- * @return UniversalProfiler
- */
- private function createQueueProfiler(JobProcessing $event): UniversalProfiler
+ private function createQueueLifecycle(JobProcessing $event): QueueTraceLifecycle
{
- $jobName = $this->getCommandFromJob($event->job);
- $spanName = SpanNaming::forQueue($jobName);
-
- return new UniversalProfiler($spanName, [
- 'job_name' => $jobName,
- 'queue' => $event->job->getQueue(),
- 'connection' => $event->connectionName,
- ]);
+ $jobName = $this->getJobDisplayName($event->job);
+
+ return new QueueTraceLifecycle(
+ $jobName,
+ $event->job->getQueue(),
+ $event->connectionName
+ );
}
- /**
- * Create a console profiler from event
- *
- * @param CommandStarting $event
- * @return UniversalProfiler|null
- */
- private function createConsoleProfiler(CommandStarting $event): ?UniversalProfiler
+ private function createConsoleLifecycle(CommandStarting $event): ?ConsoleTraceLifecycle
{
- if (!$event->command || !$event->input || !$event->output) {
+ if (!$event->command) {
return null;
}
- $spanName = SpanNaming::forConsole($event->command);
-
- return new UniversalProfiler($spanName, [
- 'command' => $event->command,
- 'arguments' => $event->input->getArguments(),
- 'options' => $event->input->getOptions(),
- ]);
+ return new ConsoleTraceLifecycle($event->command);
}
- /**
- * Store span ID for later retrieval
- *
- * @param mixed $event
- * @param string $spanId
- * @return void
- */
- private function storeSpanId($event, string $spanId): void
+ /** @param JobProcessing|CommandStarting $event */
+ private function storeSpanId(object $event, string $spanId): void
{
if ($event instanceof JobProcessing) {
- // Queue job - store in payload
- $payload = $event->job->payload();
- $payload['perfbase_span_id'] = $spanId;
- } else {
- // Console command - store as property
- $event->perfbaseSpanId = $spanId;
+ $jobId = $event->job->getJobId() ?? spl_object_hash($event->job);
+ $this->queueSpanIds[$jobId] = $spanId;
+ } elseif ($event instanceof CommandStarting && $event->command) {
+ $this->consoleSpanIds[$event->command] = $spanId;
}
}
- /**
- * Get span ID from event
- *
- * @param mixed $event
- * @return string|null
- */
- private function getSpanId($event): ?string
+ /** @param JobProcessed|JobExceptionOccurred|CommandFinished $event */
+ private function getSpanId(object $event): ?string
{
if ($event instanceof JobProcessed || $event instanceof JobExceptionOccurred) {
- return $event->job->payload()['perfbase_span_id'] ?? null;
+ $jobId = $event->job->getJobId() ?? spl_object_hash($event->job);
+ $spanId = $this->queueSpanIds[$jobId] ?? null;
+ unset($this->queueSpanIds[$jobId]);
+ return $spanId;
+ }
+
+ if ($event instanceof CommandFinished && $event->command) {
+ $spanId = $this->consoleSpanIds[$event->command] ?? null;
+ unset($this->consoleSpanIds[$event->command]);
+ return $spanId;
}
-
- return $event->perfbaseSpanId ?? null;
+
+ return null;
}
- /**
- * Handle event-specific data
- *
- * @param mixed $event
- * @param AbstractProfiler $profiler
- * @return void
- */
- private function handleEventData($event, AbstractProfiler $profiler): void
+ /** @param JobProcessed|JobExceptionOccurred|CommandFinished $event */
+ private function handleEventData(object $event, AbstractProfiler $lifecycle): void
{
if ($event instanceof JobExceptionOccurred) {
- $profiler->setException($event->exception->getMessage());
+ $lifecycle->setException($event->exception->getMessage());
} elseif ($event instanceof CommandFinished) {
- $profiler->setExitCode($event->exitCode);
+ $lifecycle->setExitCode($event->exitCode);
}
}
-
- /**
- * Get the command name from the job.
- * @param Job $job
- * @return string
- */
- private function getCommandFromJob(Job $job): string
+ private function getJobDisplayName(Job $job): string
{
$payload = $job->payload();
- // Try to get the display name first as it's the most reliable
if (isset($payload['displayName'])) {
return $payload['displayName'];
}
- // Try to get the command name from data
if (isset($payload['data']['commandName'])) {
return $payload['data']['commandName'];
}
- // Try to unserialize the command if it's a serialized object
if (isset($payload['data']['command'])) {
$command = $payload['data']['command'];
- // Check if it's a serialized object (starts with O: or a:)
if (is_string($command) && preg_match('/^[Oa]:\d+:/', $command)) {
try {
$unserialized = unserialize($command);
@@ -319,7 +242,6 @@ private function getCommandFromJob(Job $job): string
}
}
- // Fallback to the job class
if (isset($payload['job'])) {
return $payload['job'];
}
diff --git a/src/Profiling/AbstractProfiler.php b/src/Profiling/AbstractProfiler.php
index bbcfeba..0a19a19 100644
--- a/src/Profiling/AbstractProfiler.php
+++ b/src/Profiling/AbstractProfiler.php
@@ -3,17 +3,17 @@
namespace Perfbase\Laravel\Profiling;
use Illuminate\Contracts\Container\BindingResolutionException;
-use JsonException;
-use Perfbase\Laravel\Caching\CacheStrategyFactory;
+use Perfbase\Laravel\Support\PerfbaseErrorHandling;
use Perfbase\SDK\Exception\PerfbaseException;
use Perfbase\SDK\Exception\PerfbaseExtensionException;
use Perfbase\SDK\Exception\PerfbaseInvalidSpanException;
use Perfbase\SDK\Perfbase as PerfbaseClient;
-use Perfbase\SDK\Utils\EnvironmentUtils;
use RuntimeException;
abstract class AbstractProfiler
{
+ use PerfbaseErrorHandling;
+
/** @var PerfbaseClient */
protected PerfbaseClient $perfbase;
@@ -52,7 +52,6 @@ private function getPerfbaseClient(): PerfbaseClient
/**
* Start profiling with the given context
*
- * @throws JsonException
* @throws PerfbaseExtensionException
* @throws PerfbaseInvalidSpanException
* @throws PerfbaseException
@@ -69,13 +68,15 @@ public function startProfiling(): void
}
/**
- * Stop profiling and handle the trace data
+ * Stop profiling and submit the trace.
+ *
+ * On submission failure, the error is logged but not re-thrown
+ * so profiling never disrupts the application.
*
* @throws PerfbaseException
*/
public function stopProfiling(): void
{
- // Apply attributes
foreach ($this->attributes as $key => $value) {
$this->perfbase->setAttribute($key, $value);
}
@@ -84,22 +85,17 @@ public function stopProfiling(): void
return;
}
- // Determine if we should send now or cache
- $sendingMode = config('perfbase.sending.mode');
- $shouldSendNow = $sendingMode === 'sync';
-
- if (!in_array($sendingMode, ['sync', 'database', 'file'], true)) {
- throw new RuntimeException('Invalid sending mode specified in the configuration.');
- }
-
- if ($shouldSendNow) {
- $this->perfbase->submitTrace();
- } else {
- $cache = CacheStrategyFactory::make();
- $cache->store([
- 'data' => $this->perfbase->getTraceData(),
- 'created_at' => now()->toDateTimeString(),
- ]);
+ $result = $this->perfbase->submitTrace();
+
+ if (!$result->isSuccess()) {
+ $this->handleProfilingError(
+ new PerfbaseException(sprintf(
+ 'Trace submission failed (%s): %s',
+ $result->getStatus(),
+ $result->getMessage()
+ )),
+ 'submit'
+ );
}
}
@@ -124,39 +120,21 @@ public function setAttributes(array $attributes): void
}
/**
- * Set default attributes that should be included in every trace
- *
- * @throws PerfbaseException
+ * Set default attributes that should be included in every trace.
+ * Subclasses should call parent and add context-specific attributes.
*/
protected function setDefaultAttributes(): void
{
$environment = config('app.env', '');
- if (!is_string($environment)) {
- throw new PerfbaseException('Config perfbase `app.env` must be a string.');
- }
-
$appVersion = config('app.version', '');
- if (!is_string($appVersion)) {
- throw new PerfbaseException('Config `app.version` must be a string.');
- }
-
- $hostname = gethostname();
- if (!is_string($hostname)) {
- $hostname = '';
- }
-
- $phpVersion = phpversion();
- if (!is_string($phpVersion)) {
- $phpVersion = '';
- }
+ $hostname = gethostname() ?: '';
+ $phpVersion = phpversion() ?: '';
$this->setAttributes([
'hostname' => $hostname,
'environment' => $environment,
'app_version' => $appVersion,
'php_version' => $phpVersion,
- 'user_ip' => EnvironmentUtils::getUserIp() ?? '',
- 'user_agent' => EnvironmentUtils::getUserUserAgent() ?? '',
]);
}
diff --git a/src/Profiling/ConsoleProfiler.php b/src/Profiling/ConsoleProfiler.php
deleted file mode 100644
index 987fd3a..0000000
--- a/src/Profiling/ConsoleProfiler.php
+++ /dev/null
@@ -1,135 +0,0 @@
-command = $command;
- $this->input = $input;
- $this->output = $output;
- }
-
- public function setExitCode(int $exitCode): void
- {
- $this->setAttribute('exit_code', (string)$exitCode);
- }
-
- /**
- * Check to see if we should profile the command.
- *
- * @return bool
- * @throws PerfbaseException
- */
- protected function shouldProfile(): bool
- {
- if (!config('perfbase.enabled', false)) {
- return false;
- }
-
- $commandName = $this->getCommandName();
-
- $includes = config('perfbase.include.console', []);
- if (!is_array($includes)) {
- throw new PerfbaseException('Configured perfbase console `includes` must be an array.');
- }
-
- if (!empty($includes) && !in_array($commandName, $includes, true)) {
- return false;
- }
-
- $excludes = config('perfbase.exclude.console', []);
- if (!is_array($excludes)) {
- throw new PerfbaseException('Configured perfbase console `excludes` must be an array.');
- }
-
- if (!empty($excludes) && in_array($commandName, $excludes, true)) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Set the default attributes for the console trace.
- *
- * @return void
- * @throws PerfbaseException
- */
- protected function setDefaultAttributes(): void
- {
- parent::setDefaultAttributes();
-
- // Add console specific attributes
- $this->setAttributes([
- 'source' => 'console',
- 'action' => $this->getCommandName(),
- 'arguments' => json_encode($this->input->getArguments()) ?: '',
- 'options' => json_encode($this->input->getOptions()) ?: '',
- 'verbosity' => $this->getVerbosityLevel(),
- ]);
- }
-
- /**
- * Return the name of the command being run.
- *
- * @return string
- */
- private function getCommandName(): string
- {
- if ($this->command instanceof Command) {
- return $this->command->getName() ?? '';
- }
- return $this->command;
- }
-
- /**
- * Return the verbosity level of the command.
- *
- * @return string
- */
- private function getVerbosityLevel(): string
- {
- $verbosity = $this->output->getVerbosity();
- switch ($verbosity) {
- case OutputInterface::VERBOSITY_QUIET:
- return 'quiet';
- case OutputInterface::VERBOSITY_NORMAL:
- return 'normal';
- case OutputInterface::VERBOSITY_VERBOSE:
- return 'verbose';
- case OutputInterface::VERBOSITY_VERY_VERBOSE:
- return 'very_verbose';
- case OutputInterface::VERBOSITY_DEBUG:
- return 'debug';
- default:
- return 'unknown';
- }
- }
-}
diff --git a/src/Profiling/HttpProfiler.php b/src/Profiling/HttpProfiler.php
deleted file mode 100644
index d3d2ede..0000000
--- a/src/Profiling/HttpProfiler.php
+++ /dev/null
@@ -1,226 +0,0 @@
-request = $request;
- }
-
- public function setResponse(Response $response): void
- {
- $this->setAttribute('http_status_code', (string)$response->getStatusCode());
- }
-
- /**
- * Determine if the current request should be profiled.
- * This is determined by four factors:
- * 1. Whether HTTP profiling is enabled in the configuration.
- * 2. Whether the user should be profiled (if applicable).
- * 3. Whether the requested route matches the include and exclude filters.
- * 4. Whether the Perfbase extension is loaded.
- *
- * @return bool
- */
- protected function shouldProfile(): bool
- {
- if (!config('perfbase.enabled', false)) {
- return false;
- }
-
-
- /** @var Authenticatable $user */
- $user = $this->request->user();
-
- // Check if the user should be profiled
- if (!$this->shouldUserBeProfiled($user)) {
- return false;
- }
-
- // Get request components and check against filters
- $components = $this->getRequestComponents();
- if (!$this->shouldRouteBeProfiled($components)) {
- return false;
- }
-
- // Finally, check if the extension is actually loaded.
- $extensionReady = $this->perfbase->isExtensionAvailable();
- if (!$extensionReady) {
- throw new RuntimeException('Profiling was requested, but the Perfbase extension is not loaded.');
- }
-
- return true;
- }
-
- protected function setDefaultAttributes(): void
- {
- parent::setDefaultAttributes();
-
- // Set route information if available
- $route = $this->request->route();
- $action = 'Unknown HTTP Action';
- if ($route instanceof Route) {
- $action = sprintf('%s %s', $this->request->method(), $route->uri());
- }
-
- // Add HTTP specific attributes
- $this->setAttributes([
- 'source' => 'http',
- 'http_method' => $this->request->method(),
- 'http_url' => $this->request->fullUrl(),
- 'action' => $action,
- ]);
-
- // Set user ID if authenticated
- if (Auth::check()) {
- $this->setAttribute('user', (string)Auth::id());
- }
- }
-
- /**
- * Check if the user should be profiled.
- * @param Authenticatable|null $user
- * @return bool
- */
- private function shouldUserBeProfiled(?Authenticatable $user): bool
- {
- // Check if the user is authenticated and implements the ProfiledUser interface
- if ($user && method_exists($user, 'shouldBeProfiled')) {
- return $user->shouldBeProfiled();
- }
-
- // If the user is not authenticated or doesn't implement the interface, return true
- return true;
- }
-
- /**
- * Get components related to the request (path, controller, method)
- * @return array
- */
- private function getRequestComponents(): array
- {
- $pathWithSlash = '/' . ltrim($this->request->path(), '/');
- $components = [
- sprintf("%s %s", $this->request->method(), $pathWithSlash),
- sprintf("%s %s", $this->request->method(), $this->request->path()),
- $this->request->path(),
- $pathWithSlash
- ];
-
- $route = $this->request->route();
- if ($route instanceof Route) {
- $explodedAction = explode('@', $route->getActionName());
- $components[] = $route->getActionName();
- $components[] = $route->uri();
- $components[] = '/' . ltrim($route->uri(), '/');
- $components[] = $explodedAction[0];
-
- foreach ($route->methods() as $method) {
- $components[] = sprintf("%s %s", $method, $route->uri());
- $components[] = sprintf("%s %s", $method, '/' . ltrim($route->uri(), '/'));
- }
- }
-
- return $components;
- }
-
- /**
- * Check if the route should be profiled based on include and exclude filters.
- * @param array $components
- * @return bool
- */
- private function shouldRouteBeProfiled(array $components): bool
- {
- return $this->matchesIncludeFilters($components)
- && !$this->matchesExcludeFilters($components);
- }
-
- /**
- * Check if any include filters match the request components.
- * @param array $components
- * @return bool
- */
- private function matchesIncludeFilters(array $components): bool
- {
-
- /** @var array $includes */
- $includes = config('perfbase.include.http', []);
-
- // Check if includes are an array
- if (!is_array($includes)) {
- throw new RuntimeException('Configured perfbase HTTP `includes` must be an array.');
- }
-
- // If no includes are set, no need to check further.
- if (empty($includes)) {
- return false;
- }
-
- return $this->matchesFilters($components, $includes);
- }
-
- /**
- * Check if any exclude filters match the request components.
- *
- * @param array $components
- * @return bool
- */
- private function matchesExcludeFilters(array $components): bool
- {
- $excludes = config('perfbase.exclude.http', []);
- if (!is_array($excludes)) {
- throw new RuntimeException('Configured perfbase HTTP `excludes` must be an array.');
- }
-
- return !empty($excludes) && $this->matchesFilters($components, $excludes);
- }
-
- /**
- * Simplified filter matching using Laravel's pattern matching
- *
- * @param array $components The list of components to be matched against the filters.
- * @param array $filters The list of filters to apply.
- * @return bool Returns true if any component matches any filter; otherwise, false.
- */
- public static function matchesFilters(array $components, array $filters): bool
- {
- return collect($filters)->some(function ($filter) use ($components) {
- // Handle match-all wildcard
- if ($filter === '*' || $filter === '.*') {
- return true;
- }
-
- // Handle regex patterns (enclosed in forward slashes)
- if (preg_match('/^\/.*\/$/', $filter)) {
- return collect($components)->some(fn($component) => preg_match($filter, $component));
- }
-
- // Use Laravel's string matching for all other patterns
- return collect($components)->some(fn($component) => Str::is($filter, $component));
- });
- }
-}
diff --git a/src/Profiling/QueueProfiler.php b/src/Profiling/QueueProfiler.php
deleted file mode 100644
index c9f894f..0000000
--- a/src/Profiling/QueueProfiler.php
+++ /dev/null
@@ -1,98 +0,0 @@
-job = $job;
- $this->jobName = $jobName;
- }
-
- /**
- * Set the exception message for the job.
- *
- * @param string $exception
- * @return void
- */
- public function setException(string $exception): void
- {
- $this->setAttribute('exception', $exception);
- }
-
- /**
- * Determine if the current context should be profiled
- *
- * @return bool
- * @throws PerfbaseException
- */
- protected function shouldProfile(): bool
- {
- if (!config('perfbase.enabled', false)) {
- return false;
- }
-
- $includes = config('perfbase.include.queue', []);
- if (!is_array($includes)) {
- throw new PerfbaseException('Configured perfbase queue `includes` must be an array.');
- }
-
- if (!empty($includes) && !in_array($this->jobName, $includes, true)) {
- return false;
- }
-
- $excludes = config('perfbase.exclude.queue', []);
- if (!is_array($excludes)) {
- throw new PerfbaseException('Configured perfbase queue `excludes` must be an array.');
- }
-
- if (!empty($excludes) && in_array($this->jobName, $excludes, true)) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Set default attributes that should be included in every trace
- *
- * @return void
- * @throws PerfbaseException
- */
- protected function setDefaultAttributes(): void
- {
- parent::setDefaultAttributes();
-
- // Add queue specific attributes
- $this->setAttributes([
- 'source' => 'queue',
- 'action' => $this->jobName,
- 'queue' => $this->job->getQueue(),
- 'attempts' => (string)($this->job->attempts() ?? 0),
- 'connection' => $this->job->getConnectionName(),
- 'job_id' => $this->job->getJobId(),
- ]);
- }
-}
diff --git a/src/Profiling/UniversalProfiler.php b/src/Profiling/UniversalProfiler.php
deleted file mode 100644
index b879965..0000000
--- a/src/Profiling/UniversalProfiler.php
+++ /dev/null
@@ -1,93 +0,0 @@
- */
- private array $context;
-
- /** @var callable|null */
- private $shouldProfileCallback;
-
- /**
- * @param string $type The type of profiling (http, console, queue, etc.)
- * @param array $context Context data for the profiling session
- * @param callable|null $shouldProfileCallback Optional custom logic for shouldProfile
- */
- public function __construct(string $type, array $context = [], ?callable $shouldProfileCallback = null)
- {
- parent::__construct($type);
- $this->context = $context;
- $this->shouldProfileCallback = $shouldProfileCallback;
-
- // Set context as attributes (convert non-string values to strings)
- $this->setAttributes($this->convertContextToAttributes($context));
- }
-
- /**
- * Determine if the current context should be profiled
- *
- * @return bool
- */
- protected function shouldProfile(): bool
- {
- // Use custom callback if provided
- if ($this->shouldProfileCallback) {
- return call_user_func($this->shouldProfileCallback, $this->context);
- }
-
- // Default: check if this type of profiling is enabled
- return config("perfbase.profile.{$this->spanName}", true);
- }
-
- /**
- * Get the context data
- *
- * @return array
- */
- public function getContext(): array
- {
- return $this->context;
- }
-
- /**
- * Add context data
- *
- * @param array $context
- * @return void
- */
- public function addContext(array $context): void
- {
- $this->context = array_merge($this->context, $context);
- $this->setAttributes($this->convertContextToAttributes($context));
- }
-
- /**
- * Convert context array to attributes (strings only)
- *
- * @param array $context
- * @return array
- */
- private function convertContextToAttributes(array $context): array
- {
- $attributes = [];
-
- foreach ($context as $key => $value) {
- if (is_string($value)) {
- $attributes[$key] = $value;
- } elseif (is_scalar($value)) {
- $attributes[$key] = (string) $value;
- } elseif (is_array($value)) {
- $attributes[$key] = json_encode($value) ?: '[]';
- } else {
- $attributes[$key] = serialize($value) ?: '';
- }
- }
-
- return $attributes;
- }
-}
\ No newline at end of file
diff --git a/src/Support/FilterMatcher.php b/src/Support/FilterMatcher.php
new file mode 100644
index 0000000..3da2749
--- /dev/null
+++ b/src/Support/FilterMatcher.php
@@ -0,0 +1,76 @@
+ $components Values to test (e.g. route path, job name, command name)
+ * @param array $filters Patterns to match against
+ * @return bool
+ */
+ public static function matches(array $components, array $filters): bool
+ {
+ foreach ($filters as $filter) {
+ if ($filter === '*' || $filter === '.*') {
+ return true;
+ }
+
+ // Regex patterns enclosed in forward slashes
+ if (preg_match('/^\/.*\/$/', $filter)) {
+ foreach ($components as $component) {
+ if (preg_match($filter, $component)) {
+ return true;
+ }
+ }
+ continue;
+ }
+
+ // Laravel string matching for everything else
+ foreach ($components as $component) {
+ if (Str::is($filter, $component)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if a value passes include/exclude filters from config.
+ *
+ * @param array $components Values to test
+ * @param string $configKey Config key prefix (e.g. 'http', 'console', 'queue')
+ * @return bool
+ */
+ public static function passesConfigFilters(array $components, string $configKey): bool
+ {
+ /** @var array $includes */
+ $includes = config("perfbase.include.{$configKey}", []);
+ if (!is_array($includes) || empty($includes)) {
+ return false;
+ }
+
+ if (!self::matches($components, $includes)) {
+ return false;
+ }
+
+ /** @var array $excludes */
+ $excludes = config("perfbase.exclude.{$configKey}", []);
+ if (is_array($excludes) && !empty($excludes) && self::matches($components, $excludes)) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/Support/PerfbaseConfig.php b/src/Support/PerfbaseConfig.php
index 098d599..b6f3864 100644
--- a/src/Support/PerfbaseConfig.php
+++ b/src/Support/PerfbaseConfig.php
@@ -46,16 +46,6 @@ public static function sampleRate(): float
return self::get('sample_rate', 1.0);
}
- /**
- * Get the sending mode
- *
- * @return string
- */
- public static function sendingMode(): string
- {
- return self::get('sending.mode', 'sync');
- }
-
/**
* Clear the cache (useful for testing)
*
diff --git a/tests/AbstractProfilerTest.php b/tests/AbstractProfilerTest.php
index f4c52ed..6ac6eec 100644
--- a/tests/AbstractProfilerTest.php
+++ b/tests/AbstractProfilerTest.php
@@ -4,13 +4,12 @@
require_once __DIR__ . '/TestHelpers.php';
-use Illuminate\Support\Facades\File;
use Orchestra\Testbench\TestCase;
-use Perfbase\Laravel\Caching\FileStrategy;
use Perfbase\Laravel\Profiling\AbstractProfiler;
use Perfbase\Laravel\PerfbaseServiceProvider;
use Perfbase\SDK\Config;
use Perfbase\SDK\Perfbase as PerfbaseClient;
+use Perfbase\SDK\SubmitResult;
use RuntimeException;
use ReflectionClass;
use Mockery;
@@ -29,7 +28,6 @@ class AbstractProfilerTest extends TestCase
private ConcreteProfiler $profiler;
private ReflectionClass $reflection;
private $perfbaseClient;
- private string $testPath;
protected function getPackageProviders($app): array
{
@@ -47,30 +45,19 @@ protected function setUp(): void
$this->perfbaseClient->allows('startTraceSpan')->andReturns(true);
$this->perfbaseClient->allows('stopTraceSpan')->andReturns(true);
$this->perfbaseClient->allows('setAttribute')->andReturns(true);
- $this->perfbaseClient->allows('submitTrace')->andReturns(true);
+ $this->perfbaseClient->allows('submitTrace')->andReturns(SubmitResult::success());
$this->perfbaseClient->allows('getTraceData')->andReturns('serialized_trace_data');
$this->perfbaseClient->allows('reset')->andReturns(true);
$this->app->instance(Config::class, $config);
$this->app->instance(PerfbaseClient::class, $this->perfbaseClient);
-
- // Set up file path for cache tests
- $this->testPath = storage_path('testing/perfbase');
-
+
// Set up basic config
config([
'perfbase' => [
'enabled' => true,
'api_key' => 'test-key',
'sample_rate' => 1.0,
- 'sending' => [
- 'mode' => 'sync',
- 'config' => [
- 'file' => [
- 'path' => $this->testPath
- ]
- ]
- ],
],
'app' => [
'env' => 'testing',
@@ -84,10 +71,6 @@ protected function setUp(): void
protected function tearDown(): void
{
- if (File::exists($this->testPath)) {
- File::deleteDirectory($this->testPath);
- }
-
Mockery::close();
parent::tearDown();
}
@@ -168,16 +151,18 @@ public function testPassesSampleRateCheckThrowsExceptionWithNonNumeric()
public function testSetDefaultAttributes()
{
$this->callPrivateMethod('setDefaultAttributes');
-
+
$attributes = $this->getPrivateProperty('attributes');
-
+
$this->assertArrayHasKey('hostname', $attributes);
$this->assertArrayHasKey('environment', $attributes);
$this->assertArrayHasKey('app_version', $attributes);
$this->assertArrayHasKey('php_version', $attributes);
- $this->assertArrayHasKey('user_ip', $attributes);
- $this->assertArrayHasKey('user_agent', $attributes);
-
+
+ // user_ip and user_agent are HTTP-only, not in base defaults
+ $this->assertArrayNotHasKey('user_ip', $attributes);
+ $this->assertArrayNotHasKey('user_agent', $attributes);
+
$this->assertEquals('testing', $attributes['environment']);
$this->assertEquals('1.0.0', $attributes['app_version']);
$this->assertEquals(phpversion(), $attributes['php_version']);
@@ -211,57 +196,82 @@ protected function shouldProfile(): bool
$this->assertTrue(true); // Explicit assertion
}
- public function testStopProfilingWithSyncMode()
+ public function testStopProfilingSubmitsTrace()
{
- config(['perfbase.sending.mode' => 'sync']);
-
// Just test that the method can be called without errors
$this->profiler->setAttribute('test', 'value');
$this->profiler->stopProfiling();
-
- // Verify config was set correctly
- $this->assertEquals('sync', config('perfbase.sending.mode'));
+
$this->assertTrue(true);
}
- public function testStopProfilingWithFileMode()
+ public function testStopProfilingWhenSpanNotStarted()
{
- config(['perfbase.sending.mode' => 'file']);
-
- // Mock File facade to avoid actual file operations
- File::shouldReceive('exists')
- ->andReturn(false);
- File::shouldReceive('makeDirectory')
- ->andReturn(true);
- File::shouldReceive('put')
- ->andReturn(true);
-
- $this->profiler->stopProfiling();
-
- // Verify config was set correctly
- $this->assertEquals('file', config('perfbase.sending.mode'));
+ // Create a client mock where stopTraceSpan returns false
+ $client = Mockery::mock(PerfbaseClient::class);
+ $client->allows('isExtensionAvailable')->andReturns(true);
+ $client->allows('startTraceSpan');
+ $client->allows('stopTraceSpan')->andReturns(false);
+ $client->allows('setAttribute');
+ $client->allows('reset');
+ // submitTrace should NOT be called
+ $client->shouldNotReceive('submitTrace');
+
+ $this->app->instance(PerfbaseClient::class, $client);
+
+ $profiler = new ConcreteProfiler('not_started');
+ $profiler->stopProfiling();
+
$this->assertTrue(true);
}
- public function testStopProfilingWithInvalidSendingMode()
+ public function testStopProfilingLogsOnSubmitFailure()
{
- config(['perfbase.sending.mode' => 'invalid']);
-
- // Test that invalid mode is set
- $this->assertEquals('invalid', config('perfbase.sending.mode'));
-
- // Expect the RuntimeException for invalid sending mode
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Invalid sending mode specified in the configuration.');
-
- $this->profiler->stopProfiling();
+ // Override mock to return failure
+ $failClient = Mockery::mock(PerfbaseClient::class);
+ $failClient->allows('isExtensionAvailable')->andReturns(true);
+ $failClient->allows('startTraceSpan');
+ $failClient->allows('stopTraceSpan')->andReturns(true);
+ $failClient->allows('setAttribute');
+ $failClient->allows('submitTrace')->andReturns(
+ SubmitResult::retryableFailure(503, 'Service Unavailable')
+ );
+ $failClient->allows('reset');
+
+ $this->app->instance(PerfbaseClient::class, $failClient);
+
+ config(['perfbase.debug' => false, 'perfbase.log_errors' => false]);
+
+ $profiler = new ConcreteProfiler('fail_span');
+ // Should not throw even though submission failed
+ $profiler->stopProfiling();
+
+ $this->assertTrue(true);
}
- public function testStopProfilingWhenSpanNotStarted()
+ public function testStopProfilingThrowsInDebugModeOnFailure()
{
- // Just test that method can be called
- $this->profiler->stopProfiling();
- $this->assertTrue(true);
+ $failClient = Mockery::mock(PerfbaseClient::class);
+ $failClient->allows('isExtensionAvailable')->andReturns(true);
+ $failClient->allows('startTraceSpan');
+ $failClient->allows('stopTraceSpan')->andReturns(true);
+ $failClient->allows('setAttribute');
+ $failClient->allows('submitTrace')->andReturns(
+ SubmitResult::permanentFailure(401, 'Unauthorized')
+ );
+ $failClient->allows('reset');
+
+ $this->app->instance(PerfbaseClient::class, $failClient);
+
+ config(['perfbase.debug' => true]);
+ \Perfbase\Laravel\Support\PerfbaseConfig::clearCache();
+
+ $profiler = new ConcreteProfiler('debug_span');
+
+ $this->expectException(\Perfbase\SDK\Exception\PerfbaseException::class);
+ $this->expectExceptionMessage('Trace submission failed');
+
+ $profiler->stopProfiling();
}
private function callPrivateMethod(string $methodName, array $args = [])
diff --git a/tests/CacheStrategyFactoryTest.php b/tests/CacheStrategyFactoryTest.php
deleted file mode 100644
index d1ef68b..0000000
--- a/tests/CacheStrategyFactoryTest.php
+++ /dev/null
@@ -1,66 +0,0 @@
- 'database']);
-
- $strategy = CacheStrategyFactory::make();
-
- $this->assertInstanceOf(DatabaseStrategy::class, $strategy);
- }
-
- public function testMakeFileStrategy()
- {
- config(['perfbase.sending.mode' => 'file']);
-
- $strategy = CacheStrategyFactory::make();
-
- $this->assertInstanceOf(FileStrategy::class, $strategy);
- }
-
- public function testMakeInvalidStrategyThrowsException()
- {
- config(['perfbase.sending.mode' => 'invalid']);
-
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Invalid cache strategy');
-
- CacheStrategyFactory::make();
- }
-
- public function testMakeWithSyncModeThrowsException()
- {
- config(['perfbase.sending.mode' => 'sync']);
-
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Invalid cache strategy');
-
- CacheStrategyFactory::make();
- }
-
- public function testMakeWithNullModeThrowsException()
- {
- config(['perfbase.sending.mode' => null]);
-
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Invalid cache strategy');
-
- CacheStrategyFactory::make();
- }
-}
\ No newline at end of file
diff --git a/tests/ConsoleProfilerTest.php b/tests/ConsoleProfilerTest.php
deleted file mode 100644
index 6b3bb7b..0000000
--- a/tests/ConsoleProfilerTest.php
+++ /dev/null
@@ -1,153 +0,0 @@
-allows('isExtensionAvailable')->andReturns(true);
- $perfbaseClient->allows('startTraceSpan')->andReturns(true);
- $perfbaseClient->allows('stopTraceSpan')->andReturns(true);
- $perfbaseClient->allows('setAttribute')->andReturns(true);
- $perfbaseClient->allows('submitTrace')->andReturns(true);
- $perfbaseClient->allows('getTraceData')->andReturns('test-data');
- $perfbaseClient->allows('reset')->andReturns(true);
- $this->app->instance(Config::class, $config);
- $this->app->instance(PerfbaseClient::class, $perfbaseClient);
-
- // Set up basic config values needed for the test
- config([
- 'perfbase' => [
- 'enabled' => true,
- 'api_key' => 'test-key',
- 'sample_rate' => 1.0,
- 'sending' => [
- 'mode' => 'sync',
- 'timeout' => 5,
- ],
- 'include' => [
- 'console' => [],
- ],
- 'exclude' => [
- 'console' => [],
- ],
- ]
- ]);
-
- $this->command = new class extends Command {
- protected $signature = 'test:command {arg} {--option=}';
- public function handle() {}
- };
-
- $this->input = new ArrayInput(['arg' => 'value', '--option' => 'opt']);
- $this->output = new ConsoleOutput();
- $this->output->setVerbosity(OutputInterface::VERBOSITY_NORMAL);
-
- $this->profiler = new ConsoleProfiler(
- $this->command,
- $this->input,
- $this->output
- );
- $this->reflection = new ReflectionClass(ConsoleProfiler::class);
- }
-
- protected function tearDown(): void
- {
- Mockery::close();
- parent::tearDown();
- }
-
- public function testConstructor()
- {
- $this->assertEquals('console', $this->getPrivateProperty('spanName'));
- }
-
- public function testSetExitCode()
- {
- $this->profiler->setExitCode(1);
- $this->assertEquals('1', $this->getPrivateProperty('attributes')['exit_code']);
- }
-
- public function testShouldProfileWhenDisabled()
- {
- config(['perfbase.enabled' => false]);
- $this->assertFalse($this->callPrivateMethod('shouldProfile'));
- }
-
- public function testShouldProfileWhenEnabled()
- {
- config(['perfbase.enabled' => true]);
- config(['perfbase.include.console' => ['test:command']]);
- config(['perfbase.exclude.console' => []]);
-
- $this->assertTrue($this->callPrivateMethod('shouldProfile'));
- }
-
- public function testGetCommandName()
- {
- $this->assertEquals('test:command', $this->callPrivateMethod('getCommandName'));
- }
-
- public function testGetVerbosityLevel()
- {
- $this->assertEquals('normal', $this->callPrivateMethod('getVerbosityLevel'));
- }
-
- public function testSetDefaultAttributes()
- {
- $this->callPrivateMethod('setDefaultAttributes');
- $attributes = $this->getPrivateProperty('attributes');
-
- $this->assertEquals('test:command', $attributes['action']);
- $this->assertArrayHasKey('arguments', $attributes);
- $this->assertArrayHasKey('options', $attributes);
- $this->assertEquals('normal', $attributes['verbosity']);
- }
-
- private function callPrivateMethod(string $methodName, array $args = [])
- {
- $method = $this->reflection->getMethod($methodName);
- $method->setAccessible(true);
- return $method->invokeArgs($this->profiler, $args);
- }
-
- private function getPrivateProperty(string $propertyName)
- {
- $property = $this->reflection->getProperty($propertyName);
- $property->setAccessible(true);
- return $property->getValue($this->profiler);
- }
-}
diff --git a/tests/ConsoleTraceLifecycleTest.php b/tests/ConsoleTraceLifecycleTest.php
new file mode 100644
index 0000000..5ce9652
--- /dev/null
+++ b/tests/ConsoleTraceLifecycleTest.php
@@ -0,0 +1,167 @@
+perfbaseClient = Mockery::mock(PerfbaseClient::class);
+ $this->perfbaseClient->allows('isExtensionAvailable')->andReturns(true);
+ $this->perfbaseClient->allows('startTraceSpan');
+ $this->perfbaseClient->allows('stopTraceSpan')->andReturns(true);
+ $this->perfbaseClient->allows('setAttribute');
+ $this->perfbaseClient->allows('submitTrace')->andReturns(SubmitResult::success());
+ $this->perfbaseClient->allows('reset');
+
+ $this->app->instance(Config::class, Mockery::mock(Config::class));
+ $this->app->instance(PerfbaseClient::class, $this->perfbaseClient);
+
+ config([
+ 'perfbase' => [
+ 'enabled' => true,
+ 'sample_rate' => 1.0,
+ 'include' => ['console' => ['*']],
+ 'exclude' => ['console' => []],
+ ],
+ 'app' => ['env' => 'production', 'version' => '3.0.0'],
+ ]);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ parent::tearDown();
+ }
+
+ public function testSetsConsoleAttributes(): void
+ {
+ $lifecycle = new ConsoleTraceLifecycle('migrate');
+ $lifecycle->startProfiling();
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertSame('console', $attrs['source']);
+ $this->assertSame('migrate', $attrs['action']);
+ }
+
+ public function testSetsBaseAttributes(): void
+ {
+ $lifecycle = new ConsoleTraceLifecycle('db:seed');
+ $lifecycle->startProfiling();
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertArrayHasKey('hostname', $attrs);
+ $this->assertSame('production', $attrs['environment']);
+ $this->assertSame('3.0.0', $attrs['app_version']);
+ $this->assertArrayHasKey('php_version', $attrs);
+ }
+
+ public function testDoesNotSetHttpAttributes(): void
+ {
+ $lifecycle = new ConsoleTraceLifecycle('migrate');
+ $lifecycle->startProfiling();
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertArrayNotHasKey('user_ip', $attrs);
+ $this->assertArrayNotHasKey('user_agent', $attrs);
+ $this->assertArrayNotHasKey('http_method', $attrs);
+ $this->assertArrayNotHasKey('http_url', $attrs);
+ }
+
+ public function testShouldProfileReturnsTrueWhenIncluded(): void
+ {
+ config(['perfbase.include.console' => ['*']]);
+
+ $lifecycle = new ConsoleTraceLifecycle('migrate');
+ $this->assertTrue($this->callShouldProfile($lifecycle));
+ }
+
+ public function testShouldProfileReturnsFalseWhenNotIncluded(): void
+ {
+ config(['perfbase.include.console' => ['migrate*']]);
+
+ $lifecycle = new ConsoleTraceLifecycle('queue:work');
+ $this->assertFalse($this->callShouldProfile($lifecycle));
+ }
+
+ public function testShouldProfileReturnsFalseWhenExcluded(): void
+ {
+ config([
+ 'perfbase.include.console' => ['*'],
+ 'perfbase.exclude.console' => ['queue:work'],
+ ]);
+
+ $lifecycle = new ConsoleTraceLifecycle('queue:work');
+ $this->assertFalse($this->callShouldProfile($lifecycle));
+ }
+
+ public function testSpanName(): void
+ {
+ $lifecycle = new ConsoleTraceLifecycle('migrate:fresh');
+
+ $reflection = new ReflectionClass($lifecycle);
+ $prop = $reflection->getProperty('spanName');
+ $prop->setAccessible(true);
+
+ $this->assertSame('console.migrate:fresh', $prop->getValue($lifecycle));
+ }
+
+ public function testSetExitCode(): void
+ {
+ $lifecycle = new ConsoleTraceLifecycle('migrate');
+ $lifecycle->setExitCode(1);
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertSame('1', $attrs['exit_code']);
+ }
+
+ public function testSetExitCodeZero(): void
+ {
+ $lifecycle = new ConsoleTraceLifecycle('migrate');
+ $lifecycle->setExitCode(0);
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertSame('0', $attrs['exit_code']);
+ }
+
+ /**
+ * @return array
+ */
+ private function getAttributes(ConsoleTraceLifecycle $lifecycle): array
+ {
+ $reflection = new ReflectionClass($lifecycle);
+ $prop = $reflection->getProperty('attributes');
+ $prop->setAccessible(true);
+ return $prop->getValue($lifecycle);
+ }
+
+ private function callShouldProfile(ConsoleTraceLifecycle $lifecycle): bool
+ {
+ $reflection = new ReflectionClass($lifecycle);
+ $method = $reflection->getMethod('shouldProfile');
+ $method->setAccessible(true);
+ return $method->invoke($lifecycle);
+ }
+}
diff --git a/tests/ErrorHandlingTest.php b/tests/ErrorHandlingTest.php
new file mode 100644
index 0000000..3fd83a7
--- /dev/null
+++ b/tests/ErrorHandlingTest.php
@@ -0,0 +1,162 @@
+handleExtensionError($e);
+ }
+
+ public function triggerProfilingError(\Throwable $e, string $context = ''): void
+ {
+ $this->handleProfilingError($e, $context);
+ }
+}
+
+class ErrorHandlingTest extends TestCase
+{
+ protected function getPackageProviders($app): array
+ {
+ return [PerfbaseServiceProvider::class];
+ }
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ PerfbaseConfig::clearCache();
+ }
+
+ protected function tearDown(): void
+ {
+ PerfbaseConfig::clearCache();
+ parent::tearDown();
+ }
+
+ public function testDebugModeRethrowsExtensionError(): void
+ {
+ config(['perfbase.debug' => true]);
+ PerfbaseConfig::clearCache();
+
+ $subject = new ErrorHandlingTestSubject();
+ $exception = new \RuntimeException('Extension broke');
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Extension broke');
+
+ $subject->triggerExtensionError($exception);
+ }
+
+ public function testDebugModeRethrowsProfilingError(): void
+ {
+ config(['perfbase.debug' => true]);
+ PerfbaseConfig::clearCache();
+
+ $subject = new ErrorHandlingTestSubject();
+ $exception = new \RuntimeException('Profiling broke');
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Profiling broke');
+
+ $subject->triggerProfilingError($exception, 'submit');
+ }
+
+ public function testProductionModeSilencesExtensionError(): void
+ {
+ config([
+ 'perfbase.debug' => false,
+ 'perfbase.log_errors' => false,
+ ]);
+ PerfbaseConfig::clearCache();
+
+ $subject = new ErrorHandlingTestSubject();
+ $exception = new \RuntimeException('Should be silenced');
+
+ // Should not throw
+ $subject->triggerExtensionError($exception);
+ $this->assertTrue(true);
+ }
+
+ public function testProductionModeSilencesProfilingError(): void
+ {
+ config([
+ 'perfbase.debug' => false,
+ 'perfbase.log_errors' => false,
+ ]);
+ PerfbaseConfig::clearCache();
+
+ $subject = new ErrorHandlingTestSubject();
+ $exception = new \RuntimeException('Should be silenced');
+
+ $subject->triggerProfilingError($exception, 'event_start');
+ $this->assertTrue(true);
+ }
+
+ public function testDefaultDebugIsFalse(): void
+ {
+ // Don't set debug — default should be false (no rethrow)
+ config(['perfbase.log_errors' => false]);
+ PerfbaseConfig::clearCache();
+
+ $subject = new ErrorHandlingTestSubject();
+ $exception = new \RuntimeException('Should not throw');
+
+ $subject->triggerExtensionError($exception);
+ $this->assertTrue(true);
+ }
+
+ public function testLoggingModeLogsExtensionError(): void
+ {
+ config([
+ 'perfbase.debug' => false,
+ 'perfbase.log_errors' => true,
+ ]);
+ PerfbaseConfig::clearCache();
+
+ $subject = new ErrorHandlingTestSubject();
+ $exception = new \RuntimeException('Logged extension error');
+
+ // Should not throw, but should attempt to log
+ $subject->triggerExtensionError($exception);
+ $this->assertTrue(true);
+ }
+
+ public function testLoggingModeLogsProfilingError(): void
+ {
+ config([
+ 'perfbase.debug' => false,
+ 'perfbase.log_errors' => true,
+ ]);
+ PerfbaseConfig::clearCache();
+
+ $subject = new ErrorHandlingTestSubject();
+ $exception = new \RuntimeException('Logged profiling error');
+
+ $subject->triggerProfilingError($exception, 'event_start');
+ $this->assertTrue(true);
+ }
+
+ public function testLoggingDisabledSkipsLogger(): void
+ {
+ config([
+ 'perfbase.debug' => false,
+ 'perfbase.log_errors' => false,
+ ]);
+ PerfbaseConfig::clearCache();
+
+ $subject = new ErrorHandlingTestSubject();
+ $exception = new \RuntimeException('Not logged');
+
+ // Should not throw and should not attempt to log
+ $subject->triggerProfilingError($exception, 'event_stop');
+ $this->assertTrue(true);
+ }
+}
diff --git a/tests/EventFlowTest.php b/tests/EventFlowTest.php
new file mode 100644
index 0000000..a49b727
--- /dev/null
+++ b/tests/EventFlowTest.php
@@ -0,0 +1,284 @@
+ stop -> submit through the actual event flow.
+ */
+class EventFlowTest extends TestCase
+{
+ private $perfbaseClient;
+
+ protected function getPackageProviders($app): array
+ {
+ return [PerfbaseServiceProvider::class];
+ }
+
+ /**
+ * Set config BEFORE the service provider boots so event listeners are registered.
+ */
+ protected function defineEnvironment($app): void
+ {
+ // Clear static cache before boot so prior test state doesn't leak
+ PerfbaseConfig::clearCache();
+
+ $app['config']->set('perfbase.enabled', true);
+ $app['config']->set('perfbase.api_key', 'test-key');
+ $app['config']->set('perfbase.sample_rate', 1.0);
+ $app['config']->set('perfbase.flags', 0);
+ $app['config']->set('perfbase.proxy', null);
+ $app['config']->set('perfbase.timeout', 5);
+ $app['config']->set('perfbase.include.http', ['*']);
+ $app['config']->set('perfbase.include.console', ['*']);
+ $app['config']->set('perfbase.include.queue', ['*']);
+ $app['config']->set('perfbase.exclude.http', []);
+ $app['config']->set('perfbase.exclude.console', []);
+ $app['config']->set('perfbase.exclude.queue', []);
+ $app['config']->set('app.env', 'testing');
+ $app['config']->set('app.version', '1.0.0');
+ }
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ PerfbaseConfig::clearCache();
+
+ $mockExtension = Mockery::mock(ExtensionInterface::class);
+ $mockExtension->shouldReceive('isAvailable')->andReturn(true);
+ $mockExtension->shouldReceive('startSpan')->andReturn();
+ $mockExtension->shouldReceive('stopSpan')->andReturn();
+ $mockExtension->shouldReceive('getSpanData')->andReturn('{}');
+ $mockExtension->shouldReceive('reset')->andReturn();
+ $mockExtension->shouldReceive('setAttribute')->andReturn();
+
+ $this->app->instance(ExtensionInterface::class, $mockExtension);
+
+ $this->perfbaseClient = Mockery::mock(PerfbaseClient::class);
+ $this->perfbaseClient->allows('isExtensionAvailable')->andReturns(true);
+ $this->perfbaseClient->allows('startTraceSpan');
+ $this->perfbaseClient->allows('stopTraceSpan')->andReturns(true);
+ $this->perfbaseClient->allows('setAttribute');
+ $this->perfbaseClient->allows('submitTrace')->andReturns(SubmitResult::success());
+ $this->perfbaseClient->allows('reset');
+
+ $this->app->instance(Config::class, Mockery::mock(Config::class));
+ $this->app->instance(PerfbaseClient::class, $this->perfbaseClient);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ parent::tearDown();
+ }
+
+ // ---------------------------------------------------------------
+ // Queue event flow
+ // ---------------------------------------------------------------
+
+ public function testQueueJobProfilingThroughEvents(): void
+ {
+ $job = $this->createMockJob('App\Jobs\SendEmail');
+
+ Event::dispatch(new JobProcessing('database', $job));
+ Event::dispatch(new JobProcessed('database', $job));
+
+ $this->perfbaseClient->shouldHaveReceived('startTraceSpan')->once();
+ $this->perfbaseClient->shouldHaveReceived('stopTraceSpan')->once();
+ $this->perfbaseClient->shouldHaveReceived('submitTrace')->once();
+ $this->assertTrue(true);
+ }
+
+ public function testQueueJobExceptionStillSubmits(): void
+ {
+ $job = $this->createMockJob('App\Jobs\FailingJob');
+ $exception = new \RuntimeException('Job failed');
+
+ Event::dispatch(new JobProcessing('database', $job));
+ Event::dispatch(new JobExceptionOccurred('database', $job, $exception));
+
+ $this->perfbaseClient->shouldHaveReceived('startTraceSpan')->once();
+ $this->perfbaseClient->shouldHaveReceived('stopTraceSpan')->once();
+ $this->perfbaseClient->shouldHaveReceived('submitTrace')->once();
+ $this->perfbaseClient->shouldHaveReceived('setAttribute')
+ ->with('exception', 'Job failed');
+ $this->assertTrue(true);
+ }
+
+ // ---------------------------------------------------------------
+ // Console event flow
+ // ---------------------------------------------------------------
+
+ public function testConsoleCommandProfilingThroughEvents(): void
+ {
+ $input = new ArrayInput([]);
+ $output = new NullOutput();
+
+ Event::dispatch(new CommandStarting('migrate', $input, $output));
+ Event::dispatch(new CommandFinished('migrate', $input, $output, 0));
+
+ $this->perfbaseClient->shouldHaveReceived('startTraceSpan')->once();
+ $this->perfbaseClient->shouldHaveReceived('stopTraceSpan')->once();
+ $this->perfbaseClient->shouldHaveReceived('submitTrace')->once();
+ $this->assertTrue(true);
+ }
+
+ public function testConsoleCommandExitCodeIsRecorded(): void
+ {
+ $input = new ArrayInput([]);
+ $output = new NullOutput();
+
+ Event::dispatch(new CommandStarting('migrate', $input, $output));
+ Event::dispatch(new CommandFinished('migrate', $input, $output, 1));
+
+ $this->perfbaseClient->shouldHaveReceived('setAttribute')
+ ->with('exit_code', '1');
+ $this->assertTrue(true);
+ }
+
+ public function testNullCommandIsIgnored(): void
+ {
+ $event = new CommandStarting(null, new ArrayInput([]), new NullOutput());
+ Event::dispatch($event);
+
+ $this->perfbaseClient->shouldNotHaveReceived('startTraceSpan');
+ $this->assertTrue(true);
+ }
+
+ // ---------------------------------------------------------------
+ // Sample rate
+ // ---------------------------------------------------------------
+
+ public function testSampleRateZeroSkipsStartProfiling(): void
+ {
+ config(['perfbase.sample_rate' => 0.0]);
+
+ $job = $this->createMockJob('App\Jobs\SomeJob');
+ Event::dispatch(new JobProcessing('database', $job));
+ Event::dispatch(new JobProcessed('database', $job));
+
+ // startTraceSpan should never be called because sample rate check fails
+ $this->perfbaseClient->shouldNotHaveReceived('startTraceSpan');
+ $this->assertTrue(true);
+ }
+
+ // ---------------------------------------------------------------
+ // Multiple concurrent jobs
+ // ---------------------------------------------------------------
+
+ public function testTwoConcurrentJobsTrackedIndependently(): void
+ {
+ $job1 = $this->createMockJob('App\Jobs\JobA', 'job-1');
+ $job2 = $this->createMockJob('App\Jobs\JobB', 'job-2');
+
+ // Start both
+ Event::dispatch(new JobProcessing('database', $job1));
+ Event::dispatch(new JobProcessing('database', $job2));
+
+ // Finish in reverse order
+ Event::dispatch(new JobProcessed('database', $job2));
+ Event::dispatch(new JobProcessed('database', $job1));
+
+ // Both should have been profiled
+ $this->perfbaseClient->shouldHaveReceived('startTraceSpan')->twice();
+ $this->perfbaseClient->shouldHaveReceived('stopTraceSpan')->twice();
+ $this->perfbaseClient->shouldHaveReceived('submitTrace')->twice();
+ $this->assertTrue(true);
+ }
+
+ // ---------------------------------------------------------------
+ // Helpers
+ // ---------------------------------------------------------------
+
+ // ---------------------------------------------------------------
+ // Job name fallbacks
+ // ---------------------------------------------------------------
+
+ public function testJobNameFallsBackToCommandName(): void
+ {
+ $job = Mockery::mock(Job::class);
+ $job->shouldReceive('payload')->andReturn([
+ 'data' => ['commandName' => 'App\Jobs\ViaCommandName'],
+ ]);
+ $job->shouldReceive('getQueue')->andReturn('default');
+ $job->shouldReceive('getJobId')->andReturn('fallback-1');
+
+ Event::dispatch(new JobProcessing('database', $job));
+ Event::dispatch(new JobProcessed('database', $job));
+
+ $this->perfbaseClient->shouldHaveReceived('startTraceSpan')->once();
+ $this->assertTrue(true);
+ }
+
+ public function testJobNameFallsBackToPayloadJob(): void
+ {
+ $job = Mockery::mock(Job::class);
+ $job->shouldReceive('payload')->andReturn([
+ 'job' => 'App\Jobs\ViaPayloadJob',
+ 'data' => [],
+ ]);
+ $job->shouldReceive('getQueue')->andReturn('default');
+ $job->shouldReceive('getJobId')->andReturn('fallback-2');
+
+ Event::dispatch(new JobProcessing('database', $job));
+ Event::dispatch(new JobProcessed('database', $job));
+
+ $this->perfbaseClient->shouldHaveReceived('startTraceSpan')->once();
+ $this->assertTrue(true);
+ }
+
+ public function testJobNameFallsBackToUnknown(): void
+ {
+ $job = Mockery::mock(Job::class);
+ $job->shouldReceive('payload')->andReturn(['data' => []]);
+ $job->shouldReceive('getQueue')->andReturn('default');
+ $job->shouldReceive('getJobId')->andReturn('fallback-3');
+
+ Event::dispatch(new JobProcessing('database', $job));
+ Event::dispatch(new JobProcessed('database', $job));
+
+ $this->perfbaseClient->shouldHaveReceived('startTraceSpan')->once();
+ $this->assertTrue(true);
+ }
+
+ // ---------------------------------------------------------------
+ // Helpers
+ // ---------------------------------------------------------------
+
+ /**
+ * @return Job&\Mockery\MockInterface
+ */
+ private function createMockJob(string $displayName, string $jobId = '1'): Job
+ {
+ $job = Mockery::mock(Job::class);
+ $job->shouldReceive('payload')->andReturn([
+ 'displayName' => $displayName,
+ 'data' => ['commandName' => $displayName],
+ ]);
+ $job->shouldReceive('getQueue')->andReturn('default');
+ $job->shouldReceive('getJobId')->andReturn($jobId);
+ return $job;
+ }
+}
diff --git a/tests/FileStrategyTest.php b/tests/FileStrategyTest.php
deleted file mode 100644
index a63fca1..0000000
--- a/tests/FileStrategyTest.php
+++ /dev/null
@@ -1,221 +0,0 @@
-testPath = storage_path('testing/perfbase');
-
- // Set up file configuration
- config([
- 'perfbase.sending.config.file.path' => $this->testPath
- ]);
-
- // Clean up and create test directory
- if (File::exists($this->testPath)) {
- File::deleteDirectory($this->testPath);
- }
- File::makeDirectory($this->testPath, 0755, true);
-
- $this->strategy = new FileStrategy();
- }
-
- protected function tearDown(): void
- {
- // Clean up test directory
- if (File::exists($this->testPath)) {
- File::deleteDirectory($this->testPath);
- }
-
- parent::tearDown();
- }
-
- public function testConstructorThrowsExceptionWithInvalidPath()
- {
- config(['perfbase.sending.config.file.path' => null]);
-
- $this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('The file cache path must be a string');
-
- new FileStrategy();
- }
-
- public function testStore()
- {
- $profileData = ['trace' => 'test_data', 'timestamp' => time()];
-
- $this->strategy->store($profileData);
-
- $files = File::files($this->testPath);
- $this->assertCount(1, $files);
-
- $file = $files[0];
- $this->assertStringEndsWith('.perfbase', $file->getFilename());
-
- $content = unserialize(File::get($file));
- $this->assertArrayHasKey('id', $content);
- $this->assertArrayHasKey('data', $content);
- $this->assertArrayHasKey('created_at', $content);
- $this->assertEquals($profileData, $content['data']);
- }
-
- public function testStoreCreatesDirectoryIfNotExists()
- {
- // Delete the directory
- File::deleteDirectory($this->testPath);
- $this->assertFalse(File::exists($this->testPath));
-
- $profileData = ['trace' => 'test_data'];
- $this->strategy->store($profileData);
-
- $this->assertTrue(File::exists($this->testPath));
- $this->assertCount(1, File::files($this->testPath));
- }
-
- public function testGetUnsentProfiles()
- {
- // Create test profiles
- $profiles = [];
- for ($i = 0; $i < 5; $i++) {
- $data = ['trace' => "test_data_$i", 'index' => $i];
- $this->strategy->store($data);
- $profiles[] = $data;
- }
-
- // Get all profiles
- $retrievedProfiles = [];
- foreach ($this->strategy->getUnsentProfiles() as $chunk) {
- foreach ($chunk as $profile) {
- $retrievedProfiles[] = $profile;
- }
- }
-
- $this->assertCount(5, $retrievedProfiles);
-
- // Verify structure
- foreach ($retrievedProfiles as $profile) {
- $this->assertArrayHasKey('id', $profile);
- $this->assertArrayHasKey('data', $profile);
- $this->assertArrayHasKey('created_at', $profile);
- }
- }
-
- public function testGetUnsentProfilesChunking()
- {
- // Create 10 profiles
- for ($i = 0; $i < 10; $i++) {
- $this->strategy->store(['index' => $i]);
- }
-
- // Get profiles in chunks of 3
- $chunks = [];
- foreach ($this->strategy->getUnsentProfiles(3) as $chunk) {
- $chunks[] = $chunk;
- }
-
- // Should have 4 chunks: 3, 3, 3, 1
- $this->assertCount(4, $chunks);
- $this->assertCount(3, $chunks[0]);
- $this->assertCount(3, $chunks[1]);
- $this->assertCount(3, $chunks[2]);
- $this->assertCount(1, $chunks[3]);
- }
-
- public function testCountUnsentProfiles()
- {
- $this->assertEquals(0, $this->strategy->countUnsentProfiles());
-
- $this->strategy->store(['test' => 'data1']);
- $this->strategy->store(['test' => 'data2']);
- $this->strategy->store(['test' => 'data3']);
-
- $this->assertEquals(3, $this->strategy->countUnsentProfiles());
- }
-
- public function testDelete()
- {
- $this->strategy->store(['test' => 'data']);
- $files = File::files($this->testPath);
- $this->assertCount(1, $files);
-
- $filename = $files[0]->getFilename();
- $this->strategy->delete($filename);
-
- $this->assertCount(0, File::files($this->testPath));
- }
-
- public function testDeleteNonExistentFile()
- {
- // Should not throw exception
- $this->strategy->delete('non-existent-file.perfbase');
- $this->assertTrue(true); // Assert no exception was thrown
- }
-
- public function testDeleteMass()
- {
- // Create 5 files
- $filenames = [];
- for ($i = 0; $i < 5; $i++) {
- $this->strategy->store(['index' => $i]);
- }
-
- $files = File::files($this->testPath);
- foreach ($files as $file) {
- $filenames[] = $file->getFilename();
- }
-
- $this->assertCount(5, $files);
-
- // Delete first 3
- $this->strategy->deleteMass(array_slice($filenames, 0, 3));
-
- $this->assertCount(2, File::files($this->testPath));
- }
-
- public function testClear()
- {
- // Create multiple profiles
- for ($i = 0; $i < 5; $i++) {
- $this->strategy->store(['index' => $i]);
- }
-
- // Create a non-perfbase file that should not be deleted
- File::put($this->testPath . '/other-file.txt', 'content');
-
- $this->assertCount(6, File::files($this->testPath)); // 5 .perfbase + 1 .txt
-
- $this->strategy->clear();
-
- $files = File::files($this->testPath);
- $this->assertCount(1, $files); // Only the .txt file should remain
- $this->assertEquals('other-file.txt', $files[0]->getFilename());
- }
-
- public function testEmptyGetUnsentProfiles()
- {
- $profiles = [];
- foreach ($this->strategy->getUnsentProfiles() as $chunk) {
- $profiles = array_merge($profiles, $chunk);
- }
-
- $this->assertEmpty($profiles);
- }
-}
\ No newline at end of file
diff --git a/tests/FilterMatcherConfigTest.php b/tests/FilterMatcherConfigTest.php
new file mode 100644
index 0000000..83329a7
--- /dev/null
+++ b/tests/FilterMatcherConfigTest.php
@@ -0,0 +1,114 @@
+ ['*'],
+ 'perfbase.exclude.http' => [],
+ ]);
+
+ $this->assertTrue(FilterMatcher::passesConfigFilters(['GET /api/users'], 'http'));
+ }
+
+ public function testFailsWhenNoIncludes(): void
+ {
+ config([
+ 'perfbase.include.queue' => [],
+ 'perfbase.exclude.queue' => [],
+ ]);
+
+ $this->assertFalse(FilterMatcher::passesConfigFilters(['App\Jobs\SendEmail'], 'queue'));
+ }
+
+ public function testFailsWhenIncludeDoesNotMatch(): void
+ {
+ config([
+ 'perfbase.include.console' => ['migrate*'],
+ 'perfbase.exclude.console' => [],
+ ]);
+
+ $this->assertFalse(FilterMatcher::passesConfigFilters(['queue:work'], 'console'));
+ }
+
+ public function testPassesWhenIncludeMatchesSpecificPattern(): void
+ {
+ config([
+ 'perfbase.include.queue' => ['App\Jobs\Important*'],
+ 'perfbase.exclude.queue' => [],
+ ]);
+
+ $this->assertTrue(FilterMatcher::passesConfigFilters(['App\Jobs\ImportantEmail'], 'queue'));
+ $this->assertFalse(FilterMatcher::passesConfigFilters(['App\Jobs\TrivialCleanup'], 'queue'));
+ }
+
+ public function testExcludeOverridesInclude(): void
+ {
+ config([
+ 'perfbase.include.http' => ['*'],
+ 'perfbase.exclude.http' => ['GET /health*'],
+ ]);
+
+ $this->assertTrue(FilterMatcher::passesConfigFilters(['POST /api/users'], 'http'));
+ $this->assertFalse(FilterMatcher::passesConfigFilters(['GET /health-check'], 'http'));
+ }
+
+ public function testHandlesNullIncludeConfig(): void
+ {
+ config(['perfbase.include.http' => null]);
+
+ $this->assertFalse(FilterMatcher::passesConfigFilters(['GET /'], 'http'));
+ }
+
+ public function testHandlesMissingConfigKey(): void
+ {
+ // Config key that doesn't exist at all
+ $this->assertFalse(FilterMatcher::passesConfigFilters(['something'], 'nonexistent'));
+ }
+
+ public function testRegexPatternInConfig(): void
+ {
+ config([
+ 'perfbase.include.http' => ['/^POST \/api\/.*/'],
+ 'perfbase.exclude.http' => [],
+ ]);
+
+ $this->assertTrue(FilterMatcher::passesConfigFilters(['POST /api/users'], 'http'));
+ $this->assertFalse(FilterMatcher::passesConfigFilters(['GET /api/users'], 'http'));
+ }
+
+ public function testMultipleIncludePatterns(): void
+ {
+ config([
+ 'perfbase.include.console' => ['migrate', 'db:seed', 'queue:*'],
+ 'perfbase.exclude.console' => [],
+ ]);
+
+ $this->assertTrue(FilterMatcher::passesConfigFilters(['migrate'], 'console'));
+ $this->assertTrue(FilterMatcher::passesConfigFilters(['db:seed'], 'console'));
+ $this->assertTrue(FilterMatcher::passesConfigFilters(['queue:work'], 'console'));
+ $this->assertFalse(FilterMatcher::passesConfigFilters(['horizon:work'], 'console'));
+ }
+
+ public function testEmptyExcludeDoesNotBlock(): void
+ {
+ config([
+ 'perfbase.include.queue' => ['*'],
+ 'perfbase.exclude.queue' => [],
+ ]);
+
+ $this->assertTrue(FilterMatcher::passesConfigFilters(['App\Jobs\AnyJob'], 'queue'));
+ }
+}
diff --git a/tests/HttpProfilerTest.php b/tests/HttpProfilerTest.php
deleted file mode 100644
index 4bde102..0000000
--- a/tests/HttpProfilerTest.php
+++ /dev/null
@@ -1,217 +0,0 @@
-allows('isExtensionAvailable')->andReturns(true);
- $perfbaseClient->allows('startTraceSpan')->andReturns(true);
- $perfbaseClient->allows('stopTraceSpan')->andReturns(true);
- $perfbaseClient->allows('setAttribute')->andReturns(true);
- $perfbaseClient->allows('submitTrace')->andReturns(true);
- $perfbaseClient->allows('getTraceData')->andReturns('test-data');
- $perfbaseClient->allows('reset')->andReturns(true);
- $this->app->instance(Config::class, $config);
- $this->app->instance(PerfbaseClient::class, $perfbaseClient);
-
- // Set up basic config values needed for the test
- config([
- 'perfbase' => [
- 'enabled' => true,
- 'api_key' => 'test-key',
- 'sample_rate' => 1.0,
- 'sending' => [
- 'mode' => 'sync',
- 'timeout' => 5,
- ],
- 'include' => [
- 'http' => [],
- ],
- 'exclude' => [
- 'http' => [],
- ],
- ]
- ]);
-
- $this->request = new Request();
- $this->request->server->set('SERVER_NAME', 'localhost');
- $this->request->server->set('SERVER_PORT', 80);
- $this->profiler = new HttpProfiler($this->request);
- $this->reflection = new ReflectionClass(HttpProfiler::class);
- }
-
- protected function tearDown(): void
- {
- Mockery::close();
- parent::tearDown();
- }
-
- public function testConstructor()
- {
- // With standardized span naming, it should be http.METHOD.path
- $this->assertEquals('http.GET./', $this->getPrivateProperty('spanName'));
- }
-
- public function testSetResponse()
- {
- $response = new Response('', 200);
- $this->profiler->setResponse($response);
- $this->assertEquals('200', $this->getPrivateProperty('attributes')['http_status_code']);
- }
-
- public function testShouldProfileWhenDisabled()
- {
- config(['perfbase.enabled' => false]);
- $this->assertFalse($this->callPrivateMethod('shouldProfile'));
- }
-
-// public function testShouldProfileWhenEnabled()
-// {
-// config(['perfbase.enabled' => true]);
-// config(['perfbase.include.http' => ['*']]);
-// config(['perfbase.exclude.http' => []]);
-//
-// $this->assertTrue($this->callPrivateMethod('shouldProfile'));
-// }
-
- public function testShouldUserBeProfiled()
- {
- $user = new class implements Authenticatable, ProfiledUser {
- public function shouldBeProfiled(): bool
- {
- return true;
- }
-
- public function getAuthIdentifierName()
- {
- // No op
- }
-
- public function getAuthIdentifier()
- {
- // No op
- }
-
- public function getAuthPassword()
- {
- // No op
- }
-
- public function getRememberToken()
- {
- // No op
- }
-
- public function setRememberToken($value)
- {
- // No op
- }
-
- public function getRememberTokenName()
- {
- // No op
- }
- };
-
- $this->assertTrue($this->callPrivateMethod('shouldUserBeProfiled', [$user]));
- }
-
- public function testGetRequestComponents()
- {
- // Create request properly with path
- $this->request = Request::create('/test', 'GET');
- $this->request->server->set('SERVER_NAME', 'localhost');
- $this->request->server->set('SERVER_PORT', 80);
- $this->profiler = new HttpProfiler($this->request);
-
- $components = $this->callPrivateMethod('getRequestComponents');
-
- // The component should include both formats
- $this->assertContains('GET /test', $components);
- $this->assertContains('/test', $components);
- $this->assertContains('test', $components); // without leading slash
- }
-
- public function testShouldRouteBeProfiled()
- {
- $components = ['GET /test'];
- config(['perfbase.include.http' => ['GET /test']]);
- config(['perfbase.exclude.http' => []]);
-
- $this->assertTrue($this->callPrivateMethod('shouldRouteBeProfiled', [$components]));
- }
-
- public function testMatchesIncludeFilters()
- {
- $components = ['GET /test'];
- config(['perfbase.include.http' => ['GET /test']]);
-
- $this->assertTrue($this->callPrivateMethod('matchesIncludeFilters', [$components]));
- }
-
- public function testMatchesExcludeFilters()
- {
- $components = ['GET /test'];
- config(['perfbase.exclude.http' => ['GET /test']]);
-
- $this->assertTrue($this->callPrivateMethod('matchesExcludeFilters', [$components]));
- }
-
- public function testSetDefaultAttributes()
- {
- // Create request properly with path
- $this->request = Request::create('http://localhost/test', 'GET');
- $this->profiler = new HttpProfiler($this->request);
-
- $this->callPrivateMethod('setDefaultAttributes');
- $attributes = $this->getPrivateProperty('attributes');
-
- $this->assertEquals('GET', $attributes['http_method']);
- $this->assertStringContainsString('localhost', $attributes['http_url']);
- $this->assertStringContainsString('/test', $attributes['http_url']);
- }
-
- private function callPrivateMethod(string $methodName, array $args = [])
- {
- $method = $this->reflection->getMethod($methodName);
- $method->setAccessible(true);
- return $method->invokeArgs($this->profiler, $args);
- }
-
- private function getPrivateProperty(string $propertyName)
- {
- $property = $this->reflection->getProperty($propertyName);
- $property->setAccessible(true);
- return $property->getValue($this->profiler);
- }
-}
diff --git a/tests/HttpTraceLifecycleTest.php b/tests/HttpTraceLifecycleTest.php
new file mode 100644
index 0000000..fa5315f
--- /dev/null
+++ b/tests/HttpTraceLifecycleTest.php
@@ -0,0 +1,208 @@
+perfbaseClient = Mockery::mock(PerfbaseClient::class);
+ $this->perfbaseClient->allows('isExtensionAvailable')->andReturns(true);
+ $this->perfbaseClient->allows('startTraceSpan');
+ $this->perfbaseClient->allows('stopTraceSpan')->andReturns(true);
+ $this->perfbaseClient->allows('setAttribute');
+ $this->perfbaseClient->allows('submitTrace')->andReturns(SubmitResult::success());
+ $this->perfbaseClient->allows('reset');
+
+ $this->app->instance(Config::class, Mockery::mock(Config::class));
+ $this->app->instance(PerfbaseClient::class, $this->perfbaseClient);
+
+ config([
+ 'perfbase' => [
+ 'enabled' => true,
+ 'api_key' => 'test-key',
+ 'sample_rate' => 1.0,
+ 'include' => ['http' => ['*']],
+ 'exclude' => ['http' => []],
+ ],
+ 'app' => ['env' => 'testing', 'version' => '1.0.0'],
+ ]);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ parent::tearDown();
+ }
+
+ public function testSetsHttpAttributes(): void
+ {
+ $request = Request::create('/api/users', 'POST');
+ $lifecycle = new HttpTraceLifecycle($request);
+
+ $lifecycle->startProfiling();
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertSame('http', $attrs['source']);
+ $this->assertSame('POST', $attrs['http_method']);
+ $this->assertStringContainsString('/api/users', $attrs['http_url']);
+ $this->assertStringContainsString('POST', $attrs['action']);
+ }
+
+ public function testSetsResponseStatusCode(): void
+ {
+ $request = Request::create('/test', 'GET');
+ $lifecycle = new HttpTraceLifecycle($request);
+ $lifecycle->setResponse(new Response('', 404));
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertSame('404', $attrs['http_status_code']);
+ }
+
+ public function testSetsBaseAttributes(): void
+ {
+ $request = Request::create('/test', 'GET');
+ $lifecycle = new HttpTraceLifecycle($request);
+ $lifecycle->startProfiling();
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertArrayHasKey('hostname', $attrs);
+ $this->assertSame('testing', $attrs['environment']);
+ $this->assertSame('1.0.0', $attrs['app_version']);
+ $this->assertArrayHasKey('php_version', $attrs);
+ $this->assertArrayHasKey('user_ip', $attrs);
+ $this->assertArrayHasKey('user_agent', $attrs);
+ }
+
+ public function testShouldProfileReturnsFalseWhenDisabled(): void
+ {
+ config(['perfbase.enabled' => false]);
+
+ $request = Request::create('/test', 'GET');
+ $lifecycle = new HttpTraceLifecycle($request);
+
+ $result = $this->callShouldProfile($lifecycle);
+ $this->assertFalse($result);
+ }
+
+ public function testShouldProfileReturnsFalseWhenExcluded(): void
+ {
+ config([
+ 'perfbase.include.http' => ['*'],
+ 'perfbase.exclude.http' => ['GET /excluded*'],
+ ]);
+
+ $request = Request::create('/excluded-path', 'GET');
+ $lifecycle = new HttpTraceLifecycle($request);
+
+ $result = $this->callShouldProfile($lifecycle);
+ $this->assertFalse($result);
+ }
+
+ public function testShouldProfileReturnsFalseWhenNotIncluded(): void
+ {
+ config(['perfbase.include.http' => ['POST /api/*']]);
+
+ $request = Request::create('/web/page', 'GET');
+ $lifecycle = new HttpTraceLifecycle($request);
+
+ $result = $this->callShouldProfile($lifecycle);
+ $this->assertFalse($result);
+ }
+
+ public function testShouldProfileReturnsFalseWhenExtensionUnavailable(): void
+ {
+ // Override the default mock with a fresh one that returns false
+ $client = Mockery::mock(PerfbaseClient::class);
+ $client->allows('isExtensionAvailable')->andReturns(false);
+ $client->allows('startTraceSpan');
+ $client->allows('stopTraceSpan')->andReturns(false);
+ $client->allows('setAttribute');
+ $client->allows('submitTrace')->andReturns(SubmitResult::success());
+ $client->allows('reset');
+ $this->app->instance(PerfbaseClient::class, $client);
+
+ $request = Request::create('/test', 'GET');
+ $lifecycle = new HttpTraceLifecycle($request);
+
+ $result = $this->callShouldProfile($lifecycle);
+ $this->assertFalse($result);
+ }
+
+ public function testShouldProfileRespectsProfiledUserInterface(): void
+ {
+ $user = Mockery::mock(ProfiledUser::class, \Illuminate\Contracts\Auth\Authenticatable::class);
+ $user->shouldReceive('shouldBeProfiled')->andReturn(false);
+ $user->shouldReceive('getAuthIdentifierName')->andReturn('id');
+ $user->shouldReceive('getAuthIdentifier')->andReturn(1);
+
+ $request = Request::create('/test', 'GET');
+ $request->setUserResolver(fn() => $user);
+
+ $lifecycle = new HttpTraceLifecycle($request);
+
+ $result = $this->callShouldProfile($lifecycle);
+ $this->assertFalse($result);
+ }
+
+ public function testShouldProfileAllowsUserWithoutInterface(): void
+ {
+ // User that doesn't implement ProfiledUser — should be allowed
+ $user = Mockery::mock(\Illuminate\Contracts\Auth\Authenticatable::class);
+ $user->shouldReceive('getAuthIdentifierName')->andReturn('id');
+ $user->shouldReceive('getAuthIdentifier')->andReturn(1);
+
+ $request = Request::create('/test', 'GET');
+ $request->setUserResolver(fn() => $user);
+
+ $lifecycle = new HttpTraceLifecycle($request);
+
+ $result = $this->callShouldProfile($lifecycle);
+ $this->assertTrue($result);
+ }
+
+ /**
+ * @return array
+ */
+ private function getAttributes(HttpTraceLifecycle $lifecycle): array
+ {
+ $reflection = new ReflectionClass($lifecycle);
+ $prop = $reflection->getProperty('attributes');
+ $prop->setAccessible(true);
+ return $prop->getValue($lifecycle);
+ }
+
+ private function callShouldProfile(HttpTraceLifecycle $lifecycle): bool
+ {
+ $reflection = new ReflectionClass($lifecycle);
+ $method = $reflection->getMethod('shouldProfile');
+ $method->setAccessible(true);
+ return $method->invoke($lifecycle);
+ }
+}
diff --git a/tests/MatchesFiltersTest.php b/tests/MatchesFiltersTest.php
index a0e5cf2..5796822 100644
--- a/tests/MatchesFiltersTest.php
+++ b/tests/MatchesFiltersTest.php
@@ -1,6 +1,6 @@
['GET /'],
- 'expected' => true, // Exact match
+ 'expected' => true,
],
[
'filters' => ['POST /api/*'],
- 'expected' => true, // Matches with "*"
+ 'expected' => true,
],
[
'filters' => ['App\Http\Controllers*'],
- 'expected' => true, // Namespace prefix match with wildcard
+ 'expected' => true,
],
[
'filters' => ['UserController'],
- 'expected' => true, // Exact match for controller
+ 'expected' => true,
],
[
'filters' => ['/^App\\\\Http\\\\Controllers\\\\.*$/'],
- 'expected' => true, // Full regex match
+ 'expected' => true,
],
[
'filters' => ['GET /invalid', 'POST /other'],
- 'expected' => false, // No matches
+ 'expected' => false,
],
[
'filters' => [],
- 'expected' => false, // Empty filters should never match
+ 'expected' => false,
],
[
'filters' => ['*'],
- 'expected' => true, // Match all
+ 'expected' => true,
],
[
'filters' => ['/^GET \/example\/([0-9]+)\/$/'],
- 'expected' => false, // Assuming no component matches this regex
+ 'expected' => false,
],
[
'filters' => ['GET /example/*'],
- 'expected' => false, // Assuming no component starts with 'GET /example/'
+ 'expected' => false,
],
[
'filters' => ['GET /example'],
- 'expected' => true, // Exact match not present in components
+ 'expected' => true,
],
[
'filters' => ['UserController', 'App\Http\Controllers'],
- 'expected' => true, // Should match 'UserController' and 'App\Http\Controllers\UserController'
+ 'expected' => true,
],
];
- // Run each test case
foreach ($testCases as $case) {
- $result = HttpProfiler::matchesFilters($components, $case['filters']);
+ $result = FilterMatcher::matches($components, $case['filters']);
$this->assertSame($case['expected'], $result, 'Failed matching filters: "' . implode('" and "', $case['filters']) . '" against "' . implode('", "', $components) . '"');
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/PerfbaseConfigTest.php b/tests/PerfbaseConfigTest.php
index 4b84767..3cabccc 100644
--- a/tests/PerfbaseConfigTest.php
+++ b/tests/PerfbaseConfigTest.php
@@ -180,47 +180,20 @@ public function testSampleRateMethodWithInteger()
$this->assertEquals(1.0, PerfbaseConfig::sampleRate());
}
- public function testSendingModeMethod()
- {
- config(['perfbase.sending.mode' => 'async']);
- PerfbaseConfig::clearCache();
-
- $this->assertEquals('async', PerfbaseConfig::sendingMode());
- }
-
- public function testSendingModeMethodDefault()
- {
- // Don't set perfbase.sending.mode
- PerfbaseConfig::clearCache();
-
- $this->assertEquals('sync', PerfbaseConfig::sendingMode()); // Should default to 'sync'
- }
-
- public function testSendingModeMethodWithNestedConfig()
- {
- config(['perfbase.sending' => ['mode' => 'database', 'timeout' => 10]]);
- PerfbaseConfig::clearCache();
-
- $this->assertEquals('database', PerfbaseConfig::sendingMode());
- }
-
public function testMultipleCallsUseSameCache()
{
config([
'perfbase.enabled' => true,
'perfbase.sample_rate' => 0.8,
- 'perfbase.sending.mode' => 'file'
]);
PerfbaseConfig::clearCache();
-
+
// Multiple calls to different methods
$enabled = PerfbaseConfig::enabled();
$sampleRate = PerfbaseConfig::sampleRate();
- $sendingMode = PerfbaseConfig::sendingMode();
-
+
$this->assertTrue($enabled);
$this->assertEquals(0.8, $sampleRate);
- $this->assertEquals('file', $sendingMode);
}
public function testCacheIsSharedAcrossMethods()
@@ -331,18 +304,17 @@ public function testShortcutMethodsUseCaching()
config([
'perfbase.enabled' => true,
'perfbase.sample_rate' => 0.75,
- 'perfbase.sending.mode' => 'database'
]);
-
+
// First call to enabled() should populate cache
PerfbaseConfig::enabled();
-
+
// Change config
config(['perfbase.enabled' => false]);
-
+
// Second call should return cached value
$this->assertTrue(PerfbaseConfig::enabled());
-
+
// Clear cache and try again
PerfbaseConfig::clearCache();
$this->assertFalse(PerfbaseConfig::enabled());
diff --git a/tests/PerfbaseFacadeTest.php b/tests/PerfbaseFacadeTest.php
index 2e6993f..9532a7d 100644
--- a/tests/PerfbaseFacadeTest.php
+++ b/tests/PerfbaseFacadeTest.php
@@ -8,6 +8,7 @@
use Perfbase\SDK\Config;
use Perfbase\SDK\Extension\ExtensionInterface;
use Perfbase\SDK\Perfbase as PerfbaseClient;
+use Perfbase\SDK\SubmitResult;
use Mockery;
class PerfbaseFacadeTest extends TestCase
@@ -37,10 +38,8 @@ protected function setUp(): void
'enabled' => true,
'api_key' => 'test-key',
'flags' => 0,
- 'sending' => [
- 'proxy' => null,
- 'timeout' => 5,
- ],
+ 'proxy' => null,
+ 'timeout' => 5,
]
]);
}
@@ -105,16 +104,14 @@ public function testStopTraceSpanMethod()
public function testSubmitTraceMethod()
{
- // Mock the Perfbase client
$mockClient = Mockery::mock(PerfbaseClient::class);
- $mockClient->shouldReceive('submitTrace')->once()->andReturn();
-
+ $mockClient->shouldReceive('submitTrace')->once()->andReturn(SubmitResult::success());
+
$this->app->instance(PerfbaseClient::class, $mockClient);
-
- Perfbase::submitTrace();
-
- $mockClient->shouldHaveReceived('submitTrace')->once();
- $this->assertTrue(true); // Assert that we got this far
+
+ $result = Perfbase::submitTrace();
+
+ $this->assertTrue($result->isSuccess());
}
public function testGetTraceDataMethod()
@@ -284,7 +281,7 @@ public function testFacadeDocBlocks()
$this->assertStringContainsString('@method static void startTraceSpan(string $spanName, array $attributes = [])', $docComment);
$this->assertStringContainsString('@method static bool stopTraceSpan(string $spanName)', $docComment);
- $this->assertStringContainsString('@method static void submitTrace()', $docComment);
+ $this->assertStringContainsString('@method static \Perfbase\SDK\SubmitResult submitTrace()', $docComment);
$this->assertStringContainsString('@method static string getTraceData(string $spanName = \'\')', $docComment);
$this->assertStringContainsString('@method static void reset()', $docComment);
$this->assertStringContainsString('@method static bool isExtensionAvailable()', $docComment);
diff --git a/tests/PerfbaseMiddlewareTest.php b/tests/PerfbaseMiddlewareTest.php
index 2d97748..9daed91 100644
--- a/tests/PerfbaseMiddlewareTest.php
+++ b/tests/PerfbaseMiddlewareTest.php
@@ -9,6 +9,7 @@
use Perfbase\Laravel\PerfbaseServiceProvider;
use Perfbase\SDK\Config;
use Perfbase\SDK\Perfbase as PerfbaseClient;
+use Perfbase\SDK\SubmitResult;
use Mockery;
class PerfbaseMiddlewareTest extends TestCase
@@ -32,7 +33,7 @@ protected function setUp(): void
$this->perfbaseClient->allows('startTraceSpan')->andReturns(true);
$this->perfbaseClient->allows('stopTraceSpan')->andReturns(true);
$this->perfbaseClient->allows('setAttribute')->andReturns(true);
- $this->perfbaseClient->allows('submitTrace')->andReturns(true);
+ $this->perfbaseClient->allows('submitTrace')->andReturns(SubmitResult::success());
$this->perfbaseClient->allows('getTraceData')->andReturns(['trace' => 'data']);
$this->perfbaseClient->allows('reset')->andReturns(true);
@@ -45,9 +46,6 @@ protected function setUp(): void
'enabled' => true,
'api_key' => 'test-key',
'sample_rate' => 1.0,
- 'sending' => [
- 'mode' => 'sync',
- ],
'include' => [
'http' => ['*'],
],
diff --git a/tests/PerfbaseServiceProviderTest.php b/tests/PerfbaseServiceProviderTest.php
index e95ae6e..6e25197 100644
--- a/tests/PerfbaseServiceProviderTest.php
+++ b/tests/PerfbaseServiceProviderTest.php
@@ -44,10 +44,8 @@ protected function setUp(): void
'enabled' => true,
'api_key' => 'test-key',
'flags' => 0,
- 'sending' => [
- 'proxy' => null,
- 'timeout' => 5,
- ],
+ 'proxy' => null,
+ 'timeout' => 5,
]
]);
}
@@ -93,7 +91,7 @@ public function testServiceProviderMergesConfig()
$this->assertArrayHasKey('enabled', $config);
$this->assertArrayHasKey('api_key', $config);
$this->assertArrayHasKey('sample_rate', $config);
- $this->assertArrayHasKey('sending', $config);
+ $this->assertArrayHasKey('timeout', $config);
$this->assertArrayHasKey('include', $config);
$this->assertArrayHasKey('exclude', $config);
}
@@ -176,19 +174,19 @@ public function testServiceProviderPublishesConfig()
public function testConfigBindingHandlesNullProxy()
{
- config(['perfbase.sending.proxy' => null]);
-
+ config(['perfbase.proxy' => null]);
+
$config = $this->app->make(Config::class);
-
+
$this->assertInstanceOf(Config::class, $config);
}
public function testConfigBindingHandlesStringProxy()
{
- config(['perfbase.sending.proxy' => 'http://proxy.example.com']);
-
+ config(['perfbase.proxy' => 'http://proxy.example.com']);
+
$config = $this->app->make(Config::class);
-
+
$this->assertInstanceOf(Config::class, $config);
}
diff --git a/tests/QueueProfilerTest.php b/tests/QueueProfilerTest.php
deleted file mode 100644
index d95b3e8..0000000
--- a/tests/QueueProfilerTest.php
+++ /dev/null
@@ -1,131 +0,0 @@
-allows('isExtensionAvailable')->andReturns(true);
- $perfbaseClient->allows('startTraceSpan')->andReturns(true);
- $perfbaseClient->allows('stopTraceSpan')->andReturns(true);
- $perfbaseClient->allows('setAttribute')->andReturns(true);
- $perfbaseClient->allows('submitTrace')->andReturns(true);
- $perfbaseClient->allows('getTraceData')->andReturns('test-data');
- $perfbaseClient->allows('reset')->andReturns(true);
- $this->app->instance(Config::class, $config);
- $this->app->instance(PerfbaseClient::class, $perfbaseClient);
-
- // Set up basic config values needed for the test
- config([
- 'perfbase' => [
- 'enabled' => true,
- 'api_key' => 'test-key',
- 'sample_rate' => 1.0,
- 'sending' => [
- 'mode' => 'sync',
- 'timeout' => 5,
- ],
- 'include' => [
- 'queue' => [],
- ],
- 'exclude' => [
- 'queue' => [],
- ],
- ]
- ]);
-
- // Mock the job
- $this->job = Mockery::mock(Job::class);
- $this->job->shouldReceive('getName')->andReturn('App\Jobs\TestJob');
- $this->job->shouldReceive('getQueue')->andReturn('default');
- $this->job->shouldReceive('attempts')->andReturn(1);
- $this->job->shouldReceive('getConnectionName')->andReturn('redis');
- $this->job->shouldReceive('getJobId')->andReturn('123');
-
- $this->profiler = new QueueProfiler($this->job, 'App\Jobs\TestJob');
- $this->reflection = new ReflectionClass(QueueProfiler::class);
- }
-
- protected function tearDown(): void
- {
- Mockery::close();
- parent::tearDown();
- }
-
- public function testConstructor()
- {
- $this->assertEquals('App\Jobs\TestJob', $this->getPrivateProperty('spanName'));
- }
-
- public function testShouldProfileWhenDisabled()
- {
- config(['perfbase.enabled' => false]);
- $this->assertFalse($this->callPrivateMethod('shouldProfile'));
- }
-
- public function testShouldProfileWhenEnabled()
- {
- config(['perfbase.enabled' => true]);
- config(['perfbase.include.queue' => ['App\Jobs\TestJob']]);
- config(['perfbase.exclude.queue' => []]);
-
- $this->assertTrue($this->callPrivateMethod('shouldProfile'));
- }
-
- public function testGetJobName()
- {
- $this->assertEquals('App\Jobs\TestJob', $this->getPrivateProperty('jobName'));
- }
-
- public function testSetDefaultAttributes()
- {
- $this->callPrivateMethod('setDefaultAttributes');
- $attributes = $this->getPrivateProperty('attributes');
-
- $this->assertEquals('default', $attributes['queue']);
- $this->assertEquals('App\Jobs\TestJob', $attributes['action']);
- $this->assertEquals('1', $attributes['attempts']);
- $this->assertEquals('redis', $attributes['connection']);
- $this->assertEquals('123', $attributes['job_id']);
- }
-
- public function testSetException()
- {
- $this->profiler->setException('Test exception');
- $this->assertEquals('Test exception', $this->getPrivateProperty('attributes')['exception']);
- }
-
- private function callPrivateMethod(string $methodName, array $args = [])
- {
- $method = $this->reflection->getMethod($methodName);
- $method->setAccessible(true);
- return $method->invokeArgs($this->profiler, $args);
- }
-
- private function getPrivateProperty(string $propertyName)
- {
- $property = $this->reflection->getProperty($propertyName);
- $property->setAccessible(true);
- return $property->getValue($this->profiler);
- }
-}
diff --git a/tests/QueueTraceLifecycleTest.php b/tests/QueueTraceLifecycleTest.php
new file mode 100644
index 0000000..b6f1b09
--- /dev/null
+++ b/tests/QueueTraceLifecycleTest.php
@@ -0,0 +1,160 @@
+perfbaseClient = Mockery::mock(PerfbaseClient::class);
+ $this->perfbaseClient->allows('isExtensionAvailable')->andReturns(true);
+ $this->perfbaseClient->allows('startTraceSpan');
+ $this->perfbaseClient->allows('stopTraceSpan')->andReturns(true);
+ $this->perfbaseClient->allows('setAttribute');
+ $this->perfbaseClient->allows('submitTrace')->andReturns(SubmitResult::success());
+ $this->perfbaseClient->allows('reset');
+
+ $this->app->instance(Config::class, Mockery::mock(Config::class));
+ $this->app->instance(PerfbaseClient::class, $this->perfbaseClient);
+
+ config([
+ 'perfbase' => [
+ 'enabled' => true,
+ 'sample_rate' => 1.0,
+ 'include' => ['queue' => ['*']],
+ 'exclude' => ['queue' => []],
+ ],
+ 'app' => ['env' => 'testing', 'version' => '2.0.0'],
+ ]);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ parent::tearDown();
+ }
+
+ public function testSetsQueueAttributes(): void
+ {
+ $lifecycle = new QueueTraceLifecycle('App\Jobs\SendEmail', 'default', 'redis');
+ $lifecycle->startProfiling();
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertSame('queue', $attrs['source']);
+ $this->assertSame('App\Jobs\SendEmail', $attrs['action']);
+ $this->assertSame('default', $attrs['queue']);
+ $this->assertSame('redis', $attrs['connection']);
+ }
+
+ public function testSetsBaseAttributes(): void
+ {
+ $lifecycle = new QueueTraceLifecycle('App\Jobs\Test', 'high', 'database');
+ $lifecycle->startProfiling();
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertArrayHasKey('hostname', $attrs);
+ $this->assertSame('testing', $attrs['environment']);
+ $this->assertSame('2.0.0', $attrs['app_version']);
+ $this->assertArrayHasKey('php_version', $attrs);
+ }
+
+ public function testDoesNotSetHttpAttributes(): void
+ {
+ $lifecycle = new QueueTraceLifecycle('App\Jobs\Test', 'default', 'redis');
+ $lifecycle->startProfiling();
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertArrayNotHasKey('user_ip', $attrs);
+ $this->assertArrayNotHasKey('user_agent', $attrs);
+ $this->assertArrayNotHasKey('http_method', $attrs);
+ $this->assertArrayNotHasKey('http_url', $attrs);
+ }
+
+ public function testShouldProfileReturnsTrueWhenIncluded(): void
+ {
+ config(['perfbase.include.queue' => ['*']]);
+
+ $lifecycle = new QueueTraceLifecycle('App\Jobs\AnyJob', 'default', 'redis');
+ $this->assertTrue($this->callShouldProfile($lifecycle));
+ }
+
+ public function testShouldProfileReturnsFalseWhenNotIncluded(): void
+ {
+ config(['perfbase.include.queue' => ['App\Jobs\Important*']]);
+
+ $lifecycle = new QueueTraceLifecycle('App\Jobs\TrivialCleanup', 'default', 'redis');
+ $this->assertFalse($this->callShouldProfile($lifecycle));
+ }
+
+ public function testShouldProfileReturnsFalseWhenExcluded(): void
+ {
+ config([
+ 'perfbase.include.queue' => ['*'],
+ 'perfbase.exclude.queue' => ['App\Jobs\Noisy*'],
+ ]);
+
+ $lifecycle = new QueueTraceLifecycle('App\Jobs\NoisyHeartbeat', 'default', 'redis');
+ $this->assertFalse($this->callShouldProfile($lifecycle));
+ }
+
+ public function testSpanNameUsesClassBasename(): void
+ {
+ $lifecycle = new QueueTraceLifecycle('App\Jobs\Nested\DeepJob', 'default', 'redis');
+
+ $reflection = new ReflectionClass($lifecycle);
+ $prop = $reflection->getProperty('spanName');
+ $prop->setAccessible(true);
+
+ $this->assertSame('queue.DeepJob', $prop->getValue($lifecycle));
+ }
+
+ public function testSetExceptionAttribute(): void
+ {
+ $lifecycle = new QueueTraceLifecycle('App\Jobs\Failing', 'default', 'redis');
+ $lifecycle->setException('Something went wrong');
+
+ $attrs = $this->getAttributes($lifecycle);
+ $this->assertSame('Something went wrong', $attrs['exception']);
+ }
+
+ /**
+ * @return array
+ */
+ private function getAttributes(QueueTraceLifecycle $lifecycle): array
+ {
+ $reflection = new ReflectionClass($lifecycle);
+ $prop = $reflection->getProperty('attributes');
+ $prop->setAccessible(true);
+ return $prop->getValue($lifecycle);
+ }
+
+ private function callShouldProfile(QueueTraceLifecycle $lifecycle): bool
+ {
+ $reflection = new ReflectionClass($lifecycle);
+ $method = $reflection->getMethod('shouldProfile');
+ $method->setAccessible(true);
+ return $method->invoke($lifecycle);
+ }
+}
diff --git a/tests/UniversalProfilerTest.php b/tests/UniversalProfilerTest.php
deleted file mode 100644
index 0da9a18..0000000
--- a/tests/UniversalProfilerTest.php
+++ /dev/null
@@ -1,375 +0,0 @@
-shouldReceive('isAvailable')->andReturn(true);
- $mockExtension->shouldReceive('startSpan')->andReturn();
- $mockExtension->shouldReceive('stopSpan')->andReturn();
- $mockExtension->shouldReceive('getSpanData')->andReturn('{}');
- $mockExtension->shouldReceive('reset')->andReturn();
-
- $this->app->instance(ExtensionInterface::class, $mockExtension);
-
- // Set up basic config
- config([
- 'perfbase' => [
- 'enabled' => true,
- 'api_key' => 'test-key',
- 'flags' => 0,
- 'sample_rate' => 1.0,
- 'sending' => [
- 'proxy' => null,
- 'timeout' => 5,
- ],
- ]
- ]);
- }
-
- protected function tearDown(): void
- {
- Mockery::close();
- parent::tearDown();
- }
-
- public function testConstructorWithBasicType()
- {
- $profiler = new UniversalProfiler('test');
-
- $this->assertInstanceOf(UniversalProfiler::class, $profiler);
- }
-
- public function testConstructorWithContext()
- {
- $context = [
- 'user_id' => 123,
- 'action' => 'test_action',
- 'metadata' => ['key' => 'value']
- ];
-
- $profiler = new UniversalProfiler('test', $context);
-
- $this->assertEquals($context, $profiler->getContext());
- }
-
- public function testConstructorWithCustomCallback()
- {
- $callback = function($context) {
- return $context['should_profile'] ?? false;
- };
-
- $context = ['should_profile' => true];
- $profiler = new UniversalProfiler('test', $context, $callback);
-
- // Access the protected shouldProfile method
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
-
- $this->assertTrue($method->invoke($profiler));
- }
-
- public function testConstructorWithCustomCallbackFalse()
- {
- $callback = function($context) {
- return $context['should_profile'] ?? false;
- };
-
- $context = ['should_profile' => false];
- $profiler = new UniversalProfiler('test', $context, $callback);
-
- // Access the protected shouldProfile method
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
-
- $this->assertFalse($method->invoke($profiler));
- }
-
- public function testShouldProfileWithDefaultBehavior()
- {
- config(['perfbase.profile.test' => true]);
-
- $profiler = new UniversalProfiler('test');
-
- // Access the protected shouldProfile method
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
-
- $this->assertTrue($method->invoke($profiler));
- }
-
- public function testShouldProfileWithDefaultBehaviorFalse()
- {
- config(['perfbase.profile.test' => false]);
-
- $profiler = new UniversalProfiler('test');
-
- // Access the protected shouldProfile method
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
-
- $this->assertFalse($method->invoke($profiler));
- }
-
- public function testShouldProfileWithMissingConfig()
- {
- // Don't set any config for this type
- $profiler = new UniversalProfiler('unknown-type');
-
- // Access the protected shouldProfile method
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
-
- // Should default to true
- $this->assertTrue($method->invoke($profiler));
- }
-
- public function testGetContext()
- {
- $context = [
- 'user_id' => 123,
- 'action' => 'test_action'
- ];
-
- $profiler = new UniversalProfiler('test', $context);
-
- $this->assertEquals($context, $profiler->getContext());
- }
-
- public function testGetContextEmpty()
- {
- $profiler = new UniversalProfiler('test');
-
- $this->assertEquals([], $profiler->getContext());
- }
-
- public function testAddContext()
- {
- $initialContext = ['user_id' => 123];
- $profiler = new UniversalProfiler('test', $initialContext);
-
- $additionalContext = ['action' => 'test_action', 'metadata' => ['key' => 'value']];
- $profiler->addContext($additionalContext);
-
- $expectedContext = array_merge($initialContext, $additionalContext);
- $this->assertEquals($expectedContext, $profiler->getContext());
- }
-
- public function testAddContextOverwrite()
- {
- $initialContext = ['user_id' => 123, 'action' => 'initial'];
- $profiler = new UniversalProfiler('test', $initialContext);
-
- $additionalContext = ['action' => 'updated', 'new_key' => 'new_value'];
- $profiler->addContext($additionalContext);
-
- $expectedContext = ['user_id' => 123, 'action' => 'updated', 'new_key' => 'new_value'];
- $this->assertEquals($expectedContext, $profiler->getContext());
- }
-
- public function testAddContextEmpty()
- {
- $initialContext = ['user_id' => 123];
- $profiler = new UniversalProfiler('test', $initialContext);
-
- $profiler->addContext([]);
-
- $this->assertEquals($initialContext, $profiler->getContext());
- }
-
- public function testContextSetsAttributes()
- {
- $context = [
- 'user_id' => 123,
- 'action' => 'test_action'
- ];
-
- $profiler = new UniversalProfiler('test', $context);
-
- // Access the protected attributes property
- $reflection = new \ReflectionClass($profiler);
- $property = $reflection->getProperty('attributes');
- $property->setAccessible(true);
-
- $attributes = $property->getValue($profiler);
-
- // Context should be set as attributes
- $this->assertEquals(123, $attributes['user_id']);
- $this->assertEquals('test_action', $attributes['action']);
- }
-
- public function testAddContextUpdatesAttributes()
- {
- $profiler = new UniversalProfiler('test');
-
- $context = ['user_id' => 123];
- $profiler->addContext($context);
-
- // Access the protected attributes property
- $reflection = new \ReflectionClass($profiler);
- $property = $reflection->getProperty('attributes');
- $property->setAccessible(true);
-
- $attributes = $property->getValue($profiler);
-
- // New context should be added to attributes
- $this->assertEquals(123, $attributes['user_id']);
- }
-
- public function testCustomCallbackWithComplexLogic()
- {
- $callback = function($context) {
- // Complex logic: profile only if user is admin and action is important
- return ($context['user_role'] ?? '') === 'admin' &&
- in_array($context['action'] ?? '', ['create', 'update', 'delete']);
- };
-
- // Test admin with important action
- $context = ['user_role' => 'admin', 'action' => 'create'];
- $profiler = new UniversalProfiler('test', $context, $callback);
-
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
-
- $this->assertTrue($method->invoke($profiler));
-
- // Test admin with unimportant action
- $context = ['user_role' => 'admin', 'action' => 'view'];
- $profiler = new UniversalProfiler('test', $context, $callback);
-
- $this->assertFalse($method->invoke($profiler));
-
- // Test non-admin with important action
- $context = ['user_role' => 'user', 'action' => 'create'];
- $profiler = new UniversalProfiler('test', $context, $callback);
-
- $this->assertFalse($method->invoke($profiler));
- }
-
- public function testCallbackReceivesCorrectContext()
- {
- $receivedContext = null;
- $callback = function($context) use (&$receivedContext) {
- $receivedContext = $context;
- return true;
- };
-
- $originalContext = ['user_id' => 123, 'action' => 'test'];
- $profiler = new UniversalProfiler('test', $originalContext, $callback);
-
- // Trigger shouldProfile
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
- $method->invoke($profiler);
-
- $this->assertEquals($originalContext, $receivedContext);
- }
-
- public function testCallbackReceivesUpdatedContext()
- {
- $receivedContext = null;
- $callback = function($context) use (&$receivedContext) {
- $receivedContext = $context;
- return true;
- };
-
- $profiler = new UniversalProfiler('test', ['initial' => 'value'], $callback);
- $profiler->addContext(['added' => 'value']);
-
- // Trigger shouldProfile
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
- $method->invoke($profiler);
-
- $expectedContext = ['initial' => 'value', 'added' => 'value'];
- $this->assertEquals($expectedContext, $receivedContext);
- }
-
- public function testInheritsFromAbstractProfiler()
- {
- $profiler = new UniversalProfiler('test');
-
- $this->assertInstanceOf(\Perfbase\Laravel\Profiling\AbstractProfiler::class, $profiler);
- }
-
- public function testCanStartAndStopProfiling()
- {
- $profiler = new UniversalProfiler('test');
-
- // These methods should be available from AbstractProfiler
- $this->assertTrue(method_exists($profiler, 'startProfiling'));
- $this->assertTrue(method_exists($profiler, 'stopProfiling'));
- }
-
- public function testWorksWithDifferentTypes()
- {
- $httpProfiler = new UniversalProfiler('http.GET./api/users');
- $queueProfiler = new UniversalProfiler('queue.ProcessPodcast');
- $consoleProfiler = new UniversalProfiler('console.migrate');
-
- $this->assertInstanceOf(UniversalProfiler::class, $httpProfiler);
- $this->assertInstanceOf(UniversalProfiler::class, $queueProfiler);
- $this->assertInstanceOf(UniversalProfiler::class, $consoleProfiler);
- }
-
- public function testHandlesNullCallback()
- {
- $profiler = new UniversalProfiler('test', [], null);
-
- config(['perfbase.profile.test' => true]);
-
- // Access the protected shouldProfile method
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
-
- // Should fall back to default behavior
- $this->assertTrue($method->invoke($profiler));
- }
-
- public function testHandlesCallbackException()
- {
- $callback = function($context) {
- throw new \Exception('Callback error');
- };
-
- $profiler = new UniversalProfiler('test', [], $callback);
-
- // Access the protected shouldProfile method
- $reflection = new \ReflectionClass($profiler);
- $method = $reflection->getMethod('shouldProfile');
- $method->setAccessible(true);
-
- // Should handle exception gracefully
- $this->expectException(\Exception::class);
- $this->expectExceptionMessage('Callback error');
- $method->invoke($profiler);
- }
-}
\ No newline at end of file