├── .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 |
29 |
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 | 
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 |
--------------------------------------------------------------------------------