diff --git a/README.md b/README.md index 0978880..d877b94 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Laravel Exception Notifications -An easy way to send emails with stack trace whenever an exception occurs on the server for Laravel applications. +An easy way to send Notifications via email and slack channels along with full stack trace whenever an exception occurs on the server for Laravel applications. ![sneaker example image](sneaker.png?raw=true "Sneaker") @@ -15,96 +15,114 @@ $ composer require squareboat/sneaker ``` ### Configure Laravel - -Once installation operation is complete, simply add the service provider to your project's `config/app.php` file: +Once installation operation is complete, simply add both the service provider and facade classes to your project's `config/app.php` file: #### Service Provider -``` + +```php SquareBoat\Sneaker\SneakerServiceProvider::class, ``` -### Add Sneaker's Exception Capturing +#### Facade +```php +'Sneaker' => SquareBoat\Sneaker\Facades\Sneaker::class, +``` + +### Add Sneaker's Exception Capturing Add exception capturing to `app/Exceptions/Handler.php`: ```php public function report(Exception $exception) { - app('sneaker')->captureException($exception); + if ($this->shouldReport($exception)) { + Sneaker::captureException($exception); + } parent::report($exception); } ``` -### Configuration File +### Configuration -Create the Sneaker configuration file with this command: +Create the Sneaker configuration file using this command: ```bash $ php artisan vendor:publish --provider="SquareBoat\Sneaker\SneakerServiceProvider" ``` -The config file will be published in `config/sneaker.php` +The config file will be published in `config/sneaker.php` Following are the configuration attributes used for the Sneaker. #### silent -The package comes with `'silent' => true,` configuration by default, since you probably don't want error emailing enabled on your development environment. Especially if you've set `'debug' => true,`. +The package comes with `'silent' => false,` configuration by default. It means that Sneaker is configured to send notifications when an exception occurs. ```php -'silent' => env('SNEAKER_SILENT', true), +'silent' => env('SNEAKER_SILENT', false), ``` - -For sending emails when an exception occurs set `SNEAKER_SILENT=false` in your `.env` file. - +To avoid sending notifications on your development environment set `SNEAKER_SILENT=true` in your `.env` file. #### capture -It contains the list of the exception types that should be captured. You can add your exceptions here for which you want to send error emails. +It contains the list of the exception types that should be captured. You can add your exceptions types here for which you want to send notifiactions. -By default, the package has included `Symfony\Component\Debug\Exception\FatalErrorException::class`. +By defautl we have `'*'` in the array, which means that we will capture all the exceptions that occurs in the application. ```php 'capture' => [ - Symfony\Component\Debug\Exception\FatalErrorException::class, + '*' ], ``` -You can also use `'*'` in the `$capture` array which will in turn captures every exception. +To explicitly list the exception types, remove `'*'` and define them as: ```php 'capture' => [ - '*' + Symfony\Component\Debug\Exception\FatalErrorException::class, + ... ], ``` -To use this feature you should add the following code in `app/Exceptions/Handler.php`: +#### notifications -```php -public function report(Exception $exception) -{ - if ($this->shouldReport($exception)) { - app('sneaker')->captureException($exception); - } +It lists the channels on which the notification will be delivered. Out of the box, Senaker supports `'mail'` and `'slack'` channels, and plan is to add more in future. - parent::report($exception); -} +```bash +'notifications' => [ + 'mail', + 'slack', +], +``` + +#### mail + +### to +The email address used to deliver the mail notification. + +```php +'mail' => [ + 'to' => [ + // 'your@email.com', + ], +], ``` -#### to +#### slack -This is the list of recipients of error emails. +### webhook_url +The webhook URL to which the slack notification should be delivered. Webhook URLs may be generated by adding an "Incoming Webhook" service to your Slack team. ```php -'to' => [ - // 'hello@example.com', +'slack' => [ + 'webhook_url' => env('SNEAKER_SLACK_WEBHOOK_URL'), ], ``` #### ignored_bots -This is the list of bots for which we should NOT send error emails. +This is the list of bots for which we should NOT send error notifications. ```php 'ignored_bots' => [ @@ -115,29 +133,67 @@ This is the list of bots for which we should NOT send error emails. ], ``` -## Customize +## Adding Context +Sometimes you may need more information for debugging when an exception occurs like which user got the excpetion, app version, etc... -If you need to customize the subject and body of email, run following command: +For that we can add some context as: -```bash -$ php artisan vendor:publish --provider="SquareBoat\Sneaker\SneakerServiceProvider" +### User Context +You can add user context to be send in each notification. + +```php +Sneaker::userContext(function() { + return [ + 'ID' => 10, + 'Name' => 'John Doe' + ]; +}); ``` -> Note - Don't run this command again if you have run it already. +### Extra Context +You can add extra context to be send in each notification. -Now the email's subject and body view are located in the `resources/views/vendor/sneaker` directory. +```php +Sneaker::extraContext(function() { + return [ + 'App' => 'Project X', + 'Version' => 'v3.0.0' + ]; +}); +``` -We have passed the thrown exception object `$exception` in the view which you can use to customize the view to fit your needs. +The best place to add them is in `report()` method of `app/Exceptions/Handler.php`: + +```php +public function report(Exception $exception) +{ + Sneaker::userContext(function() { + // return user context array + }); + + Sneaker::extraContext(function() { + // return extra context array + }); + + if ($this->shouldReport($exception)) { + Sneaker::captureException($exception); + } + + parent::report($exception); +} +``` ## Sneak ### Test your integration -To verify that Sneaker is configured correctly and our integration is working, use `sneaker:sneak` Artisan command: +To verify that Sneaker is configured correctly and your integration is working, use `sneaker:sneak` Artisan command: ```bash $ php artisan sneaker:sneak ``` -A `SquareBoat\Sneaker\Exceptions\DummyException` class will be thrown and captured by Sneaker. The captured exception will appear in your configured email immediately. +A `SquareBoat\Sneaker\Exceptions\DummyException` class will be thrown and captured by Sneaker. The captured exception will appear in your configured notifiaction channel immediately. + +You can also add the verbosity flags `-v/-vv/-vvv`. ## Security diff --git a/composer.json b/composer.json index 4266c78..4dc908c 100644 --- a/composer.json +++ b/composer.json @@ -12,17 +12,23 @@ ], "require": { "php": ">=5.4.0", + "guzzlehttp/guzzle": "^6.2", "illuminate/support": "5.3.*|5.4.*", - "illuminate/view": "5.3.*|5.4.*", "illuminate/config": "5.3.*|5.4.*", - "illuminate/mail": "5.3.*|5.4.*", + "illuminate/notifications": "5.3.*|5.4.*", "illuminate/log": "5.3.*|5.4.*", - "symfony/debug": "~3.1|~3.2" + "illuminate/console": "5.3.*|5.4.*", + "symfony/debug": "~3.1|~3.2", + "symfony/console": "~3.1|~3.2", + "nesbot/carbon": "~1.20" }, "autoload": { "psr-4": { "SquareBoat\\Sneaker\\": "src/" - } + }, + "files": [ + "src/helpers.php" + ] }, "minimum-stability": "dev" } diff --git a/config/sneaker.php b/config/sneaker.php index a8d3803..a5c8cf6 100644 --- a/config/sneaker.php +++ b/config/sneaker.php @@ -4,27 +4,46 @@ /* |-------------------------------------------------------------------------- - | Sends an email on Exception or be silent + | Sends a notification on Exception or be silent. |-------------------------------------------------------------------------- | | Should we email error traces? | */ - 'silent' => env('SNEAKER_SILENT', true), + 'silent' => env('SNEAKER_SILENT', false), /* |-------------------------------------------------------------------------- - | A list of the exception types that should be captured. + | A list of exception types that should be captured. |-------------------------------------------------------------------------- | - | For which exception class emails should be sent? + | For which exception type notification should be sent? | - | You can also use '*' in the array which will in turn captures every - | exception. + | By defautl we have set the array to '*', which means that we will capture + | all the exceptions that occurs in the application. To explicitly list + | the class define them below as: + | + | 'capture' => [ + | Symfony\Component\Debug\Exception\FatalErrorException::class, + | ], | */ 'capture' => [ - Symfony\Component\Debug\Exception\FatalErrorException::class, + '*' + ], + + /* + |-------------------------------------------------------------------------- + | Notification Delivery Channels + |-------------------------------------------------------------------------- + | + | The channels on which the notification will be delivered. + | + */ + + 'notifications' => [ + 'mail', + 'slack', ], /* @@ -32,12 +51,27 @@ | Error email recipients |-------------------------------------------------------------------------- | - | Email stack traces to these addresses. + | The email address used to deliver the notification. + | + */ + + 'mail' => [ + 'to' => [ + // 'your@email.com', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Slack Webhook Url + |-------------------------------------------------------------------------- + | + | The webhook URL to which the notification should be delivered. | */ - 'to' => [ - // 'hello@example.com', + 'slack' => [ + 'webhook_url' => env('SNEAKER_SLACK_WEBHOOK_URL'), ], /* @@ -45,7 +79,7 @@ | Ignore Crawler Bots |-------------------------------------------------------------------------- | - | For which bots should we NOT send error emails? + | For which bots should we NOT send error notifications? | */ 'ignored_bots' => [ diff --git a/resources/views/email/body.blade.php b/resources/views/email/body.blade.php index 944cd8c..4ae658b 100644 --- a/resources/views/email/body.blade.php +++ b/resources/views/email/body.blade.php @@ -12,22 +12,129 @@ .extra-info { background-color: #FFFFFF; padding: 15px 28px; + margin: 0 auto; margin-bottom: 20px; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; border: 1px solid #ccc; + width: 970px; } - {!! $css !!} + + .padding { + padding: 15px 28px; + } + + .extra-info .title { + color: #4674ca; + font-size: large; + font-family: Monaco,monospace; + border-bottom: 1px solid #ccc; + } + + .tags.no-margin { + margin-bottom: -10px; + } + .tags { + padding-left: 0; + list-style: none; + display: flex; + flex-wrap: wrap; + font-size: 13px; + } + .tags li { + white-space: nowrap; + margin: 0 10px 10px 0; + border-radius: 1px; + display: flex; + border: 1px solid #d0c9d7; + border-radius: 3px; + box-shadow: 0 1px 2px rgba(0,0,0,.04); + line-height: 1.2; + max-width: 100%; + } + .tags .key, .tags .value { + padding: 4px 8px; + min-width: 0; + white-space: nowrap; + } + .tags .value, .tags .value>a { + max-width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + } + .tags .key { + font-family:inherit; + } + .tags .value { + color: #4674ca; + background: #fbfbfc; + border-left: 1px solid #d8d2de; + border-radius: 0 3px 3px 0; + font-family: Monaco,monospace; + } + .tags .key, .tags .value { + padding: 4px 8px; + min-width: 0; + white-space: nowrap; + } + + {!! $report->getHtmlStylesheet() !!} - {!! $content !!} -
- Requested Url - {{ request()->url() }} + {!! $report->getHtmlContent() !!} + + @if($report->getUser()) +
+
User
+
+
+ @foreach ($report->getUser() as $key => $item) +
  • + {{ $key }} + {{ $item }} +
  • + @endforeach +
    +
    +
    + @endif + + @if($report->getExtra()) +
    +
    Extra Data
    +
    +
    + @foreach ($report->getExtra() as $key => $item) +
  • + {{ $key }} + {{ $item }} +
  • + @endforeach +
    +
    +
    + @endif + +
    +
    Request
    +
    +
    + @foreach ($report->getRequest() as $key => $item) +
  • + {{ $key }} + {{ $item }} +
  • + @endforeach +
    +
    -
    - 🕐  {{ date('l, jS \of F Y h:i:s a') }} {{ date_default_timezone_get() }} + +
    +
    + 🕐  {{ $report->getTime()->format('l, jS \of F Y h:i:s a') }} {{ $report->getTime()->tzName }} +
    diff --git a/resources/views/email/subject.blade.php b/resources/views/email/subject.blade.php deleted file mode 100644 index 686f886..0000000 --- a/resources/views/email/subject.blade.php +++ /dev/null @@ -1 +0,0 @@ -[Sneaker] | {{ get_class($exception) }} | Server - {{ request()->server('SERVER_NAME') }} | Environment - {{ config('app.env') }} diff --git a/resources/views/raw.blade.php b/resources/views/raw.blade.php deleted file mode 100644 index 0e9c7db..0000000 --- a/resources/views/raw.blade.php +++ /dev/null @@ -1 +0,0 @@ -{!! $content !!} diff --git a/src/Commands/Sneak.php b/src/Commands/Sneak.php index 195e3ed..fa98525 100644 --- a/src/Commands/Sneak.php +++ b/src/Commands/Sneak.php @@ -3,6 +3,7 @@ namespace SquareBoat\Sneaker\Commands; use Exception; +use SquareBoat\Sneaker\Sneaker; use Illuminate\Console\Command; use Illuminate\Config\Repository; use SquareBoat\Sneaker\Exceptions\DummyException; @@ -31,17 +32,27 @@ class Sneak extends Command */ private $config; + /** + * The sneaker implementation. + * + * @var \SquareBoat\Sneaker\Sneaker + */ + private $sneaker; + /** * Create a sneak command instance. * * @param \Illuminate\Config\Repository $config + * @param \SquareBoat\Sneaker\Sneaker $sneaker * @return void */ - public function __construct(Repository $config) + public function __construct(Repository $config, Sneaker $sneaker) { parent::__construct(); $this->config = $config; + + $this->sneaker = $sneaker; } /** @@ -54,11 +65,24 @@ public function handle() $this->overrideConfig(); try { - app('sneaker')->captureException(new DummyException, true); + $this->sneaker + ->userContext(function() { + return [ + 'ID' => 10, + 'Name' => 'John Doe' + ]; + }) + ->extraContext(function() { + return [ + 'App' => 'Project X', + 'Version' => 'v3.0.0' + ]; + }) + ->captureException(new DummyException, true); $this->info('Sneaker is working fine ✅'); - } catch (Exception $e) { - (new ConsoleApplication)->renderException($e, $this->output); + } catch (Exception $exception) { + (new ConsoleApplication)->renderException($exception, $this->output); } } diff --git a/src/ExceptionMailer.php b/src/ExceptionMailer.php deleted file mode 100644 index cf59283..0000000 --- a/src/ExceptionMailer.php +++ /dev/null @@ -1,50 +0,0 @@ -subject = $subject; - - $this->body = $body; - } - - /** - * Build the message. - * - * @return $this - */ - public function build() - { - return $this->view('sneaker::raw') - ->with('content', $this->body); - } -} diff --git a/src/ExceptionHandler.php b/src/Exceptions/Handler.php similarity index 52% rename from src/ExceptionHandler.php rename to src/Exceptions/Handler.php index 5aaf111..e34443e 100644 --- a/src/ExceptionHandler.php +++ b/src/Exceptions/Handler.php @@ -1,55 +1,80 @@ view = $view; + $this->exception = $exception; } /** - * Create a string for the given exception. + * Get the name of given exception. + * + * @return string + */ + public function getExceptionName() + { + return get_class($this->exception); + } + + /** + * Convert the given exception to a readable message. * - * @param \Exception $exception * @return string */ - public function convertExceptionToString($exception) + public function convertExceptionToMessage() { - return $this->view->make('sneaker::email.subject', compact('exception'))->render(); + return sprintf("Exception '%s' with message '%s' in %s", + $this->getExceptionName(), + $this->exception->getMessage(), + $this->exception->getFile() + ); } /** - * Create a html for the given exception. + * Convert the given exception to a stack trace string. + * + * @return string + */ + public function convertExceptionToStacktrace() + { + return $this->exception->getTraceAsString(); + } + + /** + * Convert the given exception to a html. * - * @param \Exception $exception * @return string */ - public function convertExceptionToHtml($exception) + public function convertExceptionToHtml() { - $flat = $this->getFlattenedException($exception); + $flattened = $this->getFlattenedException($this->exception); $handler = new SymfonyExceptionHandler(); - return $this->decorate($handler->getContent($flat), $handler->getStylesheet($flat), $flat); + return [ + 'content' => $this->removeTitle($handler->getContent($flattened)), + 'stylesheet' => $handler->getStylesheet($flattened) + ]; } /** @@ -60,27 +85,13 @@ public function convertExceptionToHtml($exception) */ private function getFlattenedException($exception) { - if (!$exception instanceof FlattenException) { + if (! $exception instanceof FlattenException) { $exception = FlattenException::create($exception); } return $exception; } - /** - * Get the html response content. - * - * @param string $content - * @param string $css - * @return string - */ - private function decorate($content, $css, $exception) - { - $content = $this->removeTitle($content); - - return $this->view->make('sneaker::email.body', compact('content', 'css', 'exception'))->render(); - } - /** * Removes title from content as it is same for all exceptions and has no real value. * diff --git a/src/Facades/Sneaker.php b/src/Facades/Sneaker.php new file mode 100644 index 0000000..b062f3a --- /dev/null +++ b/src/Facades/Sneaker.php @@ -0,0 +1,21 @@ +writeln($text) + ->br(); + } + + /** + * @param string $header + * @return $this + */ + public function h1($header) + { + $header = $this->singleLine($header); + + return $this + ->writeln($header) + ->writeln(str_repeat("=", mb_strlen($header))) + ->br(); + } + + /** + * @param string $header + * @return $this + */ + public function h2($header) + { + $header = $this->singleLine($header); + + return $this + ->writeln($header) + ->writeln(str_repeat("-", mb_strlen($header))) + ->br(); + } + + /** + * @param string $header + * @return $this + */ + public function h3($header) + { + $header = $this->singleLine($header); + + return $this + ->writeln('### ' . $header) + ->br(); + } + + /** + * @param string $text + * @return $this + */ + public function blockqoute($text) + { + $lines = explode("\n", $text); + $newLines = array_map(function ($line) { + return trim('> ' . $line); + }, $lines); + + $content = implode("\n", $newLines); + + return $this->p($content); + } + + /** + * @param array $list + * @return $this + */ + public function bulletedList(array $list) + { + foreach ($list as $element) { + + $lines = explode("\n", $element); + + foreach ($lines as $i => $line) { + if ($i == 0) { + $this->writeln('* ' . $line); + } else { + $this->writeln(' ' . $line); + } + } + } + + $this->br(); + + return $this; + } + + /** + * @param array $list + * @return $this + */ + public function numberedList(array $list) + { + foreach (array_values($list) as $key => $element) { + + $lines = explode("\n", $element); + + foreach ($lines as $i => $line) { + if ($i == 0) { + $this->writeln(($key + 1) . '. ' . $line); + } else { + $this->writeln(' ' . $line); + } + } + } + + $this->br(); + + return $this; + } + + /** + * @return $this + */ + public function hr() + { + return $this->p('---------------------------------------'); + } + + /** + * @param string $code + * @param string $lang + * @return $this + */ + public function code($code, $lang = '') + { + return $this + ->writeln('```' . $lang) + ->writeln($code) + ->writeln('```') + ->br(); + } + + /** + * @return $this + */ + public function br() + { + return $this->write("\n"); + } + + /** + * @param string $code + * @return string + */ + public function inlineCode($code) + { + return sprintf('`%s`', $code); + } + + /** + * @param string $string + * @return string + */ + public function inlineItalic($string) + { + return sprintf('*%s*', $string); + } + + /** + * @param string $string + * @return string + */ + public function inlineBold($string) + { + return sprintf('**%s**', $string); + } + + /** + * @param string $url + * @param string $title + * @return string + */ + public function inlineLink($url, $title) + { + return sprintf('[%s](%s)', $title, $url); + } + + /** + * @param string $url + * @param string $title + * @return string + */ + public function inlineImg($url, $title) + { + return sprintf('![%s](%s)', $title, $url); + } + + /** + * @return string + */ + public function getMarkdown() + { + return trim($this->markdown); + } + + /** + * @param string $string + * @return $this + */ + protected function writeln($string) + { + return $this + ->write($string) + ->br(); + } + + /** + * @param string $string + * @return $this + */ + protected function write($string) + { + $this->markdown .= $string; + + return $this; + } + + /** + * @param $string + * @return mixed + */ + protected function singleLine($string) + { + $string = str_replace("\n", "", $string); + $string = preg_replace('/\s+/', " ", $string); + + return trim($string); + } + + /** + * @return string + */ + public function __toString() + { + return $this->getMarkdown(); + } +} diff --git a/src/Notifiable.php b/src/Notifiable.php new file mode 100644 index 0000000..5027eed --- /dev/null +++ b/src/Notifiable.php @@ -0,0 +1,137 @@ +fill($attributes); + } + + /** + * Route notifications for the mail channel. + * + * @return string|array + */ + public function routeNotificationForMail() + { + return $this->emails; + } + + /** + * Route notifications for the Slack channel. + * + * @return string + */ + public function routeNotificationForSlack() + { + return $this->slack_webhook_url; + } + + /** + * Get an attribute from the model. + * + * @param string $key + * @return mixed + */ + public function getAttribute($key) + { + if (isset($this->attributes[$key])) { + return $this->attributes[$key]; + } + } + + /** + * Fill the model with an array of attributes. + * + * @param array $attributes + * @return $this + * + * @throws \Illuminate\Database\Eloquent\MassAssignmentException + */ + public function fill(array $attributes) + { + foreach ($attributes as $key => $value) { + $this->setAttribute($key, $value); + } + + return $this; + } + + /** + * Set a given attribute on the model. + * + * @param string $key + * @param mixed $value + * @return $this + */ + public function setAttribute($key, $value) + { + $this->attributes[$key] = $value; + + return $this; + } + + /** + * Dynamically retrieve attributes on the report. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + return $this->getAttribute($key); + } + + /** + * Dynamically set attributes on the report. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function __set($key, $value) + { + $this->setAttribute($key, $value); + } + + /** + * Determine if an attribute exists on the report. + * + * @param string $key + * @return bool + */ + public function __isset($key) + { + return ! is_null($this->getAttribute($key)); + } + + /** + * Unset an attribute on the report. + * + * @param string $key + * @return void + */ + public function __unset($key) + { + unset($this->attributes[$key]); + } +} diff --git a/src/Notifications/ExceptionCaught.php b/src/Notifications/ExceptionCaught.php new file mode 100644 index 0000000..1f204f3 --- /dev/null +++ b/src/Notifications/ExceptionCaught.php @@ -0,0 +1,134 @@ +report = $report; + + $this->channels = $channels; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return $this->channels; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->subject($this->getSubject()) + ->view('sneaker::email.body', ['report' => $this->report]); + } + + /** + * Get the Slack representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\SlackMessage + */ + public function toSlack($notifiable) + { + return (new SlackMessage) + ->error() + ->content($this->getSubject()) + ->attachment(function ($attachment) { + $attachment->title($this->report->getMessage()) + ->content((string) Markdown::block()->code($this->report->getStacktrace())) + ->markdown(['title', 'text']); + }) + ->attachment(function ($attachment) { + if($user = $this->report->getUser()) { + $attachment->title('User') + ->fields($this->getPayload($user)) + ->markdown(['fields']); + } + }) + ->attachment(function ($attachment) { + if($extra = $this->report->getExtra()) { + $attachment->title('Extra Data') + ->fields($this->getPayload($extra)) + ->markdown(['fields']); + } + }) + ->attachment(function ($attachment) { + $attachment->title('Request') + ->fields($this->getPayload($this->report->getRequest())) + ->markdown(['fields']) + ->timestamp($this->report->getTime()) + ->footer('Sneaker'); + }); + } + + /** + * Get the subject of notification. + * + * @return string + */ + private function getSubject() + { + return sprintf("[Sneaker] | %s | Server - %s | Environment - %s", + $this->report->getName(), + request()->server('SERVER_NAME'), + $this->report->getEnv() + ); + } + + /** + * Add markdoen to given payload. + * + * @param array $items + * @return array + */ + private function getPayload($items) + { + return array_map(function($item) { + return (string) Markdown::block()->code($item); + }, $items); + } +} diff --git a/src/Report.php b/src/Report.php new file mode 100644 index 0000000..af25ea0 --- /dev/null +++ b/src/Report.php @@ -0,0 +1,295 @@ +time = Carbon::now(); + } + + /** + * Set the error name. + * + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get the error name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set the error message. + * + * @param string $message + * @return $this + */ + public function setMessage($message) + { + $this->message = $message; + + return $this; + } + + /** + * Get the error message. + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Get the error time. + * + * @return string + */ + public function getTime() + { + return $this->time; + } + + /** + * Set the application environment. + * + * @param string $env + * @return $this + */ + public function setEnv($env) + { + $this->env = $env; + + return $this; + } + + /** + * Get the application environment. + * + * @return string + */ + public function getEnv() + { + return $this->env; + } + + /** + * Set the error stacktrace. + * + * @param string $stacktrace + * @return $this + */ + public function setStacktrace($stacktrace) + { + $this->stacktrace = $stacktrace; + + return $this; + } + + /** + * Get the error stacktrace. + * + * @return string + */ + public function getStacktrace() + { + return $this->stacktrace; + } + + /** + * Set the error html. + * + * @param string $html + * @return $this + */ + public function setHtml($html) + { + $this->html = $html; + + return $this; + } + + /** + * Get the error html. + * + * @return string + */ + public function getHtml() + { + return $this->html; + } + + /** + * Get the error html content. + * + * @return string + */ + public function getHtmlContent() + { + return $this->html['content']; + } + + /** + * Get the error html content. + * + * @return string + */ + public function getHtmlStylesheet() + { + return $this->html['stylesheet']; + } + + /** + * Set the associated request. + * + * @param string $request + * @return $this + */ + public function setRequest($request) + { + $this->request = $request; + + return $this; + } + + /** + * Get the associated request. + * + * @return array + */ + public function getRequest() + { + return $this->request; + } + + /** + * Set the associated user. + * + * @param string $user + * @return $this + */ + public function setUser($user) + { + $this->user = $user; + + return $this; + } + + /** + * Get the associated user. + * + * @return array + */ + public function getUser() + { + return $this->user; + } + + /** + * Set the associated extra data. + * + * @param string $extra + * @return $this + */ + public function setExtra($extra) + { + $this->extra = $extra; + + return $this; + } + + /** + * Get the associated extra data. + * + * @return array + */ + public function getExtra() + { + return $this->extra; + } +} diff --git a/src/Request.php b/src/Request.php new file mode 100644 index 0000000..27ae729 --- /dev/null +++ b/src/Request.php @@ -0,0 +1,63 @@ +request = $request; + } + + /** + * Get the request formatted as meta data. + * + * @return array + */ + public function getMetaData() + { + $data = []; + + $data['URL'] = $this->request->fullUrl(); + + $data['Method'] = $this->request->getMethod(); + + $data['IP-Address'] = $this->request->getClientIp(); + + if ($headers = $this->request->headers->all()) { + $data['Host'] = Arr::get($headers, 'host.0'); + + $data['Connection'] = Arr::get($headers, 'connection.0'); + + $data['Upgrade-Insecure-Requests'] = Arr::get($headers, 'upgrade-insecure-requests.0'); + + $data['User-Agent'] = Arr::get($headers, 'user-agent.0'); + + $data['Accept'] = Arr::get($headers, 'accept.0'); + + $data['Referer'] = Arr::get($headers, 'referer.0'); + + $data['Accept-Encoding'] = Arr::get($headers, 'accept-encoding.0'); + + $data['Accept-Language'] = Arr::get($headers, 'accept-language.0'); + } + + return array_filter($data); + } +} \ No newline at end of file diff --git a/src/Sneaker.php b/src/Sneaker.php index 15cc729..4b74e5b 100644 --- a/src/Sneaker.php +++ b/src/Sneaker.php @@ -3,59 +3,56 @@ namespace SquareBoat\Sneaker; use Exception; -use Illuminate\Log\Writer; -use Illuminate\Contracts\Mail\Mailer; -use Illuminate\Config\Repository; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; +use Illuminate\Contracts\Logging\Log; +use Illuminate\Contracts\Config\Repository; +use SquareBoat\Sneaker\Notifications\ExceptionCaught; +use SquareBoat\Sneaker\Exceptions\Handler as ExceptionHandler; class Sneaker { /** * The config implementation. * - * @var \Illuminate\Config\Repository + * @var \Illuminate\Contracts\Config\Repository */ private $config; /** - * The exception handler implementation. + * The request implementation. * - * @var \SquareBoat\Sneaker\ExceptionHandler + * @var \SquareBoat\Sneaker\Request */ - private $handler; + private $request; /** - * The mailer instance. - * - * @var \Illuminate\Contracts\Mail\Mailer + * The log writer implementation. + * + * @var \Illuminate\Contracts\Logging\Log */ - private $mailer; + private $logger; /** - * The log writer implementation. + * The meta data to be added in sneaker notifications. * - * @var \Illuminate\Log\Writer + * @var array */ - private $logger; + private $metaData = []; /** * Create a new sneaker instance. * - * @param \Illuminate\Config\Repository $config - * @param \SquareBoat\Sneaker\ExceptionHandler $handler - * @param \Illuminate\Contracts\Mail\Mailer $mailer - * @param \Illuminate\Log\Writer $logger + * @param \Illuminate\Contracts\Config\Repository $config + * @param \SquareBoat\Sneaker\Request $request + * @param \Illuminate\Contracts\Logging\Log $logger * @return void */ - public function __construct(Repository $config, - ExceptionHandler $handler, - Mailer $mailer, - Writer $logger) + public function __construct(Repository $config, Request $request, Log $logger) { $this->config = $config; - $this->handler = $handler; - - $this->mailer = $mailer; + $this->request = $request; $this->logger = $logger; } @@ -64,6 +61,7 @@ public function __construct(Repository $config, * Checks an exception which should be tracked and captures it if applicable. * * @param \Exception $exception + * @param bool $sneaking * @return void */ public function captureException(Exception $exception, $sneaking = false) @@ -81,12 +79,7 @@ public function captureException(Exception $exception, $sneaking = false) $this->capture($exception); } } catch (Exception $e) { - $this->logger->error(sprintf( - 'Exception thrown in Sneaker when capturing an exception (%s: %s)', - get_class($e), $e->getMessage() - )); - - $this->logger->error($e); + $this->logSneakerException($e); if ($sneaking) { throw $e; @@ -94,23 +87,6 @@ public function captureException(Exception $exception, $sneaking = false) } } - /** - * Capture an exception. - * - * @param \Exception $exception - * @return void - */ - private function capture($exception) - { - $recipients = $this->config->get('sneaker.to'); - - $subject = $this->handler->convertExceptionToString($exception); - - $body = $this->handler->convertExceptionToHtml($exception); - - $this->mailer->to($recipients)->send(new ExceptionMailer($subject, $body)); - } - /** * Checks if sneaker is silent. * @@ -127,7 +103,7 @@ private function isSilent() * @param Exception $exception * @return boolean */ - private function shouldCapture(Exception $exception) + private function shouldCapture($exception) { $capture = $this->config->get('sneaker.capture'); @@ -173,4 +149,112 @@ private function isExceptionFromBot() return false; } + + /** + * Capture an exception. + * + * @param \Exception $exception + * @return void + */ + private function capture($exception) + { + $report = $this->getReport($exception); + + $notifiable = $this->getNotifiable(); + + $notifiable->notify( + new ExceptionCaught($report, $this->config->get('sneaker.notifications')) + ); + } + + /** + * Get the report of exception. + * + * @param \Exception $exception + * @return \SquareBoat\Sneaker\Report + */ + private function getReport($exception) + { + $handler = new ExceptionHandler($exception); + + return (new Report) + ->setEnv($this->config->get('app.env')) + ->setRequest($this->request->getMetaData()) + ->setUser(Arr::get($this->metaData, 'user')) + ->setExtra(Arr::get($this->metaData, 'extra')) + ->setName($handler->getExceptionName()) + ->setHtml($handler->convertExceptionToHtml()) + ->setMessage($handler->convertExceptionToMessage()) + ->setStacktrace($handler->convertExceptionToStacktrace()); + } + + /** + * Get the notifiable. + * + * @return \SquareBoat\Sneaker\Notifiable + */ + private function getNotifiable() + { + return new Notifiable([ + 'emails' => $this->config->get('sneaker.mail.to'), + 'slack_webhook_url' => $this->config->get('sneaker.slack.webhook_url') + ]); + } + + /** + * Logs the exception thrown by Sneaker itself. + * + * @param \Exception $exception + */ + private function logSneakerException(Exception $exception) + { + $this->logger->error(sprintf( + 'Exception thrown in Sneaker when capturing an exception (%s: %s)', + get_class($exception), $exception->getMessage() + )); + + $this->logger->error($exception); + } + + /** + * Execute the user context callback. + * + * @param callable $callback + * @return $this + */ + public function userContext($callback) + { + $this->metaData['user'] = $this->sanitize(call_user_func($callback)); + + return $this; + } + + /** + * Execute the extra context callback. + * + * @param callable $callback + * @return $this + */ + public function extraContext($callback) + { + $this->metaData['extra'] = $this->sanitize(call_user_func($callback)); + + return $this; + } + + /** + * [sanitize description] + * @param [type] $items [description] + * @return [type] [description] + */ + private function sanitize($items) + { + $items = $items instanceof Collection ? $items->all() : $items; + + if (! is_array($items)) { + return []; + } + + return array_flat($items); + } } diff --git a/src/SneakerServiceProvider.php b/src/SneakerServiceProvider.php index aef60ad..af818a8 100644 --- a/src/SneakerServiceProvider.php +++ b/src/SneakerServiceProvider.php @@ -11,10 +11,10 @@ class SneakerServiceProvider extends ServiceProvider * * @var bool */ - protected $defer = false; + protected $defer = true; /** - * Bootstrap the application services. + * Bootstrap the sneaker's services. * * @return void */ @@ -22,10 +22,6 @@ public function boot() { $this->loadViewsFrom(__DIR__ . '/../resources/views', 'sneaker'); - $this->publishes([ - __DIR__ . '/../resources/views/email' => resource_path('views/vendor/sneaker/email') - ], 'views'); - $this->publishes([ __DIR__.'/../config/sneaker.php' => config_path('sneaker.php'), ], 'config'); @@ -38,7 +34,7 @@ public function boot() } /** - * Register the application services. + * Register the sneaker's services. * * @return void */ @@ -52,4 +48,14 @@ public function register() return $this->app->make(Sneaker::class); }); } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return ['sneaker']; + } } diff --git a/src/helpers.php b/src/helpers.php new file mode 100644 index 0000000..776d395 --- /dev/null +++ b/src/helpers.php @@ -0,0 +1,27 @@ + $item) { + $item = $item instanceof Illuminate\Support\Collection ? $item->all() : $item; + + if(is_array($item)) { + $result = $result + array_flat($item, $prefix . $key . '.'); + } else { + $result[$prefix.$key] = $item; + } + } + + return $result; + } +}