├── .gitignore ├── resources └── views │ ├── raw.blade.php │ └── email │ ├── subject.blade.php │ └── body.blade.php ├── sneaker.png ├── src ├── Exceptions │ └── DummyException.php ├── ExceptionMailer.php ├── SneakerServiceProvider.php ├── Commands │ └── Sneak.php ├── ExceptionHandler.php └── Sneaker.php ├── LICENSE.md ├── composer.json ├── config └── sneaker.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock -------------------------------------------------------------------------------- /resources/views/raw.blade.php: -------------------------------------------------------------------------------- 1 | {!! $content !!} 2 | -------------------------------------------------------------------------------- /sneaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squareboat/sneaker/HEAD/sneaker.png -------------------------------------------------------------------------------- /resources/views/email/subject.blade.php: -------------------------------------------------------------------------------- 1 | [Sneaker] | {{ get_class($exception) }} | Server - {{ request()->server('SERVER_NAME') }} | Environment - {{ config('app.env') }} 2 | -------------------------------------------------------------------------------- /src/Exceptions/DummyException.php: -------------------------------------------------------------------------------- 1 | subject = $subject; 36 | 37 | $this->body = $body; 38 | } 39 | 40 | /** 41 | * Build the message. 42 | * 43 | * @return $this 44 | */ 45 | public function build() 46 | { 47 | return $this->view('sneaker::raw') 48 | ->with('content', $this->body); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © 2016 [SquareBoat](https://squareboat.com) 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. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "squareboat/sneaker", 3 | "description": "An easy way to send emails with stack trace whenever an exception occurs on the server for Laravel Applications.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": ["laravel", "exceptions", "email"], 7 | "authors": [ 8 | { 9 | "name": "Amit Gupta", 10 | "email": "akaamitgupta@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.4.0", 15 | "illuminate/support": "5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*|7.*", 16 | "illuminate/view": "5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*|7.*", 17 | "illuminate/config": "5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*|7.*", 18 | "illuminate/mail": "5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*|7.*", 19 | "illuminate/log": "5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*|7.*", 20 | "symfony/debug": "~3.1|~3.2|~4.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "SquareBoat\\Sneaker\\": "src/" 25 | } 26 | }, 27 | "minimum-stability": "dev", 28 | "extra": { 29 | "laravel": { 30 | "providers": [ 31 | "SquareBoat\\Sneaker\\SneakerServiceProvider" 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SneakerServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__ . '/../resources/views', 'sneaker'); 24 | 25 | $this->publishes([ 26 | __DIR__ . '/../resources/views/email' => resource_path('views/vendor/sneaker/email') 27 | ], 'views'); 28 | 29 | $this->publishes([ 30 | __DIR__.'/../config/sneaker.php' => config_path('sneaker.php'), 31 | ], 'config'); 32 | 33 | if ($this->app->runningInConsole()) { 34 | $this->commands([ 35 | \SquareBoat\Sneaker\Commands\Sneak::class, 36 | ]); 37 | } 38 | } 39 | 40 | /** 41 | * Register the application services. 42 | * 43 | * @return void 44 | */ 45 | public function register() 46 | { 47 | $this->mergeConfigFrom( 48 | __DIR__.'/../config/sneaker.php', 'sneaker' 49 | ); 50 | 51 | $this->app->singleton('sneaker', function () { 52 | return $this->app->make(Sneaker::class); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Commands/Sneak.php: -------------------------------------------------------------------------------- 1 | config = $config; 45 | } 46 | 47 | /** 48 | * Execute the console command. 49 | * 50 | * @return void 51 | */ 52 | public function handle() 53 | { 54 | $this->overrideConfig(); 55 | 56 | try { 57 | app('sneaker')->captureException(new DummyException, true); 58 | 59 | $this->info('Sneaker is working fine ✅'); 60 | } catch (Exception $e) { 61 | (new ConsoleApplication)->renderException($e, $this->output); 62 | } 63 | } 64 | 65 | /** 66 | * Overriding the default configurations. 67 | * 68 | * @return void 69 | */ 70 | public function overrideConfig() 71 | { 72 | $this->config->set('sneaker.capture', [DummyException::class]); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /config/sneaker.php: -------------------------------------------------------------------------------- 1 | env('SNEAKER_SILENT', true), 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | A list of the exception types that should be captured. 18 | |-------------------------------------------------------------------------- 19 | | 20 | | For which exception class emails should be sent? 21 | | 22 | | You can also use '*' in the array which will in turn captures every 23 | | exception. 24 | | 25 | */ 26 | 'capture' => [ 27 | Symfony\Component\Debug\Exception\FatalErrorException::class, 28 | ], 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Error email recipients 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Email stack traces to these addresses. 36 | | 37 | */ 38 | 39 | 'to' => [ 40 | // 'hello@example.com', 41 | ], 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Ignore Crawler Bots 46 | |-------------------------------------------------------------------------- 47 | | 48 | | For which bots should we NOT send error emails? 49 | | 50 | */ 51 | 'ignored_bots' => [ 52 | 'yandexbot', // YandexBot 53 | 'googlebot', // Googlebot 54 | 'bingbot', // Microsoft Bingbot 55 | 'slurp', // Yahoo! Slurp 56 | 'ia_archiver', // Alexa 57 | ], 58 | ]; 59 | -------------------------------------------------------------------------------- /resources/views/email/body.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | 24 | 25 | {!! $content !!} 26 |
27 | Requested Url - {{ request()->url() }} 28 |
29 |
30 | 🕐  {{ date('l, jS \of F Y h:i:s a') }} {{ date_default_timezone_get() }} 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /src/ExceptionHandler.php: -------------------------------------------------------------------------------- 1 | view = $view; 27 | } 28 | 29 | /** 30 | * Create a string for the given exception. 31 | * 32 | * @param \Exception $exception 33 | * @return string 34 | */ 35 | public function convertExceptionToString($exception) 36 | { 37 | return $this->view->make('sneaker::email.subject', compact('exception'))->render(); 38 | } 39 | 40 | /** 41 | * Create a html for the given exception. 42 | * 43 | * @param \Exception $exception 44 | * @return string 45 | */ 46 | public function convertExceptionToHtml($exception) 47 | { 48 | $flat = $this->getFlattenedException($exception); 49 | 50 | $handler = new SymfonyExceptionHandler(); 51 | 52 | return $this->decorate($handler->getContent($flat), $handler->getStylesheet($flat), $flat); 53 | } 54 | 55 | /** 56 | * Converts the Exception in a PHP Exception to be able to serialize it. 57 | * 58 | * @param \Exception $exception 59 | * @return \Symfony\Component\Debug\Exception\FlattenException 60 | */ 61 | private function getFlattenedException($exception) 62 | { 63 | if (!$exception instanceof FlattenException) { 64 | $exception = FlattenException::createFromThrowable($exception); 65 | } 66 | 67 | return $exception; 68 | } 69 | 70 | /** 71 | * Get the html response content. 72 | * 73 | * @param string $content 74 | * @param string $css 75 | * @return string 76 | */ 77 | private function decorate($content, $css, $exception) 78 | { 79 | $content = $this->removeTitle($content); 80 | 81 | return $this->view->make('sneaker::email.body', compact('content', 'css', 'exception'))->render(); 82 | } 83 | 84 | /** 85 | * Removes title from content as it is same for all exceptions and has no real value. 86 | * 87 | * @param string $content 88 | * @return string 89 | */ 90 | private function removeTitle($content) 91 | { 92 | $titles = [ 93 | 'Whoops, looks like something went wrong.', 94 | 'Sorry, the page you are looking for could not be found.', 95 | ]; 96 | 97 | foreach ($titles as $title) { 98 | $content = str_replace("

{$title}

", '', $content); 99 | } 100 | 101 | return $content; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Sneaker.php: -------------------------------------------------------------------------------- 1 | config = $config; 54 | 55 | $this->handler = $handler; 56 | 57 | $this->mailer = $mailer; 58 | 59 | $this->logger = $logger; 60 | } 61 | 62 | /** 63 | * Checks an exception which should be tracked and captures it if applicable. 64 | * 65 | * @param \Throwable|\Exception $exception 66 | * @return void 67 | */ 68 | public function captureException($exception, $sneaking = false) 69 | { 70 | try { 71 | if ($this->isSilent()) { 72 | return; 73 | } 74 | 75 | if ($this->isExceptionFromBot()) { 76 | return; 77 | } 78 | 79 | if ($this->shouldCapture($exception)) { 80 | $this->capture($exception); 81 | } 82 | } catch (Exception $e) { 83 | $this->logger->error(sprintf( 84 | 'Exception thrown in Sneaker when capturing an exception (%s: %s)', 85 | get_class($e), $e->getMessage() 86 | )); 87 | 88 | $this->logger->error($e); 89 | 90 | if ($sneaking) { 91 | throw $e; 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Capture an exception. 98 | * 99 | * @param \Exception|\Throwable $exception 100 | * @return void 101 | */ 102 | private function capture($exception) 103 | { 104 | $recipients = $this->config->get('sneaker.to'); 105 | 106 | $subject = $this->handler->convertExceptionToString($exception); 107 | 108 | $body = $this->handler->convertExceptionToHtml($exception); 109 | 110 | $this->mailer->to($recipients)->send(new ExceptionMailer($subject, $body)); 111 | } 112 | 113 | /** 114 | * Checks if sneaker is silent. 115 | * 116 | * @return boolean 117 | */ 118 | private function isSilent() 119 | { 120 | return $this->config->get('sneaker.silent'); 121 | } 122 | 123 | /** 124 | * Determine if the exception is in the "capture" list. 125 | * 126 | * @param \Throwable|\Exception $exception 127 | * @return boolean 128 | */ 129 | private function shouldCapture($exception) 130 | { 131 | $capture = $this->config->get('sneaker.capture'); 132 | 133 | if (! is_array($capture)) { 134 | return false; 135 | } 136 | 137 | if (in_array('*', $capture)) { 138 | return true; 139 | } 140 | 141 | foreach ($capture as $type) { 142 | if ($exception instanceof $type) { 143 | return true; 144 | } 145 | } 146 | 147 | return false; 148 | } 149 | 150 | /** 151 | * Determine if the exception is from the bot. 152 | * 153 | * @return boolean 154 | */ 155 | private function isExceptionFromBot() 156 | { 157 | $ignored_bots = $this->config->get('sneaker.ignored_bots'); 158 | 159 | $agent = array_key_exists('HTTP_USER_AGENT', $_SERVER) 160 | ? strtolower($_SERVER['HTTP_USER_AGENT']) 161 | : null; 162 | 163 | if (is_null($agent)) { 164 | return false; 165 | } 166 | 167 | foreach ($ignored_bots as $bot) { 168 | if ((strpos($agent, $bot) !== false)) { 169 | return true; 170 | } 171 | } 172 | 173 | return false; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Exception Notifications 2 | 3 | An easy way to send emails with stack trace whenever an exception occurs on the server for Laravel applications. 4 | 5 | ![sneaker example image](sneaker.png?raw=true "Sneaker") 6 | 7 | ## Install 8 | 9 | ### Install via Composer 10 | 11 | #### For Laravel <= 5.2, please use the [v1 branch](https://github.com/squareboat/sneaker/tree/v1)! 12 | #### For Laravel 5.2 < version <= 6.x, please use the [v5 branch](https://github.com/squareboat/sneaker/tree/v5)! 13 | 14 | ``` 15 | $ composer require squareboat/sneaker 16 | ``` 17 | 18 | ### Configure Laravel 19 | 20 | > If you are using __laravel 5.5__ or higher you should skip this step. 21 | 22 | If you are using laravel 5.3 or 5.4, simply add the service provider to your project's `config/app.php` file: 23 | 24 | #### Service Provider 25 | ``` 26 | SquareBoat\Sneaker\SneakerServiceProvider::class, 27 | ``` 28 | 29 | ### Add Sneaker's Exception Capturing 30 | 31 | Add exception capturing to `app/Exceptions/Handler.php`: 32 | 33 | ```php 34 | public function report(Exception $exception) 35 | { 36 | app('sneaker')->captureException($exception); 37 | 38 | parent::report($exception); 39 | } 40 | ``` 41 | 42 | ### Configuration File 43 | 44 | Create the Sneaker configuration file with this command: 45 | 46 | ```bash 47 | $ php artisan vendor:publish --provider="SquareBoat\Sneaker\SneakerServiceProvider" 48 | ``` 49 | 50 | The config file will be published in `config/sneaker.php` 51 | 52 | Following are the configuration attributes used for the Sneaker. 53 | 54 | #### silent 55 | 56 | 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,`. 57 | 58 | ```php 59 | 'silent' => env('SNEAKER_SILENT', true), 60 | ``` 61 | 62 | For sending emails when an exception occurs set `SNEAKER_SILENT=false` in your `.env` file. 63 | 64 | 65 | #### capture 66 | 67 | 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. 68 | 69 | By default, the package has included `Symfony\Component\Debug\Exception\FatalErrorException::class`. 70 | 71 | ```php 72 | 'capture' => [ 73 | Symfony\Component\Debug\Exception\FatalErrorException::class, 74 | ], 75 | ``` 76 | 77 | You can also use `'*'` in the `$capture` array which will in turn captures every exception. 78 | 79 | ```php 80 | 'capture' => [ 81 | '*' 82 | ], 83 | ``` 84 | 85 | To use this feature you should add the following code in `app/Exceptions/Handler.php`: 86 | 87 | ```php 88 | public function report(Exception $exception) 89 | { 90 | if ($this->shouldReport($exception)) { 91 | app('sneaker')->captureException($exception); 92 | } 93 | 94 | parent::report($exception); 95 | } 96 | ``` 97 | 98 | #### to 99 | 100 | This is the list of recipients of error emails. 101 | 102 | ```php 103 | 'to' => [ 104 | // 'hello@example.com', 105 | ], 106 | ``` 107 | 108 | #### ignored_bots 109 | 110 | This is the list of bots for which we should NOT send error emails. 111 | 112 | ```php 113 | 'ignored_bots' => [ 114 | 'googlebot', // Googlebot 115 | 'bingbot', // Microsoft Bingbot 116 | 'slurp', // Yahoo! Slurp 117 | 'ia_archiver', // Alexa 118 | ], 119 | ``` 120 | 121 | ## Customize 122 | 123 | If you need to customize the subject and body of email, run following command: 124 | 125 | ```bash 126 | $ php artisan vendor:publish --provider="SquareBoat\Sneaker\SneakerServiceProvider" 127 | ``` 128 | 129 | > Note - Don't run this command again if you have run it already. 130 | 131 | Now the email's subject and body view are located in the `resources/views/vendor/sneaker` directory. 132 | 133 | We have passed the thrown exception object `$exception` in the view which you can use to customize the view to fit your needs. 134 | 135 | ## Sneak 136 | ### Test your integration 137 | To verify that Sneaker is configured correctly and our integration is working, use `sneaker:sneak` Artisan command: 138 | 139 | ```bash 140 | $ php artisan sneaker:sneak 141 | ``` 142 | 143 | A `SquareBoat\Sneaker\Exceptions\DummyException` class will be thrown and captured by Sneaker. The captured exception will appear in your configured email immediately. 144 | 145 | ## Security 146 | 147 | If you discover any security related issues, please email akaamitgupta@gmail.com instead of using the issue tracker. 148 | 149 | ## Credits 150 | 151 | - [Amit Gupta](https://github.com/akaamitgupta) 152 | - [All Contributors](../../contributors) 153 | 154 | ## About SquareBoat 155 | 156 | [SquareBoat](https://squareboat.com) is a startup focused, product development company based in Gurgaon, India. You'll find an overview of all our open source projects [on GitHub](https://github.com/squareboat). 157 | 158 | # License 159 | 160 | The MIT License. Please see [License File](LICENSE.md) for more information. Copyright © 2020 [SquareBoat](https://squareboat.com) 161 | --------------------------------------------------------------------------------