├── ide.json ├── src ├── Watchers │ ├── DumpWatcher.php │ ├── DeleteQueryWatcher.php │ ├── InsertQueryWatcher.php │ ├── SelectQueryWatcher.php │ ├── UpdateQueryWatcher.php │ ├── EventWatcher.php │ ├── DeprecatedNoticeWatcher.php │ ├── ViewWatcher.php │ ├── Watcher.php │ ├── SlowQueryWatcher.php │ ├── JobWatcher.php │ ├── ConditionalQueryWatcher.php │ ├── ApplicationLogWatcher.php │ ├── DuplicateQueryWatcher.php │ ├── MailWatcher.php │ ├── CacheWatcher.php │ ├── QueryWatcher.php │ ├── HttpClientWatcher.php │ ├── ExceptionWatcher.php │ └── RequestWatcher.php ├── RayProxy.php ├── DumpRecorder │ ├── MultiDumpHandler.php │ └── DumpRecorder.php ├── Payloads │ ├── QueryPayload.php │ ├── ModelPayload.php │ ├── EventPayload.php │ ├── JobEventPayload.php │ ├── ViewPayload.php │ ├── ExecutedQueryPayload.php │ ├── CachePayload.php │ ├── ResponsePayload.php │ ├── MailablePayload.php │ ├── MarkdownPayload.php │ ├── EnvironmentPayload.php │ └── LoggedMailPayload.php ├── Support │ └── Composer.php ├── Commands │ ├── CleanRayCommand.php │ └── PublishConfigCommand.php ├── OriginFactory.php ├── RayServiceProvider.php └── Ray.php ├── phpstan.neon.dist ├── LICENSE.md ├── README.md ├── composer.json ├── phpstan-baseline.neon ├── stub └── ray.php └── CHANGELOG.md /ide.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json", 3 | "blade": { 4 | "directives": [ 5 | { 6 | "name": "ray", 7 | "prefix": "" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Watchers/DumpWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_dumps_to_ray; 15 | 16 | app(DumpRecorder::class)->register(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/RayProxy.php: -------------------------------------------------------------------------------- 1 | methodsCalled[] = compact('method', 'arguments'); 15 | } 16 | 17 | public function applyCalledMethods(Ray $ray) 18 | { 19 | foreach ($this->methodsCalled as $methodCalled) { 20 | call_user_func_array([$ray, $methodCalled['method']], $methodCalled['arguments']); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: 2 6 | paths: 7 | - src 8 | 9 | ignoreErrors: 10 | - '#^Call to method \w+\(\) on an unknown class Spatie\\YiiRay\\Ray\.$#' 11 | - '#^Call to method \w+\(\) on an unknown class Spatie\\WordPressRay\\Ray\.$#' 12 | - '#^Call to method \w+\(\) on an unknown class Spatie\\RayBundle\\Ray\.$#' 13 | - '#^Access to an undefined property Spatie\\Ray\\Settings\\Settings\:\:\$\w+\.$#' 14 | - 15 | message: '#Unsafe usage of new static\(\).#' 16 | reportUnmatched: false 17 | -------------------------------------------------------------------------------- /src/DumpRecorder/MultiDumpHandler.php: -------------------------------------------------------------------------------- 1 | handlers as $handler) { 13 | $handler($value); 14 | } 15 | } 16 | 17 | public function addHandler(?callable $callable = null): self 18 | { 19 | $this->handlers[] = $callable; 20 | 21 | return $this; 22 | } 23 | 24 | public function resetHandlers(): void 25 | { 26 | $this->handlers = []; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Watchers/DeleteQueryWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_delete_queries_to_ray ?? false; 16 | 17 | $this->setConditionalCallback(function (QueryExecuted $query) { 18 | return Str::startsWith(strtolower($query->sql), 'delete'); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Watchers/InsertQueryWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_insert_queries_to_ray ?? false; 16 | 17 | $this->setConditionalCallback(function (QueryExecuted $query) { 18 | return Str::startsWith(strtolower($query->sql), 'insert'); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Watchers/SelectQueryWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_select_queries_to_ray ?? false; 16 | 17 | $this->setConditionalCallback(function (QueryExecuted $query) { 18 | return Str::startsWith(strtolower($query->sql), 'select'); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Watchers/UpdateQueryWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_update_queries_to_ray ?? false; 16 | 17 | $this->setConditionalCallback(function (QueryExecuted $query) { 18 | return Str::startsWith(strtolower($query->sql), 'update'); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Watchers/EventWatcher.php: -------------------------------------------------------------------------------- 1 | enabled()) { 15 | return; 16 | } 17 | 18 | $payload = new EventPayload($eventName, $arguments); 19 | 20 | $ray = app(Ray::class)->sendRequest($payload); 21 | 22 | optional($this->rayProxy)->applyCalledMethods($ray); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Watchers/DeprecatedNoticeWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_deprecated_notices_to_ray; 20 | 21 | if ($this->enabled()) { 22 | return false; 23 | } 24 | 25 | return Str::contains($messageLogged->message, ['deprecated', 'Deprecated', '[\ReturnTypeWillChange]']); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Watchers/ViewWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_views_to_ray; 16 | 17 | Event::listen('composing:*', function ($event, $data) { 18 | if (! $this->enabled()) { 19 | return; 20 | } 21 | 22 | /** @var \Illuminate\View\View $view */ 23 | $view = $data[0]; 24 | 25 | $ray = app(Ray::class)->view($view); 26 | 27 | optional($this->rayProxy)->applyCalledMethods($ray); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Watchers/Watcher.php: -------------------------------------------------------------------------------- 1 | enabled; 20 | } 21 | 22 | public function enable(): Watcher 23 | { 24 | $this->enabled = true; 25 | 26 | return $this; 27 | } 28 | 29 | public function disable(): Watcher 30 | { 31 | $this->enabled = false; 32 | 33 | return $this; 34 | } 35 | 36 | public function setRayProxy(RayProxy $rayProxy): Watcher 37 | { 38 | $this->rayProxy = $rayProxy; 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Payloads/QueryPayload.php: -------------------------------------------------------------------------------- 1 | query = $query; 16 | } 17 | 18 | public function getType(): string 19 | { 20 | return 'executed_query'; 21 | } 22 | 23 | public function getContent(): array 24 | { 25 | if (method_exists($this->query, 'toRawSql')) { 26 | return [ 27 | 'sql' => $this->query->toRawSql(), 28 | ]; 29 | } 30 | 31 | return [ 32 | 'sql' => $this->query->toSql(), 33 | 'bindings' => $this->query->getBindings(), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Watchers/SlowQueryWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_slow_queries_to_ray ?? false; 17 | $this->minimumTimeInMs = $settings->slow_query_threshold_in_ms ?? $this->minimumTimeInMs; 18 | 19 | $this->setConditionalCallback(function (QueryExecuted $query) { 20 | return $query->time >= $this->minimumTimeInMs; 21 | }); 22 | } 23 | 24 | public function setMinimumTimeInMilliseconds(float $milliseconds): self 25 | { 26 | $this->minimumTimeInMs = $milliseconds; 27 | 28 | return $this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Payloads/ModelPayload.php: -------------------------------------------------------------------------------- 1 | model = $model; 17 | } 18 | 19 | public function getType(): string 20 | { 21 | return 'eloquent_model'; 22 | } 23 | 24 | public function getContent(): array 25 | { 26 | if (! $this->model) { 27 | return []; 28 | } 29 | 30 | $content = [ 31 | 'class_name' => get_class($this->model), 32 | 'attributes' => ArgumentConverter::convertToPrimitive($this->model->attributesToArray()), 33 | ]; 34 | 35 | $relations = $this->model->relationsToArray(); 36 | 37 | if (count($relations)) { 38 | $content['relations'] = ArgumentConverter::convertToPrimitive($relations); 39 | } 40 | 41 | return $content; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Watchers/JobWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_jobs_to_ray; 21 | 22 | Event::listen([ 23 | JobQueued::class, 24 | JobProcessing::class, 25 | JobProcessed::class, 26 | JobFailed::class, 27 | ], function (object $event) { 28 | if (! $this->enabled()) { 29 | return; 30 | } 31 | 32 | $payload = new JobEventPayload($event); 33 | 34 | $ray = app(Ray::class)->sendRequest($payload); 35 | 36 | optional($this->rayProxy)->applyCalledMethods($ray); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Payloads/EventPayload.php: -------------------------------------------------------------------------------- 1 | eventName = $eventName; 22 | 23 | class_exists($eventName) 24 | ? $this->event = $payload[0] 25 | : $this->payload = $payload; 26 | } 27 | 28 | public function getType(): string 29 | { 30 | return 'event'; 31 | } 32 | 33 | public function getContent(): array 34 | { 35 | return [ 36 | 'name' => $this->eventName, 37 | 'event' => $this->event ? ArgumentConverter::convertToPrimitive($this->event) : null, 38 | 'payload' => count($this->payload) ? ArgumentConverter::convertToPrimitive($this->payload) : null, 39 | 'class_based_event' => ! is_null($this->event), 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Support/Composer.php: -------------------------------------------------------------------------------- 1 | $packages 16 | * @param string|null $composerBinary 17 | * @return bool 18 | */ 19 | public function requirePackages(array $packages, bool $dev = false, Closure|OutputInterface|null $output = null, $composerBinary = null) 20 | { 21 | $command = collect([ 22 | ...$this->findComposer($composerBinary), 23 | 'require', 24 | ...$packages, 25 | ]) 26 | ->when($dev, function ($command) { 27 | $command->push('--dev'); 28 | })->all(); 29 | 30 | return $this->getProcess($command, ['COMPOSER_MEMORY_LIMIT' => '-1']) 31 | ->run( 32 | $output instanceof OutputInterface 33 | ? function ($type, $line) use ($output) { 34 | $output->write(' '.$line); 35 | } : $output 36 | ) === 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/CleanRayCommand.php: -------------------------------------------------------------------------------- 1 | requirePackages(['rector/rector'], true, $this->output); 32 | } 33 | 34 | $this->withProgressBar($directories, function ($directory) { 35 | $result = Process::run('./vendor/bin/remove-ray.sh '.$directory); 36 | 37 | if (! $result->successful()) { 38 | $this->error($result->errorOutput()); 39 | 40 | return; 41 | } 42 | }); 43 | 44 | $this->newLine(2); 45 | $this->info('All Ray calls have been removed from your codebase.'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Payloads/JobEventPayload.php: -------------------------------------------------------------------------------- 1 | event = $event; 23 | 24 | // Some queue drivers use an intermediate job with the orignal job stored inside. 25 | // For other drivers, the job is not altered, and it can be used directly. 26 | $this->job = $event->job instanceof Job 27 | ? unserialize($event->job->payload()['data']['command']) 28 | : $this->job = $event->job; 29 | 30 | if (property_exists($event, 'exception')) { 31 | $this->exception = $event->exception ?? null; 32 | } 33 | } 34 | 35 | public function getType(): string 36 | { 37 | return 'job_event'; 38 | } 39 | 40 | public function getContent(): array 41 | { 42 | return [ 43 | 'event_name' => class_basename($this->event), 44 | 'job' => $this->job ? ArgumentConverter::convertToPrimitive($this->job) : null, 45 | 'exception' => $this->exception ? ArgumentConverter::convertToPrimitive($this->exception) : null, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Payloads/ViewPayload.php: -------------------------------------------------------------------------------- 1 | view = $view; 18 | } 19 | 20 | public function getType(): string 21 | { 22 | return 'view'; 23 | } 24 | 25 | public function getContent(): array 26 | { 27 | return [ 28 | 'view_path' => $this->view->getPath(), 29 | 'view_path_relative_to_project_root' => Str::after($this->pathRelativeToProjectRoot($this->view), '/'), 30 | 'data' => ArgumentConverter::convertToPrimitive($this->getData($this->view)), 31 | ]; 32 | } 33 | 34 | protected function pathRelativeToProjectRoot(View $view): string 35 | { 36 | $path = $view->getPath(); 37 | 38 | if (Str::startsWith($path, base_path())) { 39 | $path = substr($path, strlen(base_path())); 40 | } 41 | 42 | return $path; 43 | } 44 | 45 | protected function getData(View $view): array 46 | { 47 | return collect($view->getData()) 48 | ->filter(function ($value, $key) { 49 | return ! in_array($key, ['app', '__env', 'obLevel', 'errors']); 50 | }) 51 | ->toArray(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Payloads/ExecutedQueryPayload.php: -------------------------------------------------------------------------------- 1 | query = $query; 16 | } 17 | 18 | public function getType(): string 19 | { 20 | return 'executed_query'; 21 | } 22 | 23 | public function getContent(): array 24 | { 25 | $grammar = $this->query->connection->getQueryGrammar(); 26 | 27 | $properties = method_exists($grammar, 'substituteBindingsIntoRawSql') ? [ 28 | 'sql' => $grammar->substituteBindingsIntoRawSql( 29 | $this->query->sql, 30 | $this->query->connection->prepareBindings($this->query->bindings) 31 | ), 32 | ] : [ 33 | 'sql' => $this->query->sql, 34 | 'bindings' => $this->query->bindings, 35 | ]; 36 | 37 | if ($this->hasAllProperties()) { 38 | $properties = array_merge($properties, [ 39 | 'connection_name' => $this->query->connectionName, 40 | 'time' => $this->query->time, 41 | ]); 42 | } 43 | 44 | return $properties; 45 | } 46 | 47 | protected function hasAllProperties(): bool 48 | { 49 | return ! is_null($this->query->time); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Payloads/CachePayload.php: -------------------------------------------------------------------------------- 1 | type = $type; 28 | 29 | $this->key = $key; 30 | 31 | $this->tags = is_array($tags) ? $tags : [$tags]; 32 | 33 | $this->value = $value; 34 | 35 | $this->expirationInSeconds = $expirationInSeconds; 36 | } 37 | 38 | public function getType(): string 39 | { 40 | return 'table'; 41 | } 42 | 43 | public function getContent(): array 44 | { 45 | $values = array_filter([ 46 | 'Event' => ''.$this->type.'', 47 | 'Key' => $this->key, 48 | 'Value' => ArgumentConverter::convertToPrimitive($this->value), 49 | 'Tags' => count($this->tags) ? ArgumentConverter::convertToPrimitive($this->tags) : null, 50 | 'Expiration in seconds' => $this->expirationInSeconds, 51 | ]); 52 | 53 | return [ 54 | 'values' => $values, 55 | 'label' => 'Cache', 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Commands/PublishConfigCommand.php: -------------------------------------------------------------------------------- 1 | exists('ray.php')) { 18 | $this->error('ray.php already exists in the project root'); 19 | 20 | return; 21 | } 22 | 23 | copy(__DIR__.'/../../stub/ray.php', base_path('ray.php')); 24 | 25 | if ($this->option('docker')) { 26 | file_put_contents( 27 | base_path('ray.php'), 28 | str_replace( 29 | "'host' => env('RAY_HOST', 'localhost')", 30 | "'host' => env('RAY_HOST', 'host.docker.internal')", 31 | file_get_contents(base_path('ray.php')) 32 | ) 33 | ); 34 | } 35 | 36 | if ($this->option('homestead')) { 37 | file_put_contents( 38 | base_path('ray.php'), 39 | str_replace( 40 | "'host' => env('RAY_HOST', 'localhost')", 41 | "'host' => env('RAY_HOST', '10.0.2.2')", 42 | file_get_contents(base_path('ray.php')) 43 | ) 44 | ); 45 | } 46 | 47 | $this->info('`ray.php` created in the project base directory'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Watchers/ConditionalQueryWatcher.php: -------------------------------------------------------------------------------- 1 | setConditionalCallback($condition); 20 | 21 | return app()->instance(static::abstractName($name), $watcher); 22 | } 23 | 24 | public static function abstractName(string $name) 25 | { 26 | return static::class.':'.$name; 27 | } 28 | 29 | public function setConditionalCallback($conditionalCallback) 30 | { 31 | $this->conditionalCallback = $conditionalCallback; 32 | 33 | $this->listen(); 34 | } 35 | 36 | public function register(): void 37 | { 38 | throw new BadMethodCallException('ConditionalQueryWatcher cannot be registered. Only its child classes.'); 39 | } 40 | 41 | public function listen(): void 42 | { 43 | Event::listen(QueryExecuted::class, function (QueryExecuted $query) { 44 | if (! $this->enabled()) { 45 | return; 46 | } 47 | 48 | if (! $this->conditionalCallback) { 49 | return; 50 | } 51 | 52 | $ray = app(Ray::class); 53 | 54 | if (($this->conditionalCallback)($query)) { 55 | $payload = new ExecutedQueryPayload($query); 56 | 57 | $ray->sendRequest($payload); 58 | } 59 | 60 | optional($this->rayProxy)->applyCalledMethods($ray); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Debug with Ray to fix problems faster 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-ray.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-ray) 4 | ![Tests](https://github.com/spatie/laravel-ray/workflows/Tests/badge.svg) 5 | [![PHPStan](https://github.com/spatie/laravel-ray/actions/workflows/phpstan.yml/badge.svg)](https://github.com/spatie/laravel-ray/actions/workflows/phpstan.yml) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-ray.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-ray) 7 | 8 | This package can be installed in any PHP application to send messages to [the Ray app](https://myray.app). 9 | 10 | [](https://spatie.be/github-ad-click/laravel-ray) 11 | 12 | The desktop app: 13 | 14 | - can be used in WordPress, Laravel, PHP, JavaScript function 15 | - shows you models, mails, queries, ... IN Laravel 16 | - helps you to debug locally or via SSH 17 | - lets you measure performance & set breakpoints 18 | 19 | 20 | ## Documentation 21 | 22 | You can find the full documentation on [our documentation site](https://spatie.be/docs/ray). 23 | 24 | ## Testing 25 | 26 | ``` bash 27 | composer test 28 | ``` 29 | 30 | ## Changelog 31 | 32 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 33 | 34 | ## Contributing 35 | 36 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 37 | 38 | ## Security Vulnerabilities 39 | 40 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 41 | 42 | ## Credits 43 | 44 | - [Freek Van der Herten](https://github.com/freekmurze) 45 | - [All Contributors](../../contributors) 46 | 47 | ## License 48 | 49 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 50 | -------------------------------------------------------------------------------- /src/Payloads/ResponsePayload.php: -------------------------------------------------------------------------------- 1 | getStatusCode(), 27 | $testResponse->headers->all(), 28 | $testResponse->content(), 29 | $json = rescue(function () use ($testResponse) { 30 | return $testResponse->json(); 31 | }, null, false) 32 | ); 33 | } 34 | 35 | public function __construct(int $statusCode, array $headers, string $content, ?array $json = null) 36 | { 37 | $this->statusCode = $statusCode; 38 | 39 | $this->headers = $this->normalizeHeaders($headers); 40 | 41 | $this->content = $content; 42 | 43 | $this->json = $json; 44 | } 45 | 46 | public function getType(): string 47 | { 48 | return 'response'; 49 | } 50 | 51 | public function getContent(): array 52 | { 53 | return [ 54 | 'status_code' => $this->statusCode, 55 | 'headers' => ArgumentConverter::convertToPrimitive($this->headers), 56 | 'content' => $this->content, 57 | 'json' => ArgumentConverter::convertToPrimitive($this->json), 58 | ]; 59 | } 60 | 61 | protected function normalizeHeaders(array $headers): array 62 | { 63 | return collect($headers) 64 | ->map(function (array $values) { 65 | return $values[0] ?? null; 66 | }) 67 | ->filter() 68 | ->toArray(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Payloads/MailablePayload.php: -------------------------------------------------------------------------------- 1 | html = $html; 25 | 26 | $this->mailable = $mailable; 27 | } 28 | 29 | public function getType(): string 30 | { 31 | return 'mailable'; 32 | } 33 | 34 | public function getContent(): array 35 | { 36 | $content = [ 37 | 'html' => $this->html, 38 | 'from' => [], 39 | 'to' => [], 40 | 'cc' => [], 41 | 'bcc' => [], 42 | ]; 43 | 44 | if ($this->mailable) { 45 | $content = array_merge($content, [ 46 | 'mailable_class' => get_class($this->mailable), 47 | 'from' => $this->convertToPersons($this->mailable->from), 48 | 'subject' => $this->mailable->subject, 49 | 'to' => $this->convertToPersons($this->mailable->to), 50 | 'cc' => $this->convertToPersons($this->mailable->cc), 51 | 'bcc' => $this->convertToPersons($this->mailable->bcc), 52 | ]); 53 | } 54 | 55 | return $content; 56 | } 57 | 58 | protected static function renderMailable(Mailable $mailable): string 59 | { 60 | try { 61 | return $mailable->render(); 62 | } catch (Throwable $exception) { 63 | return "Mailable could not be rendered because {$exception->getMessage()}"; 64 | } 65 | } 66 | 67 | protected function convertToPersons(array $persons): array 68 | { 69 | return collect($persons) 70 | ->map(function (array $person) { 71 | return [ 72 | 'email' => $person['address'], 73 | 'name' => $person['name'] ?? '', 74 | ]; 75 | }) 76 | ->toArray(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Watchers/ApplicationLogWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $ray->settings->send_log_calls_to_ray; 18 | 19 | Event::listen(MessageLogged::class, function (MessageLogged $message) { 20 | if (! $this->shouldLogMessage($message)) { 21 | return; 22 | } 23 | 24 | if (! class_exists('Spatie\Ray\Payloads\ApplicationLogPayload')) { 25 | return; 26 | } 27 | 28 | $payload = new ApplicationLogPayload($message->message, $message->context); 29 | 30 | /** @var Ray $ray */ 31 | $ray = app(Ray::class); 32 | 33 | $ray->sendRequest($payload); 34 | 35 | switch ($message->level) { 36 | case 'error': 37 | case 'critical': 38 | case 'alert': 39 | case 'emergency': 40 | $ray->color('red'); 41 | 42 | break; 43 | case 'warning': 44 | $ray->color('orange'); 45 | 46 | break; 47 | } 48 | }); 49 | } 50 | 51 | protected function shouldLogMessage(MessageLogged $message): bool 52 | { 53 | if (! $this->enabled()) { 54 | return false; 55 | } 56 | 57 | if (is_null($message->message)) { 58 | return false; 59 | } 60 | 61 | /** @var Ray $ray */ 62 | $ray = app(Ray::class); 63 | 64 | if (! $ray->settings->send_log_calls_to_ray) { 65 | return false; 66 | } 67 | 68 | if ((new ExceptionWatcher)->concernsException($message)) { 69 | return false; 70 | } 71 | 72 | if ((new MailWatcher)->concernsLoggedMail($message)) { 73 | return false; 74 | } 75 | 76 | if ((new DeprecatedNoticeWatcher)->concernsDeprecatedNotice($message)) { 77 | return false; 78 | } 79 | 80 | return true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Watchers/DuplicateQueryWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_duplicate_queries_to_ray; 23 | 24 | Event::listen(QueryExecuted::class, function (QueryExecuted $query) { 25 | if (! $this->enabled()) { 26 | return; 27 | } 28 | 29 | $sql = Str::replaceArray('?', $this->cleanupBindings($query->bindings), $query->sql); 30 | 31 | $duplicated = in_array($sql, $this->executedQueries); 32 | 33 | $this->executedQueries[] = $sql; 34 | 35 | if (! $duplicated) { 36 | return; 37 | } 38 | 39 | $payload = new ExecutedQueryPayload($query); 40 | 41 | $ray = app(Ray::class)->sendRequest($payload); 42 | 43 | optional($this->rayProxy)->applyCalledMethods($ray); 44 | }); 45 | } 46 | 47 | private function cleanupBindings(array $bindings): array 48 | { 49 | return array_map(function ($binding) { 50 | if ($binding instanceof \DateTimeInterface) { 51 | return $binding->format('Y-m-d H:i:s'); 52 | } 53 | 54 | return $binding; 55 | }, $bindings); 56 | } 57 | 58 | public function enable(): Watcher 59 | { 60 | if (app()->bound('db')) { 61 | collect(DB::getConnections())->each(function ($connection) { 62 | $connection->enableQueryLog(); 63 | }); 64 | } 65 | 66 | parent::enable(); 67 | 68 | return $this; 69 | } 70 | 71 | public function getExecutedQueries(): array 72 | { 73 | return $this->executedQueries; 74 | } 75 | 76 | public function disable(): Watcher 77 | { 78 | DB::disableQueryLog(); 79 | 80 | parent::disable(); 81 | 82 | return $this; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Watchers/MailWatcher.php: -------------------------------------------------------------------------------- 1 | send_mails_to_ray ?? true) { 20 | $this->enable(); 21 | } 22 | 23 | $this->supportsMessageSendingEvent() 24 | ? $this->registerMessageSendingEventListener() 25 | : $this->listenForLoggedMails(); 26 | } 27 | 28 | protected function registerMessageSendingEventListener(): void 29 | { 30 | Event::listen([ 31 | MessageSending::class, 32 | ], function (MessageSending $event) { 33 | if (! $this->enabled()) { 34 | return; 35 | } 36 | 37 | $payload = new MailablePayload($event->message->getHtmlBody() ?? $event->message->getTextBody()); 38 | 39 | $ray = app(Ray::class)->sendRequest($payload); 40 | 41 | optional($this->rayProxy)->applyCalledMethods($ray); 42 | }); 43 | } 44 | 45 | public function listenForLoggedMails(): void 46 | { 47 | Event::listen(MessageLogged::class, function (MessageLogged $messageLogged) { 48 | if (! $this->enabled()) { 49 | return; 50 | } 51 | 52 | if (! $this->concernsLoggedMail($messageLogged)) { 53 | return; 54 | } 55 | 56 | /** @var Ray $ray */ 57 | $ray = app(Ray::class); 58 | 59 | $ray->loggedMail($messageLogged->message); 60 | }); 61 | } 62 | 63 | public function concernsLoggedMail(MessageLogged $messageLogged): bool 64 | { 65 | if (! Str::contains($messageLogged->message, 'Message-ID')) { 66 | return false; 67 | } 68 | 69 | if (! Str::contains($messageLogged->message, 'To:')) { 70 | return false; 71 | } 72 | 73 | return true; 74 | } 75 | 76 | public function supportsMessageSendingEvent(): bool 77 | { 78 | return version_compare(app()->version(), '11.0.0', '>='); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Watchers/CacheWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_cache_to_ray; 20 | 21 | app('events')->listen(CacheHit::class, function (CacheHit $event) { 22 | if (! $this->enabled()) { 23 | return; 24 | } 25 | 26 | $payload = new CachePayload('Hit', $event->key, $event->tags, $event->value); 27 | 28 | $ray = $this->ray()->sendRequest($payload); 29 | 30 | optional($this->rayProxy)->applyCalledMethods($ray); 31 | }); 32 | 33 | app('events')->listen(CacheMissed::class, function (CacheMissed $event) { 34 | if (! $this->enabled()) { 35 | return; 36 | } 37 | 38 | $payload = new CachePayload('Missed', $event->key, $event->tags); 39 | 40 | $this->ray()->sendRequest($payload); 41 | }); 42 | 43 | app('events')->listen(KeyWritten::class, function (KeyWritten $event) { 44 | if (! $this->enabled()) { 45 | return; 46 | } 47 | 48 | $payload = new CachePayload( 49 | 'Key written', 50 | $event->key, 51 | $event->tags, 52 | $event->value, 53 | $this->formatExpiration($event), 54 | ); 55 | 56 | $this->ray()->sendRequest($payload); 57 | }); 58 | 59 | app('events')->listen(KeyForgotten::class, function (KeyForgotten $event) { 60 | if (! $this->enabled()) { 61 | return; 62 | } 63 | 64 | $payload = new CachePayload( 65 | 'Key forgotten', 66 | $event->key, 67 | $event->tags 68 | ); 69 | 70 | $this->ray()->sendRequest($payload); 71 | }); 72 | } 73 | 74 | protected function formatExpiration(KeyWritten $event): ?int 75 | { 76 | return $event->seconds; 77 | } 78 | 79 | public function ray(): Ray 80 | { 81 | return app(Ray::class); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Payloads/MarkdownPayload.php: -------------------------------------------------------------------------------- 1 | markdown = $markdown; 16 | } 17 | 18 | public function getType(): string 19 | { 20 | return 'custom'; 21 | } 22 | 23 | public function getContent(): array 24 | { 25 | return [ 26 | 'content' => $this->markdownToHtml($this->markdown), 27 | 'label' => 'Markdown', 28 | ]; 29 | } 30 | 31 | protected function markdownToHtml(string $markdown): string 32 | { 33 | $converter = new GithubFlavoredMarkdownConverter([ 34 | 'renderer' => [ 35 | 'block_separator' => "
\n", 36 | ], 37 | 'html_input' => 'allow', 38 | 'allow_unsafe_links' => false, 39 | ]); 40 | 41 | $html = $converter->convertToHtml($markdown); 42 | $html = $this->processCodeBlocks($html); 43 | $html = $this->processHeaderTags($html); 44 | $css = $this->getCustomStyles(); 45 | 46 | return trim("{$css}{$html}"); 47 | } 48 | 49 | protected function getCustomStyles(): string 50 | { 51 | // render links as underlined 52 | return ''; 53 | } 54 | 55 | protected function processCodeBlocks($html): string 56 | { 57 | // format code blocks background color, padding, and display width; the background 58 | // color changes based on light or dark app theme. 59 | return str_replace('
', '

', '

', '

'], [ 66 | '
', 67 | '
', 68 | '
', 69 | '
', 70 | ], $html); 71 | 72 | // replace closing header tags with closing div tags 73 | return preg_replace('~~', '
', $html); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Watchers/QueryWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_queries_to_ray; 28 | 29 | Event::listen(QueryExecuted::class, function (QueryExecuted $query) { 30 | if (! $this->enabled()) { 31 | return; 32 | } 33 | 34 | if ($this->keepExecutedQueries) { 35 | $this->executedQueries[] = $query; 36 | } 37 | 38 | if (! $this->sendIndividualQueries) { 39 | return; 40 | } 41 | 42 | $payload = new ExecutedQueryPayload($query); 43 | 44 | $ray = app(Ray::class)->sendRequest($payload); 45 | 46 | optional($this->rayProxy)->applyCalledMethods($ray); 47 | }); 48 | } 49 | 50 | public function enable(): Watcher 51 | { 52 | if (app()->bound('db')) { 53 | collect(DB::getConnections())->each(function ($connection) { 54 | $connection->enableQueryLog(); 55 | }); 56 | } 57 | 58 | parent::enable(); 59 | 60 | return $this; 61 | } 62 | 63 | public function keepExecutedQueries(): self 64 | { 65 | $this->keepExecutedQueries = true; 66 | 67 | return $this; 68 | } 69 | 70 | public function getExecutedQueries(): array 71 | { 72 | return $this->executedQueries; 73 | } 74 | 75 | public function sendIndividualQueries(): self 76 | { 77 | $this->sendIndividualQueries = true; 78 | 79 | return $this; 80 | } 81 | 82 | public function doNotSendIndividualQueries(): self 83 | { 84 | $this->sendIndividualQueries = false; 85 | 86 | return $this; 87 | } 88 | 89 | public function stopKeepingAndClearExecutedQueries(): self 90 | { 91 | $this->keepExecutedQueries = false; 92 | 93 | $this->executedQueries = []; 94 | 95 | return $this; 96 | } 97 | 98 | public function disable(): Watcher 99 | { 100 | DB::disableQueryLog(); 101 | 102 | parent::disable(); 103 | 104 | return $this; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-ray", 3 | "description": "Easily debug Laravel apps", 4 | "license": "MIT", 5 | "keywords": [ 6 | "spatie", 7 | "laravel-ray" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Freek Van der Herten", 12 | "email": "freek@spatie.be", 13 | "homepage": "https://spatie.be", 14 | "role": "Developer" 15 | } 16 | ], 17 | "homepage": "https://github.com/spatie/laravel-ray", 18 | "funding": [ 19 | { 20 | "type": "github", 21 | "url": "https://github.com/sponsors/spatie" 22 | }, 23 | { 24 | "type": "other", 25 | "url": "https://spatie.be/open-source/support-us" 26 | } 27 | ], 28 | "require": { 29 | "php": "^7.4|^8.0", 30 | "ext-json": "*", 31 | "composer-runtime-api": "^2.2", 32 | "illuminate/contracts": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0", 33 | "illuminate/database": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0", 34 | "illuminate/queue": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0", 35 | "illuminate/support": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0", 36 | "spatie/backtrace": "^1.7.1", 37 | "spatie/ray": "^1.44.0", 38 | "symfony/stopwatch": "4.2|^5.1|^6.0|^7.0|^8.0", 39 | "zbateson/mail-mime-parser": "^1.3.1|^2.0|^3.0" 40 | }, 41 | "require-dev": { 42 | "guzzlehttp/guzzle": "^7.3", 43 | "laravel/framework": "^7.20|^8.19|^9.0|^10.0|^11.0|^12.0", 44 | "laravel/pint": "^1.26", 45 | "orchestra/testbench-core": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", 46 | "pestphp/pest": "^1.22|^2.0|^3.0|^4.0", 47 | "phpstan/phpstan": "^1.10.57|^2.0.2", 48 | "phpunit/phpunit": "^9.3|^10.1|^11.0.10|^12.4", 49 | "rector/rector": "^0.19.2|^1.0.1|^2.0.0", 50 | "spatie/pest-plugin-snapshots": "^1.1|^2.0", 51 | "symfony/var-dumper": "^4.2|^5.1|^6.0|^7.0.3|^8.0" 52 | }, 53 | "minimum-stability": "dev", 54 | "prefer-stable": true, 55 | "autoload": { 56 | "psr-4": { 57 | "Spatie\\LaravelRay\\": "src" 58 | } 59 | }, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "Spatie\\LaravelRay\\Tests\\": "tests" 63 | } 64 | }, 65 | "config": { 66 | "allow-plugins": { 67 | "pestphp/pest-plugin": true 68 | }, 69 | "sort-packages": true 70 | }, 71 | "extra": { 72 | "branch-alias": { 73 | "dev-main": "1.x-dev" 74 | }, 75 | "laravel": { 76 | "providers": [ 77 | "Spatie\\LaravelRay\\RayServiceProvider" 78 | ] 79 | } 80 | }, 81 | "scripts": { 82 | "analyse": "vendor/bin/phpstan", 83 | "format": "vendor/bin/pint", 84 | "test": "vendor/bin/pest", 85 | "test-coverage": "vendor/bin/pest --coverage" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Call to an undefined method Spatie\\\\LaravelRay\\\\OriginFactory\\:\\:returnTinkerFrame\\(\\)\\.$#" 5 | count: 1 6 | path: src/OriginFactory.php 7 | 8 | - 9 | message: "#^Variable \\$foundFrame in PHPDoc tag @var does not exist\\.$#" 10 | count: 2 11 | path: src/OriginFactory.php 12 | 13 | - 14 | message: "#^Call to method createReport\\(\\) on an unknown class Facade\\\\FlareClient\\\\Flare\\.$#" 15 | count: 1 16 | path: src/Watchers/ExceptionWatcher.php 17 | 18 | - 19 | message: "#^Call to method createReport\\(\\) on an unknown class Spatie\\\\FlareClient\\\\Flare\\.$#" 20 | count: 1 21 | path: src/Watchers/ExceptionWatcher.php 22 | 23 | - 24 | message: "#^Call to method trim\\(\\) on an unknown class Facade\\\\FlareClient\\\\Truncation\\\\ReportTrimmer\\.$#" 25 | count: 1 26 | path: src/Watchers/ExceptionWatcher.php 27 | 28 | - 29 | message: "#^Call to method trim\\(\\) on an unknown class Spatie\\\\FlareClient\\\\Truncation\\\\ReportTrimmer\\.$#" 30 | count: 1 31 | path: src/Watchers/ExceptionWatcher.php 32 | 33 | - 34 | message: "#^Class Facade\\\\FlareClient\\\\Flare not found\\.$#" 35 | count: 2 36 | path: src/Watchers/ExceptionWatcher.php 37 | 38 | - 39 | message: "#^Class Spatie\\\\FlareClient\\\\Flare not found\\.$#" 40 | count: 2 41 | path: src/Watchers/ExceptionWatcher.php 42 | 43 | - 44 | message: "#^Instantiated class Facade\\\\FlareClient\\\\Truncation\\\\ReportTrimmer not found\\.$#" 45 | count: 1 46 | path: src/Watchers/ExceptionWatcher.php 47 | 48 | - 49 | message: "#^Instantiated class Spatie\\\\FlareClient\\\\Truncation\\\\ReportTrimmer not found\\.$#" 50 | count: 1 51 | path: src/Watchers/ExceptionWatcher.php 52 | 53 | - 54 | message: "#^PHPDoc tag @var for variable \\$flare contains unknown class Facade\\\\FlareClient\\\\Flare\\.$#" 55 | count: 1 56 | path: src/Watchers/ExceptionWatcher.php 57 | 58 | - 59 | message: "#^Access to an undefined property Illuminate\\\\Support\\\\Optional::\\$file.$#" 60 | count: 1 61 | path: src/OriginFactory.php 62 | 63 | - 64 | message: "#^Access to an undefined property Illuminate\\\\Support\\\\Optional::\\$lineNumber.$#" 65 | count: 1 66 | path: src/OriginFactory.php 67 | 68 | - 69 | message: "#^Call to an undefined method Illuminate\\\\Support\\\\Optional::applyCalledMethods\\(\\).$#" 70 | count: 11 71 | path: src/ 72 | 73 | - 74 | message: "#^Call to an undefined method Illuminate\\\\Support\\\\Optional::getActionName\\(\\).$#" 75 | count: 1 76 | path: src/Watchers/RequestWatcher.php 77 | 78 | - 79 | message: "#^Call to an undefined method Illuminate\\\\Support\\\\Optional::gatherMiddleware\\(\\).$#" 80 | count: 1 81 | path: src/Watchers/RequestWatcher.php 82 | 83 | - 84 | message: "#^Access to protected property Illuminate\\\\Support\\\\Collection::\\$items.$#" 85 | count: 2 86 | path: src/RayServiceProvider.php 87 | 88 | - 89 | message: "#^Access to protected property Illuminate\\\\Support\\\\Stringable::\\$value.$#" 90 | count: 2 91 | path: src/RayServiceProvider.php 92 | -------------------------------------------------------------------------------- /src/Payloads/EnvironmentPayload.php: -------------------------------------------------------------------------------- 1 | environmentFilePath(); 27 | 28 | $this->path = dirname($filename); 29 | 30 | $this->filename = basename($filename); 31 | 32 | $this->values = $this->getDotEnvValues($onlyShowNames); 33 | } 34 | 35 | public function getType(): string 36 | { 37 | return 'table'; 38 | } 39 | 40 | public function getContent(): array 41 | { 42 | $values = array_map(function ($value) { 43 | $value = $this->decorateSpecialValues($value); 44 | 45 | return ArgumentConverter::convertToPrimitive($value); 46 | }, $this->values); 47 | 48 | return [ 49 | 'values' => $values, 50 | 'label' => '.env', 51 | ]; 52 | } 53 | 54 | protected function decorateSpecialValues($value) 55 | { 56 | if ($value === '') { 57 | return '
(empty)
'; 58 | } 59 | 60 | if ($value === 'null' || $value === 'NULL') { 61 | return '
NULL
'; 62 | } 63 | 64 | if ($value === 'true' || $value === 'false') { 65 | $color = $value === 'true' ? 'green' : 'red'; 66 | 67 | return "
{$value}
"; 68 | } 69 | 70 | if (preg_match('~^https?://~', $value) === 1) { 71 | return "{$value}"; 72 | } 73 | 74 | if (strpos($value, 'base64:') === 0) { 75 | return "
{$value}
"; 76 | } 77 | 78 | // ip addresses 79 | if (preg_match('~(\d{1,3}\.){3}\d{1,3}~', $value) === 1) { 80 | return "
{$value}
"; 81 | } 82 | 83 | return $value; 84 | } 85 | 86 | protected function loadDotEnv(): array 87 | { 88 | return Dotenv::create( 89 | Env::getRepository(), 90 | $this->path, 91 | $this->filename 92 | )->safeLoad(); 93 | } 94 | 95 | protected function getDotEnvValues(?array $filterNames): array 96 | { 97 | $values = $this->loadDotEnv(); 98 | 99 | if (! $filterNames) { 100 | return $values; 101 | } 102 | 103 | return array_filter($values, function ($value) use ($filterNames) { 104 | return in_array($value, $filterNames, true); 105 | }, ARRAY_FILTER_USE_KEY); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/DumpRecorder/DumpRecorder.php: -------------------------------------------------------------------------------- 1 | app = $app; 26 | 27 | if (static::$runningLaravel9 === null) { 28 | static::$runningLaravel9 = version_compare(app()->version(), '9.0.0', '>='); 29 | } 30 | } 31 | 32 | public function register(): self 33 | { 34 | $multiDumpHandler = new MultiDumpHandler; 35 | 36 | $this->app->singleton(MultiDumpHandler::class, function () use ($multiDumpHandler) { 37 | return $multiDumpHandler; 38 | }); 39 | 40 | if (! static::$registeredHandler || static::$runningLaravel9) { 41 | static::$registeredHandler = true; 42 | 43 | $multiDumpHandler->resetHandlers(); 44 | 45 | $this->ensureOriginalHandlerExists(); 46 | 47 | $originalHandler = VarDumper::setHandler(function ($dumpedVariable) use ($multiDumpHandler) { 48 | $multiDumpHandler->dump($dumpedVariable); 49 | }); 50 | 51 | if ($originalHandler) { 52 | $multiDumpHandler->addHandler($originalHandler); 53 | } 54 | 55 | $multiDumpHandler->addHandler(function ($dumpedVariable) { 56 | if ($this->shouldDump()) { 57 | app(Ray::class)->send($dumpedVariable); 58 | } 59 | }); 60 | } 61 | 62 | return $this; 63 | } 64 | 65 | protected function shouldDump(): bool 66 | { 67 | /** @var Ray $ray */ 68 | $ray = app(Ray::class); 69 | 70 | return $ray->settings->send_dumps_to_ray; 71 | } 72 | 73 | /** 74 | * Only the `VarDumper` knows how to create the orignal HTML or CLI VarDumper. 75 | * Using reflection and the private VarDumper::register() method we can force it 76 | * to create and register a new VarDumper::$handler before we'll overwrite it. 77 | * Of course, we only need to do this if there isn't a registered VarDumper::$handler. 78 | * 79 | * @throws \ReflectionException 80 | */ 81 | protected function ensureOriginalHandlerExists(): void 82 | { 83 | $reflectionProperty = new ReflectionProperty(VarDumper::class, 'handler'); 84 | if (PHP_VERSION_ID < 80100) { 85 | $reflectionProperty->setAccessible(true); 86 | } 87 | $handler = $reflectionProperty->getValue(); 88 | 89 | if (! $handler) { 90 | // No handler registered yet, so we'll force VarDumper to create one. 91 | $reflectionMethod = new ReflectionMethod(VarDumper::class, 'register'); 92 | if (PHP_VERSION_ID < 80100) { 93 | $reflectionMethod->setAccessible(true); 94 | } 95 | $reflectionMethod->invoke(null); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Watchers/HttpClientWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_http_client_requests_to_ray; 25 | 26 | Event::listen(RequestSending::class, function (RequestSending $event) { 27 | if (! $this->enabled()) { 28 | return; 29 | } 30 | 31 | $ray = $this->handleRequest($event->request); 32 | 33 | optional($this->rayProxy)->applyCalledMethods($ray); 34 | }); 35 | 36 | Event::listen(ResponseReceived::class, function (ResponseReceived $event) { 37 | if (! $this->enabled()) { 38 | return; 39 | } 40 | 41 | $ray = $this->handleResponse($event->request, $event->response); 42 | 43 | optional($this->rayProxy)->applyCalledMethods($ray); 44 | }); 45 | } 46 | 47 | protected function handleRequest(Request $request) 48 | { 49 | $payload = new TablePayload([ 50 | 'Method' => $request->method(), 51 | 'URL' => $request->url(), 52 | 'Headers' => $request->headers(), 53 | 'Data' => $request->data(), 54 | 'Body' => $request->body(), 55 | 'Type' => $this->getRequestType($request), 56 | ], 'Http'); 57 | 58 | return app(Ray::class)->sendRequest($payload); 59 | } 60 | 61 | protected function getRequestType(Request $request) 62 | { 63 | if ($request->isJson()) { 64 | return 'Json'; 65 | } 66 | 67 | if ($request->isMultipart()) { 68 | return 'Multipart'; 69 | } 70 | 71 | return 'Form'; 72 | } 73 | 74 | protected function handleResponse(Request $request, Response $response) 75 | { 76 | $payload = new TablePayload([ 77 | 'URL' => $request->url(), 78 | 'Real Request' => ! empty($response->handlerStats()), 79 | 'Success' => $response->successful(), 80 | 'Status' => $response->status(), 81 | 'Headers' => $response->headers(), 82 | 'Body' => rescue(function () use ($response) { 83 | return $response->json(); 84 | }, $response->body(), false), 85 | 'Cookies' => $response->cookies(), 86 | 'Size' => $response->handlerStats()['size_download'] ?? null, 87 | 'Connection time' => $response->handlerStats()['connect_time'] ?? null, 88 | 'Duration' => $response->handlerStats()['total_time'] ?? null, 89 | 'Request Size' => $response->handlerStats()['request_size'] ?? null, 90 | ], 'Http'); 91 | 92 | return app(Ray::class)->sendRequest($payload); 93 | } 94 | 95 | public static function supportedByLaravelVersion() 96 | { 97 | return version_compare(app()->version(), '8.46.0', '>='); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Payloads/LoggedMailPayload.php: -------------------------------------------------------------------------------- 1 | parse($loggedMail, true); 37 | 38 | // get the part in $loggedMail that starts with getHeader(HeaderConsts::FROM)), 45 | $message->getHeaderValue(HeaderConsts::SUBJECT), 46 | self::convertHeaderToPersons($message->getHeader(HeaderConsts::TO)), 47 | self::convertHeaderToPersons($message->getHeader(HeaderConsts::CC)), 48 | self::convertHeaderToPersons($message->getHeader(HeaderConsts::BCC)), 49 | ); 50 | } 51 | 52 | public function __construct( 53 | string $html, 54 | array $from = [], 55 | ?string $subject = null, 56 | array $to = [], 57 | array $cc = [], 58 | array $bcc = [] 59 | ) { 60 | $this->html = $html; 61 | $this->from = $from; 62 | $this->subject = $subject; 63 | $this->to = $to; 64 | $this->cc = $cc; 65 | $this->bcc = $bcc; 66 | } 67 | 68 | protected static function getMailContent(string $loggedMail, IMessage $message): string 69 | { 70 | $startOfHtml = strpos($loggedMail, 'getContent() ?? $message->getHtmlContent() ?? ''; 74 | } 75 | 76 | return substr($loggedMail, $startOfHtml) ?? ''; 77 | } 78 | 79 | public function getType(): string 80 | { 81 | return 'mailable'; 82 | } 83 | 84 | public function getContent(): array 85 | { 86 | return [ 87 | 'html' => $this->sanitizeHtml($this->html), 88 | 'subject' => $this->subject, 89 | 'from' => $this->from, 90 | 'to' => $this->to, 91 | 'cc' => $this->cc, 92 | 'bcc' => $this->bcc, 93 | ]; 94 | } 95 | 96 | protected function sanitizeHtml(string $html): string 97 | { 98 | $needle = 'Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable'; 99 | 100 | if (strpos($html, $needle) !== false) { 101 | $html = substr($html, strpos($html, $needle)); 102 | } 103 | 104 | return $html; 105 | } 106 | 107 | protected static function convertHeaderToPersons(?AddressHeader $header): array 108 | { 109 | if ($header === null) { 110 | return []; 111 | } 112 | 113 | return array_map( 114 | function (AddressPart $address) { 115 | return [ 116 | 'name' => $address->getName(), 117 | 'email' => $address->getEmail(), 118 | ]; 119 | }, 120 | $header->getAddresses() 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Watchers/ExceptionWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_exceptions_to_ray; 25 | 26 | Event::listen(MessageLogged::class, function (MessageLogged $message) { 27 | if (! $this->enabled()) { 28 | return; 29 | } 30 | 31 | if (! $this->concernsException($message)) { 32 | return; 33 | } 34 | 35 | $exception = $message->context['exception']; 36 | 37 | $meta = []; 38 | 39 | if ($flareReport = $this->getFlareReport($exception)) { 40 | $meta['flare_report'] = $flareReport; 41 | } 42 | 43 | $exceptionContext = $this->getRequestAndRouteContext(); 44 | 45 | $meta = array_merge($meta, $exceptionContext); 46 | 47 | /** @var Ray $ray */ 48 | $ray = app(Ray::class); 49 | 50 | $ray->exception( 51 | $exception, 52 | $meta, 53 | ); 54 | }); 55 | } 56 | 57 | public function concernsException(MessageLogged $messageLogged): bool 58 | { 59 | if (! isset($messageLogged->context['exception'])) { 60 | return false; 61 | } 62 | 63 | if (! $messageLogged->context['exception'] instanceof Exception) { 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | 70 | public function getFlareReport(Throwable $exception): ?array 71 | { 72 | if (app()->bound(Flare::class)) { 73 | $flare = app(Flare::class); 74 | 75 | $report = $flare->createReport($exception); 76 | 77 | return (new ReportTrimmer)->trim($report->toArray()); 78 | } 79 | 80 | if (app()->bound(FacadeFlare::class)) { 81 | /** @var \Facade\FlareClient\Flare $flare */ 82 | $flare = app(FacadeFlare::class); 83 | 84 | $report = $flare->createReport($exception); 85 | 86 | return (new FacadeReportTrimmer)->trim($report->toArray()); 87 | } 88 | 89 | return null; 90 | } 91 | 92 | protected function getRequestAndRouteContext(): array 93 | { 94 | return [ 95 | 'request_headers' => $this->getRequestHeaders(), 96 | 'application_route' => $this->getApplicationRouteContext(), 97 | 'application_route_parameters' => $this->getApplicationRouteParameters(), 98 | ]; 99 | } 100 | 101 | /** 102 | * Get the request's headers. 103 | * 104 | * @return array 105 | */ 106 | protected function getRequestHeaders(): array 107 | { 108 | return array_map(function (array $header) { 109 | return implode(', ', $header); 110 | }, request()->headers->all()); 111 | } 112 | 113 | /** 114 | * Get the application's route context. 115 | * 116 | * @return array 117 | */ 118 | protected function getApplicationRouteContext(): array 119 | { 120 | $route = request()->route(); 121 | 122 | return $route ? array_filter([ 123 | 'controller' => $route->getActionName(), 124 | 'route name' => $route->getName() ?: null, 125 | 'middleware' => implode(', ', array_map(function ($middleware) { 126 | return $middleware instanceof Closure ? 'Closure' : $middleware; 127 | }, $route->gatherMiddleware())), 128 | ]) : []; 129 | } 130 | 131 | /** 132 | * Get the application's route parameters context. 133 | */ 134 | protected function getApplicationRouteParameters(): ?string 135 | { 136 | $route = request()->route(); 137 | 138 | $parameters = $route ? $route->parameters() : null; 139 | 140 | return $parameters ? json_encode(array_map( 141 | fn ($value) => $value instanceof Model ? $value->withoutRelations() : $value, 142 | $parameters 143 | ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : null; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /stub/ray.php: -------------------------------------------------------------------------------- 1 | env('RAY_ENABLED', true), 10 | 11 | /* 12 | * When enabled, all cache events will automatically be sent to Ray. 13 | */ 14 | 'send_cache_to_ray' => env('SEND_CACHE_TO_RAY', false), 15 | 16 | /* 17 | * When enabled, all things passed to `dump` or `dd` 18 | * will be sent to Ray as well. 19 | */ 20 | 'send_dumps_to_ray' => env('SEND_DUMPS_TO_RAY', true), 21 | 22 | /* 23 | * When enabled all job events will automatically be sent to Ray. 24 | */ 25 | 'send_jobs_to_ray' => env('SEND_JOBS_TO_RAY', false), 26 | 27 | /* 28 | * When enabled all mails will automatically be sent to Ray. 29 | */ 30 | 'send_mails_to_ray' => env('SEND_MAILS_TO_RAY', true), 31 | 32 | /* 33 | * When enabled, all things logged to the application log 34 | * will be sent to Ray as well. 35 | */ 36 | 'send_log_calls_to_ray' => env('SEND_LOG_CALLS_TO_RAY', true), 37 | 38 | /* 39 | * When enabled, all queries will automatically be sent to Ray. 40 | */ 41 | 'send_queries_to_ray' => env('SEND_QUERIES_TO_RAY', false), 42 | 43 | /** 44 | * When enabled, all duplicate queries will automatically be sent to Ray. 45 | */ 46 | 'send_duplicate_queries_to_ray' => env('SEND_DUPLICATE_QUERIES_TO_RAY', false), 47 | 48 | /* 49 | * When enabled, slow queries will automatically be sent to Ray. 50 | */ 51 | 'send_slow_queries_to_ray' => env('SEND_SLOW_QUERIES_TO_RAY', false), 52 | 53 | /** 54 | * Queries that are longer than this number of milliseconds will be regarded as slow. 55 | */ 56 | 'slow_query_threshold_in_ms' => env('RAY_SLOW_QUERY_THRESHOLD_IN_MS', 500), 57 | 58 | /* 59 | * When enabled, all update queries will automatically be sent to Ray. 60 | */ 61 | 'send_update_queries_to_ray' => env('SEND_UPDATE_QUERIES_TO_RAY', false), 62 | 63 | /* 64 | * When enabled, all insert queries will automatically be sent to Ray. 65 | */ 66 | 'send_insert_queries_to_ray' => env('SEND_INSERT_QUERIES_TO_RAY', false), 67 | 68 | /* 69 | * When enabled, all delete queries will automatically be sent to Ray. 70 | */ 71 | 'send_delete_queries_to_ray' => env('SEND_DELETE_QUERIES_TO_RAY', false), 72 | 73 | /* 74 | * When enabled, all select queries will automatically be sent to Ray. 75 | */ 76 | 'send_select_queries_to_ray' => env('SEND_SELECT_QUERIES_TO_RAY', false), 77 | 78 | /* 79 | * When enabled, all requests made to this app will automatically be sent to Ray. 80 | */ 81 | 'send_requests_to_ray' => env('SEND_REQUESTS_TO_RAY', false), 82 | 83 | /** 84 | * When enabled, all Http Client requests made by this app will be automatically sent to Ray. 85 | */ 86 | 'send_http_client_requests_to_ray' => env('SEND_HTTP_CLIENT_REQUESTS_TO_RAY', false), 87 | 88 | /* 89 | * When enabled, all views that are rendered automatically be sent to Ray. 90 | */ 91 | 'send_views_to_ray' => env('SEND_VIEWS_TO_RAY', false), 92 | 93 | /* 94 | * When enabled, all exceptions will be automatically sent to Ray. 95 | */ 96 | 'send_exceptions_to_ray' => env('SEND_EXCEPTIONS_TO_RAY', true), 97 | 98 | /* 99 | * When enabled, all deprecation notices will be automatically sent to Ray. 100 | */ 101 | 'send_deprecated_notices_to_ray' => env('SEND_DEPRECATED_NOTICES_TO_RAY', false), 102 | 103 | /* 104 | * The host used to communicate with the Ray app. 105 | * When using Docker on Mac or Windows, you can replace localhost with 'host.docker.internal' 106 | * When using Docker on Linux, you can replace localhost with '172.17.0.1' 107 | * When using Homestead with the VirtualBox provider, you can replace localhost with '10.0.2.2' 108 | * When using Homestead with the Parallels provider, you can replace localhost with '10.211.55.2' 109 | */ 110 | 'host' => env('RAY_HOST', 'localhost'), 111 | 112 | /* 113 | * The port number used to communicate with the Ray app. 114 | */ 115 | 'port' => env('RAY_PORT', 23517), 116 | 117 | /* 118 | * Absolute base path for your sites or projects in Homestead, 119 | * Vagrant, Docker, or another remote development server. 120 | */ 121 | 'remote_path' => env('RAY_REMOTE_PATH', null), 122 | 123 | /* 124 | * Absolute base path for your sites or projects on your local 125 | * computer where your IDE or code editor is running on. 126 | */ 127 | 'local_path' => env('RAY_LOCAL_PATH', null), 128 | 129 | /* 130 | * When this setting is enabled, the package will not try to format values sent to Ray. 131 | */ 132 | 'always_send_raw_values' => false, 133 | ]; 134 | -------------------------------------------------------------------------------- /src/Watchers/RequestWatcher.php: -------------------------------------------------------------------------------- 1 | enabled = $settings->send_requests_to_ray; 25 | 26 | Event::listen(RequestHandled::class, function (RequestHandled $event) { 27 | if (! $this->enabled()) { 28 | return; 29 | } 30 | 31 | $ray = $this->handleRequest($event->request, $event->response); 32 | 33 | optional($this->rayProxy)->applyCalledMethods($ray); 34 | }); 35 | } 36 | 37 | protected function handleRequest(Request $request, Response $response): Ray 38 | { 39 | $startTime = defined('LARAVEL_START') 40 | ? LARAVEL_START 41 | : $request->server('REQUEST_TIME_FLOAT'); 42 | 43 | $headers = collect($request->headers->all()) 44 | ->map(function (array $header) { 45 | return $header[0]; 46 | }) 47 | ->toArray(); 48 | 49 | $session = $request->hasSession() 50 | ? $request->session()->all() 51 | : []; 52 | 53 | $payload = new TablePayload([ 54 | 'IP Address' => $request->ip(), 55 | 'URI' => str_replace($request->root(), '', $request->fullUrl()) ?: '/', 56 | 'Method' => $request->method(), 57 | 'Controller action' => optional($request->route())->getActionName(), 58 | 'Middleware' => array_values(optional($request->route())->gatherMiddleware() ?? []), 59 | 'Headers' => $headers, 60 | 'Payload' => $this->payload($request), 61 | 'Session' => $session, 62 | 'Response code' => $response->getStatusCode(), 63 | 'Response' => $this->response($response), 64 | 'Duration' => $startTime ? floor((microtime(true) - $startTime) * 1000) : null, 65 | 'Memory' => round(memory_get_peak_usage(true) / 1024 / 1024, 1), 66 | ], 'Request'); 67 | 68 | return app(Ray::class)->sendRequest($payload); 69 | } 70 | 71 | private function payload(Request $request) 72 | { 73 | $files = $request->files->all(); 74 | 75 | array_walk_recursive($files, function (&$file) { 76 | $file = [ 77 | 'name' => $file->getClientOriginalName(), 78 | 'size' => $file->isFile() ? ($file->getSize() / 1000).'KB' : '0', 79 | ]; 80 | }); 81 | 82 | return array_replace_recursive($request->input(), $files); 83 | } 84 | 85 | protected function response(Response $response) 86 | { 87 | $content = $response->getContent(); 88 | 89 | if (is_string($content)) { 90 | if (is_array(json_decode($content, true)) && 91 | json_last_error() === JSON_ERROR_NONE) { 92 | return json_decode($content, true); 93 | } 94 | 95 | if (Str::startsWith(strtolower($response->headers->get('Content-Type')), 'text/plain')) { 96 | return $content; 97 | } 98 | } 99 | 100 | if ($response instanceof RedirectResponse) { 101 | return 'Redirected to '.$response->getTargetUrl(); 102 | } 103 | 104 | if ($response instanceof IlluminateResponse && $response->getOriginalContent() instanceof View) { 105 | return [ 106 | 'view' => $response->getOriginalContent()->getPath(), 107 | 'data' => $this->extractDataFromView($response->getOriginalContent()), 108 | ]; 109 | } 110 | 111 | return 'HTML Response'; 112 | } 113 | 114 | protected function extractDataFromView($view) 115 | { 116 | return collect($view->getData()) 117 | ->map(function ($value) { 118 | if ($value instanceof Model) { 119 | return $value->toArray(); 120 | } 121 | 122 | if (is_object($value)) { 123 | return [ 124 | 'class' => get_class($value), 125 | 'properties' => json_decode(json_encode($value), true), 126 | ]; 127 | } 128 | 129 | return json_decode(json_encode($value), true); 130 | }) 131 | ->toArray(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/OriginFactory.php: -------------------------------------------------------------------------------- 1 | getFrame(); 30 | 31 | return new Origin( 32 | optional($frame)->file, 33 | optional($frame)->lineNumber, 34 | ); 35 | } 36 | 37 | protected function getFrame(): ?Frame 38 | { 39 | $frames = collect(Backtrace::create()->frames())->reverse(); 40 | 41 | $indexOfRay = $frames 42 | ->search(function (Frame $frame) { 43 | if ($frame->class === Ray::class) { 44 | return true; 45 | } 46 | 47 | if (Str::startsWith($frame->file, __DIR__)) { 48 | return true; 49 | } 50 | 51 | return false; 52 | }); 53 | 54 | /** @var Frame|null $rayFrame */ 55 | $rayFrame = $frames[$indexOfRay] ?? null; 56 | 57 | $rayFunctionFrame = $frames[$indexOfRay + 2] ?? null; 58 | 59 | /** @var Frame|null $originFrame */ 60 | $originFrame = $frames[$indexOfRay + 1] ?? null; 61 | 62 | if ($originFrame && Str::endsWith($originFrame->file, Ray::makePathOsSafe('ray/src/helpers.php'))) { 63 | $framesAbove = 2; 64 | 65 | if ($rayFunctionFrame && $rayFunctionFrame->method === 'rd') { 66 | $framesAbove = 3; 67 | } 68 | 69 | $originFrame = $frames[$indexOfRay + $framesAbove] ?? null; 70 | } 71 | 72 | if (! $rayFrame) { 73 | return null; 74 | } 75 | 76 | if ($rayFrame->class === Stringable::class) { 77 | return $this->findFrameForStringableMacro($frames, $indexOfRay); 78 | } 79 | 80 | if ($rayFrame->class === Collection::class && Str::startsWith($rayFrame->method, 'Spatie\LaravelRay')) { 81 | return $this->findFrameForCollectionMacro($frames, $indexOfRay); 82 | } 83 | 84 | if ($rayFrame->class === QueryWatcher::class) { 85 | return $this->findFrameForQuery($frames); 86 | } 87 | 88 | if ($rayFrame->class === ViewWatcher::class) { 89 | return $this->findFrameForView($frames, $indexOfRay); 90 | } 91 | 92 | if ($rayFrame->class === DumpRecorder::class) { 93 | return $this->findFrameForDump($frames); 94 | } 95 | 96 | if ($rayFrame->class === CacheWatcher::class) { 97 | return $this->findFrameForCache($frames); 98 | } 99 | 100 | if ($originFrame->class === Dispatcher::class) { 101 | return $this->findFrameForEvent($frames); 102 | } 103 | 104 | if ($originFrame->class === Builder::class) { 105 | return $this->findFrameForQueryBuilder($frames); 106 | } 107 | 108 | if (Str::endsWith($originFrame->file, Ray::makePathOsSafe('/vendor/psy/psysh/src/ExecutionLoopClosure.php'))) { 109 | $this->returnTinkerFrame(); 110 | } 111 | 112 | try { 113 | if (Str::startsWith($originFrame->file, config('view.compiled'))) { 114 | return $this->replaceCompiledViewPathWithOriginalViewPath($originFrame); 115 | } 116 | } catch (BindingResolutionException $exception) { 117 | // ignore errors caused by using `storage_path` 118 | } 119 | 120 | if ($originFrame->class === Invador::class) { 121 | return $frames[$indexOfRay + 2]; 122 | } 123 | 124 | return $originFrame; 125 | } 126 | 127 | protected function findFrameForStringableMacro(Collection $frames, int $indexOfFoundFrame): ?Frame 128 | { 129 | return $frames[$indexOfFoundFrame + 2]; 130 | } 131 | 132 | protected function findFrameForCollectionMacro(Collection $frames, int $indexOfFoundFrame): ?Frame 133 | { 134 | return $frames[$indexOfFoundFrame + 2]; 135 | } 136 | 137 | protected function findFrameForQuery(Collection $frames): ?Frame 138 | { 139 | $indexOfLastDatabaseCall = $frames 140 | ->filter(function (Frame $frame) { 141 | return ! is_null($frame->class); 142 | }) 143 | ->search(function (Frame $frame) { 144 | return Str::startsWith($frame->class, 'Illuminate\Database'); 145 | }); 146 | 147 | return $frames[$indexOfLastDatabaseCall + 1] ?? null; 148 | } 149 | 150 | protected function findFrameForQueryBuilder(Collection $frames): ?Frame 151 | { 152 | $indexOfLastDatabaseCall = $frames 153 | ->filter(function (Frame $frame) { 154 | return ! is_null($frame->class); 155 | }) 156 | ->search(function (Frame $frame) { 157 | return Str::startsWith($frame->class, 'Illuminate\Database'); 158 | }); 159 | 160 | return $frames[$indexOfLastDatabaseCall + 1] ?? null; 161 | } 162 | 163 | protected function findFrameForView(Collection $frames, int $indexOfRayFrame): ?Frame 164 | { 165 | return $frames[$indexOfRayFrame + 6] ?? null; 166 | } 167 | 168 | protected function findFrameForDump(Collection $frames): ?Frame 169 | { 170 | $indexOfDumpCall = $frames 171 | ->search(function (Frame $frame) { 172 | if (! is_null($frame->class)) { 173 | return false; 174 | } 175 | 176 | return in_array($frame->method, ['dump', 'dd']); 177 | }); 178 | 179 | return $frames[$indexOfDumpCall + 1] ?? null; 180 | } 181 | 182 | protected function findFrameForEvent(Collection $frames): ?Frame 183 | { 184 | $indexOfLoggerCall = $frames 185 | ->search(function (Frame $frame) { 186 | return $frame->class === Logger::class; 187 | }); 188 | 189 | if ($indexOfLoggerCall) { 190 | return $this->findFrameForLog($frames, $indexOfLoggerCall); 191 | } 192 | 193 | $indexOfEventDispatcherCall = $frames 194 | ->search(function (Frame $frame) { 195 | return ($frame->class === Dispatcher::class) && $frame->method === 'dispatch'; 196 | }); 197 | 198 | /** @var Frame $foundFrame */ 199 | if ($foundFrame = $frames[$indexOfEventDispatcherCall + 2]) { 200 | if (Str::endsWith($foundFrame->file, Ray::makePathOsSafe('/Illuminate/Foundation/Events/Dispatchable.php'))) { 201 | $foundFrame = $frames[$indexOfEventDispatcherCall + 3]; 202 | } 203 | } 204 | 205 | return $foundFrame ?? null; 206 | } 207 | 208 | protected function findFrameForLog(Collection $frames, int $indexOfLoggerCall): ?Frame 209 | { 210 | /** @var Frame $foundFrame */ 211 | if ($foundFrame = $frames[$indexOfLoggerCall + 1]) { 212 | if ($foundFrame->class === LogManager::class) { 213 | $foundFrame = $frames[$indexOfLoggerCall + 2]; 214 | 215 | if ($foundFrame->class = Facade::class) { 216 | $foundFrame = $frames[$indexOfLoggerCall + 3]; 217 | } 218 | 219 | if (Str::endsWith($foundFrame->file, Ray::makePathOsSafe('/Illuminate/Foundation/helpers.php'))) { 220 | $foundFrame = $frames[$indexOfLoggerCall + 3]; 221 | } 222 | } 223 | } 224 | 225 | return $foundFrame ?? null; 226 | } 227 | 228 | public function findFrameForCache(Collection $frames): ?Frame 229 | { 230 | $index = $frames->search(function (Frame $frame) { 231 | return $frame->class === CacheManager::class; 232 | }); 233 | 234 | while (Str::startsWith($frames[$index]->class, 'Illuminate')) { 235 | $index++; 236 | } 237 | 238 | return $frames[$index] ?? null; 239 | } 240 | 241 | protected function replaceCompiledViewPathWithOriginalViewPath(Frame $frame): Frame 242 | { 243 | if (! file_exists($frame->file)) { 244 | return $frame; 245 | } 246 | 247 | $fileContents = file_get_contents($frame->file); 248 | 249 | $originalViewPath = trim(Str::between($fileContents, '/**PATH', 'ENDPATH**/')); 250 | 251 | if (! file_exists($originalViewPath)) { 252 | return $frame; 253 | } 254 | 255 | $frame->file = $originalViewPath; 256 | $frame->lineNumber = 1; 257 | 258 | return $frame; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/RayServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerCommands() 49 | ->registerSettings() 50 | ->setProjectName() 51 | ->registerBindings() 52 | ->registerWatchers() 53 | ->registerMacros() 54 | ->registerBindings() 55 | ->registerBladeDirectives() 56 | ->registerPayloadFinder(); 57 | } 58 | 59 | public function boot() 60 | { 61 | $this->bootWatchers(); 62 | } 63 | 64 | protected function registerCommands(): self 65 | { 66 | $this->commands(PublishConfigCommand::class); 67 | $this->commands(CleanRayCommand::class); 68 | 69 | return $this; 70 | } 71 | 72 | protected function registerSettings(): self 73 | { 74 | $this->app->singleton(Settings::class, function ($app) { 75 | $settings = SettingsFactory::createFromConfigFile($app->configPath()); 76 | 77 | return $settings->setDefaultSettings([ 78 | 'enable' => env('RAY_ENABLED', ! app()->environment('production')), 79 | 'send_cache_to_ray' => env('SEND_CACHE_TO_RAY', false), 80 | 'send_dumps_to_ray' => env('SEND_DUMPS_TO_RAY', true), 81 | 'send_jobs_to_ray' => env('SEND_JOBS_TO_RAY', false), 82 | 'send_mails_to_ray' => env('SEND_MAILS_TO_RAY', true), 83 | 'send_log_calls_to_ray' => env('SEND_LOG_CALLS_TO_RAY', true), 84 | 'send_queries_to_ray' => env('SEND_QUERIES_TO_RAY', false), 85 | 'send_duplicate_queries_to_ray' => env('SEND_DUPLICATE_QUERIES_TO_RAY', false), 86 | 'send_slow_queries_to_ray' => env('SEND_SLOW_QUERIES_TO_RAY', false), 87 | 'send_update_queries_to_ray' => env('SEND_UPDATE_QUERIES_TO_RAY', false), 88 | 'send_insert_queries_to_ray' => env('SEND_INSERT_QUERIES_TO_RAY', false), 89 | 'send_delete_queries_to_ray' => env('SEND_DELETE_QUERIES_TO_RAY', false), 90 | 'send_select_queries_to_ray' => env('SEND_SELECT_QUERIES_TO_RAY', false), 91 | 'send_requests_to_ray' => env('SEND_REQUESTS_TO_RAY', false), 92 | 'send_http_client_requests_to_ray' => env('SEND_HTTP_CLIENT_REQUESTS_TO_RAY', false), 93 | 'send_views_to_ray' => env('SEND_VIEWS_TO_RAY', false), 94 | 'send_exceptions_to_ray' => env('SEND_EXCEPTIONS_TO_RAY', true), 95 | 'send_deprecated_notices_to_ray' => env('SEND_DEPRECATED_NOTICES_TO_RAY', false), 96 | ]); 97 | }); 98 | 99 | return $this; 100 | } 101 | 102 | public function setProjectName(): self 103 | { 104 | if (Ray::$projectName === '') { 105 | $projectName = config('app.name'); 106 | 107 | if ($projectName !== 'Laravel') { 108 | ray()->project($projectName); 109 | } 110 | } 111 | 112 | return $this; 113 | } 114 | 115 | protected function registerBindings(): self 116 | { 117 | $settings = app(Settings::class); 118 | 119 | $this->app->bind(Client::class, function () use ($settings) { 120 | return new Client($settings->port, $settings->host); 121 | }); 122 | 123 | $this->app->bind(Ray::class, function () { 124 | $client = app(Client::class); 125 | 126 | $settings = app(Settings::class); 127 | 128 | $ray = new Ray($settings, $client); 129 | 130 | if (! $settings->enable) { 131 | $ray->disable(); 132 | } 133 | 134 | return $ray; 135 | }); 136 | 137 | Payload::$originFactoryClass = OriginFactory::class; 138 | 139 | return $this; 140 | } 141 | 142 | protected function registerWatchers(): self 143 | { 144 | $watchers = [ 145 | ExceptionWatcher::class, 146 | MailWatcher::class, 147 | ApplicationLogWatcher::class, 148 | JobWatcher::class, 149 | EventWatcher::class, 150 | DumpWatcher::class, 151 | QueryWatcher::class, 152 | DuplicateQueryWatcher::class, 153 | SlowQueryWatcher::class, 154 | InsertQueryWatcher::class, 155 | SelectQueryWatcher::class, 156 | UpdateQueryWatcher::class, 157 | DeleteQueryWatcher::class, 158 | ViewWatcher::class, 159 | CacheWatcher::class, 160 | RequestWatcher::class, 161 | HttpClientWatcher::class, 162 | DeprecatedNoticeWatcher::class, 163 | ]; 164 | 165 | collect($watchers) 166 | ->each(function (string $watcherClass) { 167 | $this->app->singleton($watcherClass); 168 | }); 169 | 170 | return $this; 171 | } 172 | 173 | protected function bootWatchers(): self 174 | { 175 | $watchers = [ 176 | ExceptionWatcher::class, 177 | ApplicationLogWatcher::class, 178 | JobWatcher::class, 179 | EventWatcher::class, 180 | DumpWatcher::class, 181 | QueryWatcher::class, 182 | DuplicateQueryWatcher::class, 183 | SlowQueryWatcher::class, 184 | InsertQueryWatcher::class, 185 | SelectQueryWatcher::class, 186 | UpdateQueryWatcher::class, 187 | DeleteQueryWatcher::class, 188 | ViewWatcher::class, 189 | CacheWatcher::class, 190 | RequestWatcher::class, 191 | HttpClientWatcher::class, 192 | DeprecatedNoticeWatcher::class, 193 | MailWatcher::class, 194 | ]; 195 | 196 | collect($watchers) 197 | ->each(function (string $watcherClass) { 198 | /** @var \Spatie\LaravelRay\Watchers\Watcher $watcher */ 199 | $watcher = app($watcherClass); 200 | 201 | $watcher->register(); 202 | }); 203 | 204 | return $this; 205 | } 206 | 207 | protected function registerMacros(): self 208 | { 209 | Collection::macro('ray', function (string $description = '') { 210 | $description === '' 211 | ? ray($this->items) 212 | : ray($description, $this->items); 213 | 214 | return $this; 215 | }); 216 | 217 | TestResponse::macro('ray', function () { 218 | ray()->testResponse($this); 219 | 220 | return $this; 221 | }); 222 | 223 | Stringable::macro('ray', function (string $description = '') { 224 | $description === '' 225 | ? ray($this->value) 226 | : ray($description, $this->value); 227 | 228 | return $this; 229 | }); 230 | 231 | Builder::macro('ray', function () { 232 | $payload = new QueryPayload($this); 233 | 234 | ray()->sendRequest($payload); 235 | 236 | return $this; 237 | }); 238 | 239 | return $this; 240 | } 241 | 242 | protected function registerBladeDirectives(): self 243 | { 244 | if (! $this->app->has('blade.compiler')) { 245 | return $this; 246 | } 247 | 248 | $this->callAfterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) { 249 | Blade::directive('ray', function ($expression) { 250 | return ""; 251 | }); 252 | Blade::directive('measure', function () { 253 | return 'measure() ?>'; 254 | }); 255 | Blade::directive('xray', function () { 256 | return ''; 257 | }); 258 | }); 259 | 260 | return $this; 261 | } 262 | 263 | protected function registerPayloadFinder(): self 264 | { 265 | PayloadFactory::registerPayloadFinder(function ($argument) { 266 | if ($argument instanceof Model) { 267 | return new ModelPayload($argument); 268 | } 269 | 270 | if ($argument instanceof Mailable) { 271 | return MailablePayload::forMailable($argument); 272 | } 273 | 274 | return null; 275 | }); 276 | 277 | return $this; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/Ray.php: -------------------------------------------------------------------------------- 1 | sendRequest($payload); 68 | 69 | return $this; 70 | } 71 | 72 | public function mailable(Mailable ...$mailables): self 73 | { 74 | $shouldRestoreFake = false; 75 | 76 | if (get_class(app(MailManager::class)) === MailFake::class) { 77 | $shouldRestoreFake = true; 78 | 79 | Mail::swap(new MailManager(app())); 80 | } 81 | 82 | if ($shouldRestoreFake) { 83 | Mail::fake(); 84 | } 85 | 86 | $payloads = array_map(function (Mailable $mailable) { 87 | return MailablePayload::forMailable($mailable); 88 | }, $mailables); 89 | 90 | $this->sendRequest($payloads); 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * @param null $callable 97 | * @return \Spatie\LaravelRay\Ray 98 | */ 99 | public function showMails($callable = null) 100 | { 101 | $watcher = app(MailWatcher::class); 102 | 103 | $watcher->enable(); 104 | 105 | return $this->handleWatcherCallable($watcher, $callable); 106 | } 107 | 108 | public function stopShowingMails(): self 109 | { 110 | app(MailWatcher::class)->disable(); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * @param array|string ...$keys 117 | * @return $this 118 | */ 119 | public function context(...$keys): self 120 | { 121 | if (! class_exists(Context::class)) { 122 | return $this; 123 | } 124 | 125 | if (isset($keys[0]) && is_array($keys[0])) { 126 | $keys = $keys[0]; 127 | } 128 | 129 | $context = count($keys) 130 | ? Context::only($keys) 131 | : Context::all(); 132 | 133 | $this 134 | ->send($context) 135 | ->label('Context'); 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * @param array|string ...$keys 142 | * @return $this 143 | */ 144 | public function hiddenContext(...$keys): self 145 | { 146 | if (! class_exists(Context::class)) { 147 | return $this; 148 | } 149 | 150 | if (isset($keys[0]) && is_array($keys[0])) { 151 | $keys = $keys[0]; 152 | } 153 | 154 | $hiddenContext = count($keys) 155 | ? Context::onlyHidden($keys) 156 | : Context::allHidden(); 157 | 158 | $this 159 | ->send($hiddenContext) 160 | ->label('Hidden Context'); 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * @param Model|iterable ...$model 167 | */ 168 | public function model(...$model): self 169 | { 170 | $models = []; 171 | foreach ($model as $passedModel) { 172 | if (is_null($passedModel)) { 173 | $models[] = null; 174 | 175 | continue; 176 | } 177 | if ($passedModel instanceof Model) { 178 | $models[] = $passedModel; 179 | 180 | continue; 181 | } 182 | 183 | if (is_iterable($model)) { 184 | foreach ($passedModel as $item) { 185 | $models[] = $item; 186 | 187 | continue; 188 | } 189 | } 190 | } 191 | 192 | $payloads = array_map(function (?Model $model) { 193 | return new ModelPayload($model); 194 | }, $models); 195 | 196 | foreach ($payloads as $payload) { 197 | ray()->sendRequest($payload); 198 | } 199 | 200 | return $this; 201 | } 202 | 203 | /** 204 | * @param Model|iterable $models 205 | */ 206 | public function models($models): self 207 | { 208 | return $this->model($models); 209 | } 210 | 211 | public function markdown(string $markdown): self 212 | { 213 | $payload = new MarkdownPayload($markdown); 214 | 215 | $this->sendRequest($payload); 216 | 217 | return $this; 218 | } 219 | 220 | /** 221 | * @param string[]|array|null $onlyShowNames 222 | */ 223 | public function env(?array $onlyShowNames = null, ?string $filename = null): self 224 | { 225 | $filename ??= app()->environmentFilePath(); 226 | 227 | $payload = new EnvironmentPayload($onlyShowNames, $filename); 228 | 229 | $this->sendRequest($payload); 230 | 231 | return $this; 232 | } 233 | 234 | /** 235 | * @param null $callable 236 | * @return \Spatie\LaravelRay\Ray 237 | */ 238 | public function showEvents($callable = null) 239 | { 240 | $watcher = app(EventWatcher::class); 241 | 242 | return $this->handleWatcherCallable($watcher, $callable); 243 | } 244 | 245 | public function events($callable = null) 246 | { 247 | return $this->showEvents($callable); 248 | } 249 | 250 | public function stopShowingEvents(): self 251 | { 252 | /** @var \Spatie\LaravelRay\Watchers\EventWatcher $eventWatcher */ 253 | $eventWatcher = app(EventWatcher::class); 254 | 255 | $eventWatcher->disable(); 256 | 257 | return $this; 258 | } 259 | 260 | public function showExceptions(): self 261 | { 262 | /** @var \Spatie\LaravelRay\Watchers\ExceptionWatcher $exceptionWatcher */ 263 | $exceptionWatcher = app(ExceptionWatcher::class); 264 | 265 | $exceptionWatcher->enable(); 266 | 267 | return $this; 268 | } 269 | 270 | public function stopShowingExceptions(): self 271 | { 272 | /** @var \Spatie\LaravelRay\Watchers\ExceptionWatcher $exceptionWatcher */ 273 | $exceptionWatcher = app(ExceptionWatcher::class); 274 | 275 | $exceptionWatcher->disable(); 276 | 277 | return $this; 278 | } 279 | 280 | /** 281 | * @param null $callable 282 | * @return \Spatie\LaravelRay\Ray 283 | */ 284 | public function showJobs($callable = null) 285 | { 286 | $watcher = app(JobWatcher::class); 287 | 288 | return $this->handleWatcherCallable($watcher, $callable); 289 | } 290 | 291 | /** 292 | * @param null $callable 293 | * @return \Spatie\LaravelRay\Ray 294 | */ 295 | public function showCache($callable = null) 296 | { 297 | $watcher = app(CacheWatcher::class); 298 | 299 | return $this->handleWatcherCallable($watcher, $callable); 300 | } 301 | 302 | public function stopShowingCache(): self 303 | { 304 | app(CacheWatcher::class)->disable(); 305 | 306 | return $this; 307 | } 308 | 309 | public function jobs($callable = null) 310 | { 311 | return $this->showJobs($callable); 312 | } 313 | 314 | public function stopShowingJobs(): self 315 | { 316 | app(JobWatcher::class)->disable(); 317 | 318 | return $this; 319 | } 320 | 321 | public function view(View $view): self 322 | { 323 | $payload = new ViewPayload($view); 324 | 325 | return $this->sendRequest($payload); 326 | } 327 | 328 | /** 329 | * @param null $callable 330 | * @return \Spatie\LaravelRay\Ray 331 | */ 332 | public function showViews($callable = null) 333 | { 334 | $watcher = app(ViewWatcher::class); 335 | 336 | return $this->handleWatcherCallable($watcher, $callable); 337 | } 338 | 339 | public function views($callable = null) 340 | { 341 | return $this->showViews($callable); 342 | } 343 | 344 | public function stopShowingViews(): self 345 | { 346 | app(ViewWatcher::class)->disable(); 347 | 348 | return $this; 349 | } 350 | 351 | /** 352 | * @param null $callable 353 | * @return \Spatie\LaravelRay\Ray 354 | */ 355 | public function showQueries($callable = null) 356 | { 357 | $watcher = app(QueryWatcher::class); 358 | 359 | return $this->handleWatcherCallable($watcher, $callable); 360 | } 361 | 362 | public function countQueries(callable $callable) 363 | { 364 | /** @var QueryWatcher $watcher */ 365 | $watcher = app(QueryWatcher::class); 366 | 367 | $watcher->keepExecutedQueries(); 368 | 369 | if (! $watcher->enabled()) { 370 | $watcher->doNotSendIndividualQueries(); 371 | } 372 | 373 | $output = $this->handleWatcherCallable($watcher, $callable); 374 | 375 | $executedQueryStatistics = collect($watcher->getExecutedQueries()) 376 | 377 | ->pipe(function (Collection $queries) { 378 | return [ 379 | 'Count' => $queries->count(), 380 | 'Total time' => $queries->sum(function (QueryExecuted $query) { 381 | return $query->time; 382 | }), 383 | ]; 384 | }); 385 | 386 | $executedQueryStatistics['Total time'] .= ' ms'; 387 | 388 | $watcher 389 | ->stopKeepingAndClearExecutedQueries() 390 | ->sendIndividualQueries(); 391 | 392 | $this->table($executedQueryStatistics, 'Queries'); 393 | 394 | return $output; 395 | } 396 | 397 | public function queries($callable = null) 398 | { 399 | return $this->showQueries($callable); 400 | } 401 | 402 | public function stopShowingQueries(): self 403 | { 404 | app(QueryWatcher::class)->disable(); 405 | 406 | return $this; 407 | } 408 | 409 | public function slowQueries($milliseconds = 500, $callable = null) 410 | { 411 | return $this->showSlowQueries($milliseconds, $callable); 412 | } 413 | 414 | public function showSlowQueries($milliseconds = 500, $callable = null) 415 | { 416 | $watcher = app(SlowQueryWatcher::class) 417 | ->setMinimumTimeInMilliseconds($milliseconds); 418 | 419 | return $this->handleWatcherCallable($watcher, $callable); 420 | } 421 | 422 | public function stopShowingSlowQueries(): self 423 | { 424 | app(SlowQueryWatcher::class)->disable(); 425 | 426 | return $this; 427 | } 428 | 429 | /** 430 | * @param null $callable 431 | * @return \Spatie\LaravelRay\Ray 432 | */ 433 | public function showDuplicateQueries($callable = null) 434 | { 435 | $watcher = app(DuplicateQueryWatcher::class); 436 | 437 | return $this->handleWatcherCallable($watcher, $callable); 438 | } 439 | 440 | public function stopShowingDuplicateQueries(): self 441 | { 442 | app(DuplicateQueryWatcher::class)->disable(); 443 | 444 | return $this; 445 | } 446 | 447 | public function showConditionalQueries(Closure $condition, $callable = null, $name = 'default') 448 | { 449 | $watcher = ConditionalQueryWatcher::buildWatcherForName($condition, $name); 450 | 451 | return $this->handleWatcherCallable($watcher, $callable); 452 | } 453 | 454 | public function stopShowingConditionalQueries($name = 'default'): self 455 | { 456 | app(ConditionalQueryWatcher::abstractName($name))->disable(); 457 | 458 | return $this; 459 | } 460 | 461 | public function showUpdateQueries($callable = null) 462 | { 463 | $watcher = app(UpdateQueryWatcher::class); 464 | 465 | return $this->handleWatcherCallable($watcher, $callable); 466 | } 467 | 468 | public function stopShowingUpdateQueries(): self 469 | { 470 | app(UpdateQueryWatcher::class)->disable(); 471 | 472 | return $this; 473 | } 474 | 475 | public function showDeleteQueries($callable = null) 476 | { 477 | $watcher = app(DeleteQueryWatcher::class); 478 | 479 | return $this->handleWatcherCallable($watcher, $callable); 480 | } 481 | 482 | public function stopShowingDeleteQueries(): self 483 | { 484 | app(DeleteQueryWatcher::class)->disable(); 485 | 486 | return $this; 487 | } 488 | 489 | public function showInsertQueries($callable = null) 490 | { 491 | $watcher = app(InsertQueryWatcher::class); 492 | 493 | return $this->handleWatcherCallable($watcher, $callable); 494 | } 495 | 496 | public function stopShowingInsertQueries(): self 497 | { 498 | app(InsertQueryWatcher::class)->disable(); 499 | 500 | return $this; 501 | } 502 | 503 | public function showSelectQueries($callable = null) 504 | { 505 | $watcher = app(SelectQueryWatcher::class); 506 | 507 | return $this->handleWatcherCallable($watcher, $callable); 508 | } 509 | 510 | public function stopShowingSelectQueries(): self 511 | { 512 | app(SelectQueryWatcher::class)->disable(); 513 | 514 | return $this; 515 | } 516 | 517 | /** 518 | * @param null $callable 519 | * @return \Spatie\LaravelRay\Ray 520 | */ 521 | public function showRequests($callable = null) 522 | { 523 | $watcher = app(RequestWatcher::class); 524 | 525 | return $this->handleWatcherCallable($watcher, $callable); 526 | } 527 | 528 | public function requests($callable = null) 529 | { 530 | return $this->showRequests($callable); 531 | } 532 | 533 | public function stopShowingRequests(): self 534 | { 535 | $this->requestWatcher()->disable(); 536 | 537 | return $this; 538 | } 539 | 540 | /** 541 | * @param null $callable 542 | * @return \Spatie\LaravelRay\Ray 543 | */ 544 | public function showHttpClientRequests($callable = null) 545 | { 546 | if (! HttpClientWatcher::supportedByLaravelVersion()) { 547 | $this->send('Http logging is not available in your Laravel version')->red(); 548 | 549 | return $this; 550 | } 551 | 552 | $watcher = app(HttpClientWatcher::class); 553 | 554 | return $this->handleWatcherCallable($watcher, $callable); 555 | } 556 | 557 | public function httpClientRequests($callable = null) 558 | { 559 | return $this->showHttpClientRequests($callable); 560 | } 561 | 562 | public function stopShowingHttpClientRequests(): self 563 | { 564 | app(HttpClientWatcher::class)->disable(); 565 | 566 | return $this; 567 | } 568 | 569 | protected function handleWatcherCallable(Watcher $watcher, ?Closure $callable = null) 570 | { 571 | $rayProxy = new RayProxy; 572 | 573 | $wasEnabled = $watcher->enabled(); 574 | 575 | $watcher->enable(); 576 | 577 | if ($rayProxy) { 578 | $watcher->setRayProxy($rayProxy); 579 | } 580 | 581 | if ($callable) { 582 | $output = $callable(); 583 | 584 | if (! $wasEnabled) { 585 | $watcher->disable(); 586 | } 587 | 588 | if ((new ReflectionFunction($callable))->hasReturnType()) { 589 | return $output; 590 | } 591 | } 592 | 593 | return $rayProxy; 594 | } 595 | 596 | public function testResponse(TestResponse $testResponse) 597 | { 598 | $payload = ResponsePayload::fromTestResponse($testResponse); 599 | 600 | $this->sendRequest($payload); 601 | } 602 | 603 | protected function requestWatcher(): RequestWatcher 604 | { 605 | return app(RequestWatcher::class); 606 | } 607 | 608 | public function exception(Throwable $exception, array $meta = []) 609 | { 610 | $payloads[] = new ExceptionPayload($exception, $meta); 611 | 612 | if ($exception instanceof QueryException) { 613 | $executedQuery = new QueryExecuted($exception->getSql(), $exception->getBindings(), null, DB::connection(config('database.default'))); 614 | 615 | $payloads[] = new ExecutedQueryPayload($executedQuery); 616 | } 617 | 618 | $this->sendRequest($payloads)->red(); 619 | 620 | return $this; 621 | } 622 | 623 | /** 624 | * @param \Spatie\Ray\Payloads\Payload|\Spatie\Ray\Payloads\Payload[] $payloads 625 | * 626 | * @throws \Exception 627 | */ 628 | public function sendRequest($payloads, array $meta = []): BaseRay 629 | { 630 | if (! $this->enabled()) { 631 | return $this; 632 | } 633 | 634 | $meta['laravel_version'] = app()->version(); 635 | 636 | if (class_exists(InstalledVersions::class)) { 637 | try { 638 | $meta['laravel_ray_package_version'] = InstalledVersions::getVersion('spatie/laravel-ray'); 639 | } catch (\Exception $e) { 640 | $meta['laravel_ray_package_version'] = '0.0.0'; 641 | } 642 | } 643 | 644 | return BaseRay::sendRequest($payloads, $meta); 645 | } 646 | } 647 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-ray` will be documented in this file 4 | 5 | ## 1.43.2 - 2025-12-15 6 | 7 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.43.1...1.43.2 8 | 9 | ## 1.43.1 - 2025-11-24 10 | 11 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.43.0...1.43.1 12 | 13 | ## 1.43.0 - 2025-11-24 14 | 15 | ### What's Changed 16 | 17 | * Add more context to exceptions by @freekmurze in https://github.com/spatie/laravel-ray/pull/396 18 | 19 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.42.0...1.43.0 20 | 21 | ## 1.42.0 - 2025-11-20 22 | 23 | ### What's Changed 24 | 25 | * Bump stefanzweifel/git-auto-commit-action from 6 to 7 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/392 26 | 27 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.41.0...1.42.0 28 | 29 | ## 1.41.0 - 2025-10-16 30 | 31 | ### What's Changed 32 | 33 | * Fix PHP 8.5 deprecations by @IonBazan in https://github.com/spatie/laravel-ray/pull/393 34 | 35 | ### New Contributors 36 | 37 | * @IonBazan made their first contribution in https://github.com/spatie/laravel-ray/pull/393 38 | 39 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.40.3...1.41.0 40 | 41 | ## 1.40.3 - 2025-10-13 42 | 43 | ### What's Changed 44 | 45 | * Bump shivammathur/setup-php from 2.32.0 to 2.33.0 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/378 46 | * Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/379 47 | * Bump shivammathur/setup-php from 2.33.0 to 2.34.1 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/381 48 | * Bump shivammathur/setup-php from 2.34.1 to 2.35.1 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/383 49 | * Bump shivammathur/setup-php from 2.35.1 to 2.35.2 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/384 50 | * Bump shivammathur/setup-php from 2.35.2 to 2.35.3 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/386 51 | * Bump shivammathur/setup-php from 2.35.3 to 2.35.4 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/387 52 | * Fix unnecessary consecutive spaces in comment by @msng in https://github.com/spatie/laravel-ray/pull/388 53 | * Update issue template by @AlexVanderbist in https://github.com/spatie/laravel-ray/pull/389 54 | * Bump shivammathur/setup-php from 2.35.4 to 2.35.5 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/390 55 | * Fix no-op Invador frame check in OriginFactory::getFrame() by @webpresencekyle in https://github.com/spatie/laravel-ray/pull/391 56 | * Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/385 57 | * Bump stefanzweifel/git-auto-commit-action from 5 to 6 by @dependabot[bot] in https://github.com/spatie/laravel-ray/pull/382 58 | 59 | ### New Contributors 60 | 61 | * @msng made their first contribution in https://github.com/spatie/laravel-ray/pull/388 62 | * @webpresencekyle made their first contribution in https://github.com/spatie/laravel-ray/pull/391 63 | 64 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.40.2...1.40.3 65 | 66 | ## 1.40.2 - 2025-03-27 67 | 68 | ### What's Changed 69 | 70 | * Respect custom path for cached views by @alies-dev in https://github.com/spatie/laravel-ray/pull/376 71 | 72 | ### New Contributors 73 | 74 | * @alies-dev made their first contribution in https://github.com/spatie/laravel-ray/pull/376 75 | 76 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.40.1...1.40.2 77 | 78 | ## 1.40.1 - 2025-03-14 79 | 80 | ### What's Changed 81 | 82 | * Fix issue #374 recent changes to mailwatcher has broken mailraw by @timvandijck in https://github.com/spatie/laravel-ray/pull/375 83 | 84 | ### New Contributors 85 | 86 | * @timvandijck made their first contribution in https://github.com/spatie/laravel-ray/pull/375 87 | 88 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.40.0...1.40.1 89 | 90 | ## 1.40.0 - 2025-03-08 91 | 92 | ### What's Changed 93 | 94 | * Allow to show emails (not just when log driver) by @glorand in https://github.com/spatie/laravel-ray/pull/373 95 | 96 | ### New Contributors 97 | 98 | * @glorand made their first contribution in https://github.com/spatie/laravel-ray/pull/373 99 | 100 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.39.1...1.40.0 101 | 102 | ## 1.39.1 - 2025-02-05 103 | 104 | ### What's Changed 105 | 106 | * Update Dependencies by @sweptsquash in https://github.com/spatie/laravel-ray/pull/369 107 | * Bump shivammathur/setup-php from 2.31.1 to 2.32.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/370 108 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/371 109 | * Supports Laravel 12 by @crynobone in https://github.com/spatie/laravel-ray/pull/372 110 | 111 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.39.0...1.39.1 112 | 113 | ## 1.39.0 - 2024-12-11 114 | 115 | ### What's Changed 116 | 117 | * Update the Rector dependency to version ^2.0.0-rc2 by @martio in https://github.com/spatie/laravel-ray/pull/368 118 | * Install `rector/rector` during first run of `php artisan ray:clean` instead of requiring `rector/rector` by @crynobone in https://github.com/spatie/laravel-ray/pull/363 119 | * Add conditional query watcher and convenience methods for update, delete, insert and select queries by @patrickomeara in https://github.com/spatie/laravel-ray/pull/359 120 | 121 | ### New Contributors 122 | 123 | * @martio made their first contribution in https://github.com/spatie/laravel-ray/pull/368 124 | * @patrickomeara made their first contribution in https://github.com/spatie/laravel-ray/pull/359 125 | 126 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.38.0...1.39.0 127 | 128 | ## 1.37.1 - 2024-07-15 129 | 130 | ### What's Changed 131 | 132 | * Add the xray directive to show all available variables in a Blade view. 133 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/356 134 | 135 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.37.0...1.37.1 136 | 137 | ## 1.37.0 - 2024-07-03 138 | 139 | ### What's Changed 140 | 141 | * Add 'measure' directive to RayServiceProvider by @iurigustavo in https://github.com/spatie/laravel-ray/pull/355 142 | 143 | ### New Contributors 144 | 145 | * @iurigustavo made their first contribution in https://github.com/spatie/laravel-ray/pull/355 146 | 147 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.36.3...1.37.0 148 | 149 | ## 1.36.3 - 2024-07-02 150 | 151 | ### What's Changed 152 | 153 | * Bump shivammathur/setup-php from 2.30.4 to 2.30.5 by @dependabot in https://github.com/spatie/laravel-ray/pull/350 154 | * Bump shivammathur/setup-php from 2.30.5 to 2.31.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/353 155 | * Allow zbateson/mail-mime-parser ^3.0 by @JamesFreeman in https://github.com/spatie/laravel-ray/pull/354 156 | 157 | ### New Contributors 158 | 159 | * @JamesFreeman made their first contribution in https://github.com/spatie/laravel-ray/pull/354 160 | 161 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.36.2...1.36.3 162 | 163 | ## 1.36.2 - 2024-05-02 164 | 165 | ### What's Changed 166 | 167 | - fix signed URLs in logged mailables 168 | 169 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.36.1...1.36.2 170 | 171 | ## 1.36.1 - 2024-04-12 172 | 173 | ### What's Changed 174 | 175 | * Bump shivammathur/setup-php from 2.30.1 to 2.30.2 by @dependabot in https://github.com/spatie/laravel-ray/pull/338 176 | * Make implicit nullable param to explicit (PHP 8.4 compatibility) by @GromNaN in https://github.com/spatie/laravel-ray/pull/340 177 | * Update branch alias for version 1.x by @GromNaN in https://github.com/spatie/laravel-ray/pull/341 178 | 179 | ### New Contributors 180 | 181 | * @GromNaN made their first contribution in https://github.com/spatie/laravel-ray/pull/340 182 | 183 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.36.0...1.36.1 184 | 185 | ## 1.36.0 - 2024-03-29 186 | 187 | ### What's Changed 188 | 189 | * Bump shivammathur/setup-php from 2.29.0 to 2.30.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/334 190 | * Bump shivammathur/setup-php from 2.30.0 to 2.30.1 by @dependabot in https://github.com/spatie/laravel-ray/pull/335 191 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/336 192 | * Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/spatie/laravel-ray/pull/333 193 | * Support context by @freekmurze in https://github.com/spatie/laravel-ray/pull/337 194 | 195 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.35.1...1.36.0 196 | 197 | ## 1.35.1 - 2024-02-13 198 | 199 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.35.0...1.35.1 200 | 201 | ## 1.35.0 - 2024-02-03 202 | 203 | ### What's Changed 204 | 205 | * Supports Laravel 11 by @crynobone in https://github.com/spatie/laravel-ray/pull/329 206 | 207 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.34.0...1.35.0 208 | 209 | ## 1.34.0 - 2024-01-25 210 | 211 | ### What's Changed 212 | 213 | * Adds an artisan command to remove ray calls from your codebase. 214 | * Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/spatie/laravel-ray/pull/321 215 | * Bump shivammathur/setup-php from 2.28.0 to 2.29.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/330 216 | 217 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.33.1...1.34.0 218 | 219 | ## 1.33.1 - 2024-01-04 220 | 221 | - Allow symphony stopwatch 7 222 | 223 | ## 1.33.0 - 2023-09-04 224 | 225 | ### What's Changed 226 | 227 | - Bump shivammathur/setup-php from 2.25.4 to 2.25.5 by @dependabot in https://github.com/spatie/laravel-ray/pull/309 228 | - Add ability to return the results of callable by @grantholle in https://github.com/spatie/laravel-ray/pull/314 229 | 230 | ### New Contributors 231 | 232 | - @grantholle made their first contribution in https://github.com/spatie/laravel-ray/pull/314 233 | 234 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.6...1.33.0 235 | 236 | ## 1.32.6 - 2023-07-19 237 | 238 | ### What's Changed 239 | 240 | - Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/305 241 | - feat: support raw sql by @innocenzi in https://github.com/spatie/laravel-ray/pull/306 242 | 243 | ### New Contributors 244 | 245 | - @innocenzi made their first contribution in https://github.com/spatie/laravel-ray/pull/306 246 | 247 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.5...1.32.6 248 | 249 | ## 1.32.5 - 2023-06-23 250 | 251 | - fix displaying Mailables 252 | 253 | ## 1.32.4 - 2023-03-23 254 | 255 | ### What's Changed 256 | 257 | - Avoid making DB connection unless necessary by @crynobone in https://github.com/spatie/laravel-ray/pull/295 258 | 259 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.3...1.32.4 260 | 261 | ## 1.32.3 - 2023-03-03 262 | 263 | - display correct origin when using `invade` 264 | 265 | ## 1.32.2 - 2023-02-06 266 | 267 | ### What's Changed 268 | 269 | - Bump shivammathur/setup-php from 2.23.0 to 2.24.0 by @dependabot in https://github.com/spatie/laravel-ray/pull/291 270 | - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/laravel-ray/pull/292 271 | - Add context to ApplicationLogPayload by @bilfeldt in https://github.com/spatie/laravel-ray/pull/293 272 | 273 | ### New Contributors 274 | 275 | - @bilfeldt made their first contribution in https://github.com/spatie/laravel-ray/pull/293 276 | 277 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.1...1.32.2 278 | 279 | ## 1.32.1 - 2023-01-26 280 | 281 | ### What's Changed 282 | 283 | - Make DB connection optional by @lentex in https://github.com/spatie/laravel-ray/pull/290 284 | 285 | ### New Contributors 286 | 287 | - @lentex made their first contribution in https://github.com/spatie/laravel-ray/pull/290 288 | 289 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.32.0...1.32.1 290 | 291 | ## 1.32.0 - 2023-01-11 292 | 293 | - add support for Laravel 10 294 | 295 | ## 1.31.0 - 2022-09-20 296 | 297 | ### What's Changed 298 | 299 | - Added in comment to docblock for linux docker users by @jaetoole in https://github.com/spatie/laravel-ray/pull/271 300 | - @ray blade directive completion for Laravel Idea(PhpStorm) by @adelf in https://github.com/spatie/laravel-ray/pull/273 301 | 302 | ### New Contributors 303 | 304 | - @jaetoole made their first contribution in https://github.com/spatie/laravel-ray/pull/271 305 | - @adelf made their first contribution in https://github.com/spatie/laravel-ray/pull/273 306 | 307 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.30.0...1.31.0 308 | 309 | ## 1.30.0 - 2022-07-29 310 | 311 | ### What's Changed 312 | 313 | - Add `send_deprecated_notices_to_ray` to config stub by @squatto in https://github.com/spatie/laravel-ray/pull/267 314 | - Feat: Slow query configuration by @fullstackfool in https://github.com/spatie/laravel-ray/pull/269 315 | 316 | ### New Contributors 317 | 318 | - @fullstackfool made their first contribution in https://github.com/spatie/laravel-ray/pull/269 319 | 320 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.7...1.30.0 321 | 322 | ## 1.29.7 - 2022-05-27 323 | 324 | ## What's Changed 325 | 326 | - Fixes https://github.com/spatie/laravel-ray/issues/250 by @dfox288 in https://github.com/spatie/laravel-ray/pull/251 327 | 328 | ## New Contributors 329 | 330 | - @dfox288 made their first contribution in https://github.com/spatie/laravel-ray/pull/251 331 | 332 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.6...1.29.7 333 | 334 | ## 1.29.6 - 2022-04-15 335 | 336 | ## What's Changed 337 | 338 | - ignore php 8.1 deprecation notices by @Nielsvanpach in https://github.com/spatie/laravel-ray/pull/247 339 | 340 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.5...1.29.6 341 | 342 | ## 1.29.5 - 2022-04-05 343 | 344 | ## What's Changed 345 | 346 | - Fix undefined payload when using queue driver other than sync by @stein-j in https://github.com/spatie/laravel-ray/pull/245 347 | 348 | ## New Contributors 349 | 350 | - @stein-j made their first contribution in https://github.com/spatie/laravel-ray/pull/245 351 | 352 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.4...1.29.5 353 | 354 | ## 1.29.4 - 2022-02-22 355 | 356 | ## What's Changed 357 | 358 | - check if ApplicationLogPayload can be loaded by @ThomasEnssner in https://github.com/spatie/laravel-ray/pull/242 359 | 360 | ## New Contributors 361 | 362 | - @ThomasEnssner made their first contribution in https://github.com/spatie/laravel-ray/pull/242 363 | 364 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.3...1.29.4 365 | 366 | ## 1.29.3 - 2022-02-15 367 | 368 | - correctly display mailables that are written to the log in Laravel 9 369 | 370 | ## 1.29.2 - 2022-02-13 371 | 372 | ## What's Changed 373 | 374 | - Fix deprecated by @TiiFuchs in https://github.com/spatie/laravel-ray/pull/240 375 | 376 | ## New Contributors 377 | 378 | - @TiiFuchs made their first contribution in https://github.com/spatie/laravel-ray/pull/240 379 | 380 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.29.1...1.29.2 381 | 382 | ## 1.29.1 - 2022-02-09 383 | 384 | - moved dependency 385 | 386 | ## 1.29.0 - 2022-01-13 387 | 388 | - automatically set project name 389 | 390 | ## 1.28.0 - 2022-01-11 391 | 392 | 1.28.0 393 | 394 | - allow Laravel 9 395 | 396 | ## 1.28.0 - 2022-01-11 397 | 398 | - allow Laravel 9 399 | 400 | ## 1.27.2 - 2021-12-27 401 | 402 | - Fix: make sure there is always a `VarDumper` handler registered to output to HTML or CLI 403 | 404 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.27.1...1.27.2 405 | 406 | ## 1.27.1 - 2021-12-27 407 | 408 | ## What's Changed 409 | 410 | - Register `DumpRecorder` only once and keep original handler connected by @AlexVanderbist in https://github.com/spatie/laravel-ray/pull/233 411 | 412 | ## New Contributors 413 | 414 | - @AlexVanderbist made their first contribution in https://github.com/spatie/laravel-ray/pull/233 415 | 416 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.27.0...1.27.1 417 | 418 | ## 1.27.0 - 2021-12-26 419 | 420 | ## What's Changed 421 | 422 | - Slow Query Logging by @patinthehat in https://github.com/spatie/laravel-ray/pull/232 423 | 424 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.5...1.27.0 425 | 426 | ## 1.26.5 - 2021-12-21 427 | 428 | ## What's Changed 429 | 430 | - add support for Symfony 6 by @Nielsvanpach in https://github.com/spatie/laravel-ray/pull/231 431 | 432 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.4...1.26.5 433 | 434 | ## 1.26.4 - 2021-12-10 435 | 436 | ## What's Changed 437 | 438 | - Added DeprecatedNoticeWatcher that piggy backs off of the Application… by @JuanRangel in https://github.com/spatie/laravel-ray/pull/229 439 | 440 | ## New Contributors 441 | 442 | - @JuanRangel made their first contribution in https://github.com/spatie/laravel-ray/pull/229 443 | 444 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.3...1.26.4 445 | 446 | ## 1.26.3 - 2021-11-22 447 | 448 | ## What's Changed 449 | 450 | - Fix typo in ray.php docblock by @iDiegoNL in https://github.com/spatie/laravel-ray/pull/227 451 | 452 | ## New Contributors 453 | 454 | - @iDiegoNL made their first contribution in https://github.com/spatie/laravel-ray/pull/227 455 | 456 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.2...1.26.3 457 | 458 | ## 1.26.2 - 2021-11-15 459 | 460 | ## What's Changed 461 | 462 | - Check if Laravel has been bound with `Facade\FlareClient\Flare` by @crynobone in https://github.com/spatie/laravel-ray/pull/224 463 | 464 | **Full Changelog**: https://github.com/spatie/laravel-ray/compare/1.26.1...1.26.2 465 | 466 | ## 1.26.1 - 2021-10-01 467 | 468 | - fix #217 error with duplicate queries log (#220) 469 | 470 | ## 1.26.0 - 2021-09-27 471 | 472 | - feature duplicate queries (#216) 473 | 474 | ## 1.25.2 - 2021-09-10 475 | 476 | - enhance metadata instead of overriding it in when sending a request (#215) 477 | 478 | ## 1.25.1 - 2021-09-07 479 | 480 | - add support for zbateson/mail-mime-parser v2 (#214) 481 | 482 | ## 1.25.0 - 2021-08-27 483 | 484 | - add tags to cache payload (#210) 485 | 486 | ## 1.24.2 - 2021-07-23 487 | 488 | - fix origin of query builder ray calls (now for real) 489 | 490 | ## 1.24.1 - 2021-07-23 491 | 492 | - fix origin of query builder ray calls 493 | 494 | ## 1.24.0 - 2021-07-23 495 | 496 | - add `ray` macro on query builder 497 | 498 | ## 1.23.0 - 2021-06-24 499 | 500 | - allow multiple mailables ([#204](https://github.com/spatie/laravel-ray/pull/204)) 501 | 502 | ## 1.22.0 - 2021-06-24 503 | 504 | - when a query exception occurs, the query itself will also be sent to Ray. 505 | 506 | ## 1.21.0 - 2021-06-22 507 | 508 | - add `countQueries` 509 | 510 | ## 1.20.2 - 2021-06-21 511 | 512 | - fix `mailable` when using `Mail::fake` 513 | 514 | ## 1.20.1 - 2021-06-15 515 | 516 | - fix origin of stringable 517 | 518 | ## 1.20.0 - 2021-06-15 519 | 520 | - add support for stringables 521 | 522 | ## 1.19.1 - 2021-06-11 523 | 524 | - better HTTP Client logging (#201) 525 | 526 | ## 1.19.0 - 2021-06-04 527 | 528 | - add http logging methods 529 | 530 | ## 1.18.0 - 2021-03-23 531 | 532 | - colorize high severity messages (#197) 533 | 534 | ## 1.17.4 - 2021-04-30 535 | 536 | - check if an exception is passed before log dumping 537 | 538 | ## 1.17.3 - 2021-04-29 539 | 540 | - the package won't send dumps to Ray when dump sending is disabled 541 | 542 | ## 1.17.2 - 2021-04-06 543 | 544 | - Laravel Octane Compatibility (#178) 545 | 546 | ## 1.17.1 - 2021-03-14 547 | 548 | - send exceptions by default 549 | 550 | ## 1.17.0 - 2021-03-13 551 | 552 | - enable/disable sending exceptions to Ray (#173) 553 | 554 | ## 1.16.0 - 2021-03-12 555 | 556 | - allow using `env()` when config is not available (#172) 557 | 558 | ## 1.15.1 - 2021-03-10 559 | 560 | - fix handling of null logs (#171) 561 | 562 | ## 1.15.0 - 2021-03-09 563 | 564 | - add `env` method 565 | 566 | ## 1.14.0 - 2021-03-04 567 | 568 | - add support for hostname 569 | 570 | ## 1.13.0 - 2021-02-22 571 | 572 | - add exception watcher 573 | 574 | ## 1.12.6 - 2021-02-10 575 | 576 | - replace spaces with underscores in `env()` calls (#154) 577 | 578 | ## 1.12.5 - 2021-02-10 579 | 580 | - fix "Package spatie/laravel-ray is not installed" exception (#156) 581 | 582 | ## 1.12.4 - 2021-02-10 583 | 584 | - handle edge case where ray proxy would not be set 585 | 586 | ## 1.12.3 - 2021-02-08 587 | 588 | - chain colours on `show*` methods (#149) 589 | 590 | ## 1.12.2 - 2021-02-07 591 | 592 | - ignore errors caused by using `storage_path` 593 | 594 | ## 1.12.1 - 2021-02-05 595 | 596 | - register watchers on boot (#138) 597 | 598 | ## 1.12.0 - 2021-02-03 599 | 600 | - remove enabled methods (#132) 601 | 602 | ## 1.11.2 - 2021-02-02 603 | 604 | - do not blow up when using `Mail::fake()` 605 | 606 | ## 1.11.1 - 2021-02-01 607 | 608 | - update config file 609 | 610 | ## 1.11.0 - 2021-01-31 611 | 612 | - add view requests 613 | - add view cache 614 | 615 | ## 1.10.1 - 2021-01-31 616 | 617 | - display logged exceptions 618 | 619 | ## 1.10.0 - 2021-01-29 620 | 621 | - add view methods 622 | 623 | ## 1.9.3 - 2021-01-28 624 | 625 | - internals cleanup 626 | 627 | ## 1.9.2 - 2021-01-28 628 | 629 | - improve dependencies 630 | 631 | ## 1.9.1 - 2021-01-25 632 | 633 | - improve service provider 634 | 635 | ## 1.9.0 - 2021-01-22 636 | 637 | - add `showJobs` 638 | 639 | ## 1.8.0 - 2021-01-19 640 | 641 | - the package will now select the best payload type when passing something to `ray()` 642 | 643 | ## 1.7.1 - 2021-01-17 644 | 645 | - lower dependencies 646 | 647 | ## 1.7.0 - 2021-01-15 648 | 649 | - make `model` more flexible 650 | 651 | ## 1.6.1 - 2021-01-15 652 | 653 | - better support for logged mailables 654 | 655 | ## 1.6.0 - 2021-01-15 656 | 657 | - add `markdown` method 658 | 659 | ## 1.5.2 - 2021-01-13 660 | 661 | - fix headers on response payload 662 | 663 | ## 1.5.1 - 2021-01-13 664 | 665 | - make the test response macro chainable 666 | 667 | ## 1.5.0 - 2021-01-13 668 | 669 | - add `testResponse` method 670 | 671 | ## 1.4.0 - 2021-01-13 672 | 673 | - let the `model` call accepts multiple models. 674 | 675 | ## 1.3.6 - 2021-01-13 676 | 677 | - update `str_replace()` calls in `ray:publish-config` with `env()` usage (#82) 678 | 679 | ## 1.3.5 - 2021-01-12 680 | 681 | - improve recognizing mails in logs 682 | 683 | ## 1.3.4 - 2021-01-09 684 | 685 | - add `env()` vars for each Laravel config setting (#55) 686 | 687 | ## 1.3.3 - 2021-01-09 688 | 689 | - add `enabled()` and `disabled()` methods (#54) 690 | 691 | ## 1.3.2 - 2021-01-09 692 | 693 | - fix frame for `rd` function 694 | 695 | ## 1.3.1 - 2021-01-09 696 | 697 | - fix broken `queries()`-method (#51) 698 | 699 | ## 1.3.0 - 2021-01-08 700 | 701 | - Add `PublishConfigCommand` 702 | 703 | ## 1.2.0 - 2021-01-08 704 | 705 | - add support for `local_path` and `remote_path` settings 706 | 707 | ## 1.1.0 - 2021-01-07 708 | 709 | - add support for Lumen (#22) 710 | 711 | ## 1.0.3 - 20201-01-07 712 | 713 | - fix incompatibilities on Windows (#20) 714 | - fix host settings (#14) 715 | 716 | ## 1.0.2 - 2021-01-07 717 | 718 | - fix deps 719 | 720 | ## 1.0.1 - 2021-01-07 721 | 722 | - fix deps 723 | 724 | ## 1.0.0 - 2021-01-07 725 | 726 | - initial release 727 | --------------------------------------------------------------------------------