File Coverage

blib/lib/Mojolicious/Plugin/GetSentry.pm
Criterion Covered Total %
statement 15 85 17.6
branch 0 24 0.0
condition 0 5 0.0
subroutine 5 20 25.0
pod 13 13 100.0
total 33 147 22.4


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::GetSentry;
2 1     1   68868 use Mojo::Base 'Mojolicious::Plugin';
  1         198750  
  1         10  
3              
4             our $VERSION = '1.1.7';
5              
6 1     1   1755 use Data::Dump 'dump';
  1         4617  
  1         70  
7 1     1   508 use Devel::StackTrace::Extract;
  1         4251  
  1         49  
8 1     1   519 use Mojo::IOLoop;
  1         155700  
  1         8  
9 1     1   615 use Sentry::Raven;
  1         131319  
  1         1932  
10              
11             has [qw(
12             sentry_dsn timeout
13             )];
14              
15             has 'log_levels' => sub { ['error', 'fatal'] };
16             has 'processors' => sub { [] };
17              
18             has 'raven' => sub {
19             my $self = shift;
20              
21             foreach my $processor (@{ $self->processors }) {
22             eval "require $processor; $processor->import;";
23              
24             warn $@ if $@;
25             }
26              
27             return Sentry::Raven->new(
28             sentry_dsn => $self->sentry_dsn,
29             timeout => $self->timeout,
30             processors => $self->processors,
31             );
32             };
33              
34             has 'handlers' => sub {
35             my $self = shift;
36              
37             return {
38             capture_request => sub { $self->capture_request(@_) },
39             capture_message => sub { $self->capture_message(@_) },
40             stacktrace_context => sub { $self->stacktrace_context(@_) },
41             exception_context => sub { $self->exception_context(@_) },
42             user_context => sub { $self->user_context(@_) },
43             request_context => sub { $self->request_context(@_) },
44             tags_context => sub { $self->tags_context(@_) },
45             ignore => sub { $self->ignore(@_) },
46             on_error => sub { $self->on_error(@_) },
47             };
48             };
49              
50             has 'custom_handlers' => sub { {} };
51             has 'pending' => sub { {} };
52              
53             =head2 register
54              
55             =cut
56              
57             sub register {
58 0     0 1   my ($self, $app, $config) = (@_);
59            
60 0           my $handlers = {};
61              
62 0           foreach my $name (keys(%{ $self->handlers })) {
  0            
63 0           $handlers->{ $name } = delete($config->{ $name });
64             }
65              
66             # Set custom handlers
67 0           $self->custom_handlers($handlers);
68              
69 0   0       $config ||= {};
70 0           $self->{ $_ } = $config->{ $_ } for keys %$config;
71            
72 0           $self->hook_after_dispatch($app);
73 0           $self->hook_on_message($app);
74             }
75              
76             =head2 hook_after_dispatch
77              
78             =cut
79              
80             sub hook_after_dispatch {
81 0     0 1   my $self = shift;
82 0           my $app = shift;
83              
84             $app->hook(after_dispatch => sub {
85 0     0     my $controller = shift;
86              
87 0 0         if (my $exception = $controller->stash('exception')) {
88             # Mark this exception as handled. We don't delete it from $pending
89             # because if the same exception is logged several times within a
90             # 2-second period, we want the logger to ignore it.
91 0 0         $self->pending->{ $exception } = 0 if defined $self->pending->{ $exception };
92            
93             # Check if the exception should be ignored
94 0 0         if (!$self->handle('ignore', $exception)) {
95 0           $self->handle('capture_request', $exception, $controller);
96             }
97             }
98 0           });
99             }
100              
101             =head2 hook_on_message
102              
103             =cut
104              
105             sub hook_on_message {
106 0     0 1   my $self = shift;
107 0           my $app = shift;
108              
109             $app->log->on(message => sub {
110 0     0     my ($log, $level, $exception) = @_;
111              
112 0 0         if( grep { $level eq $_ } @{ $self->log_levels } ) {
  0            
  0            
113 0 0         $exception = Mojo::Exception->new($exception) unless ref $exception;
114              
115             # This exception is already pending
116 0 0         return if defined $self->pending->{ $exception };
117            
118 0           $self->pending->{ $exception } = 1;
119              
120             # Check if the exception should be ignored
121 0 0         if (!$self->handle('ignore', $exception)) {
122             # Wait 2 seconds before we handle it; if the exception happened in
123             # a request we want the after_dispatch-hook to handle it instead.
124             Mojo::IOLoop->timer(2 => sub {
125 0           $self->handle('capture_message', $exception);
126 0           });
127             }
128             }
129 0           });
130             }
131              
132             =head2 handle
133              
134             =cut
135              
136             sub handle {
137 0     0 1   my ($self, $method) = (shift, shift);
138              
139             return $self->custom_handlers->{ $method }->($self, @_)
140 0 0         if (defined($self->custom_handlers->{ $method }));
141            
142 0           return $self->handlers->{ $method }->(@_);
143             }
144              
145             =head2 capture_request
146              
147             =cut
148              
149             sub capture_request {
150 0     0 1   my ($self, $exception, $controller) = @_;
151              
152 0           $self->handle('stacktrace_context', $exception);
153 0           $self->handle('exception_context', $exception);
154 0           $self->handle('user_context', $controller);
155 0           $self->handle('tags_context', $controller);
156            
157 0           my $request_context = $self->handle('request_context', $controller);
158              
159 0           my $event_id = $self->raven->capture_request($controller->url_for->to_abs, %$request_context, $self->raven->get_context);
160              
161 0 0         if (!defined($event_id)) {
162 0           $self->handle('on_error', $exception->message, $self->raven->get_context);
163             }
164              
165 0           return $event_id;
166             }
167              
168             =head2 capture_message
169              
170             =cut
171              
172             sub capture_message {
173 0     0 1   my ($self, $exception) = @_;
174              
175 0           $self->handle('exception_context', $exception);
176              
177 0           my $event_id = $self->raven->capture_message($exception->message, $self->raven->get_context);
178              
179 0 0         if (!defined($event_id)) {
180 0           $self->handle('on_error', $exception->message, $self->raven->get_context);
181             }
182              
183 0           return $event_id;
184             }
185              
186             =head2 stacktrace_context
187              
188             $app->sentry->stacktrace_context($exception)
189              
190             Build the stacktrace context from current exception.
191             See also L<Sentry::Raven->stacktrace_context|https://metacpan.org/pod/Sentry::Raven#Sentry::Raven-%3Estacktrace_context(-$frames-)>
192              
193             =cut
194              
195             sub stacktrace_context {
196 0     0 1   my ($self, $exception) = @_;
197              
198 0           my $stacktrace = Devel::StackTrace::Extract::extract_stack_trace($exception);
199              
200 0           $self->raven->add_context(
201             $self->raven->stacktrace_context($self->raven->_get_frames_from_devel_stacktrace($stacktrace))
202             );
203             }
204              
205             =head2 exception_context
206              
207             $app->sentry->exception_context($exception)
208              
209             Build the exception context from current exception.
210             See also L<Sentry::Raven->exception_context|https://metacpan.org/pod/Sentry::Raven#Sentry::Raven-%3Eexception_context(-$value,-%25exception_context-)>
211              
212             =cut
213              
214             sub exception_context {
215 0     0 1   my ($self, $exception) = @_;
216              
217 0           $self->raven->add_context(
218             $self->raven->exception_context($exception->message, type => ref($exception))
219             );
220             }
221              
222             =head2 user_context
223              
224             $app->sentry->user_context($controller)
225              
226             Build the user context from current controller.
227             See also L<Sentry::Raven->user_context|https://metacpan.org/pod/Sentry::Raven#Sentry::Raven-%3Euser_context(-%25user_context-)>
228              
229             =cut
230              
231             sub user_context {
232 0     0 1   my ($self, $controller) = @_;
233              
234 0 0         if (defined($controller->user)) {
235 0   0       $self->raven->add_context(
236             $self->raven->user_context(
237             id => $controller->user->id,
238             ip_address => $controller->tx && $controller->tx->remote_address,
239             )
240             );
241             }
242             }
243              
244             =head2 request_context
245              
246             $app->sentry->request_context($controller)
247              
248             Build the request context from current controller.
249             See also L<Sentry::Raven->request_context|https://metacpan.org/pod/Sentry::Raven#Sentry::Raven-%3Erequest_context(-$url,-%25request_context-)>
250              
251             =cut
252              
253             sub request_context {
254 0     0 1   my ($self, $controller) = @_;
255              
256 0 0         if (defined($controller->req)) {
257 0           my $request_context = {
258             method => $controller->req->method,
259             headers => $controller->req->headers->to_hash,
260             };
261              
262 0           $self->raven->add_context(
263             $self->raven->request_context($controller->url_for->to_abs, %$request_context)
264             );
265              
266 0           return $request_context;
267             }
268              
269 0           return {};
270             }
271              
272             =head2 tags_context
273            
274             $app->sentry->tags_context($controller)
275              
276             Add some tags to the context.
277             See also L<Sentry::Raven->3Emerge_tags|https://metacpan.org/pod/Sentry::Raven#$raven-%3Emerge_tags(-%25tags-)>
278              
279             =cut
280              
281             sub tags_context {
282 0     0 1   my ($self, $c) = @_;
283              
284 0           $self->raven->merge_tags(
285             getsentry => $VERSION,
286             );
287             }
288              
289             =head2 ignore
290            
291             $app->sentry->ignore($exception)
292              
293             Check if the exception should be ignored.
294              
295             =cut
296              
297             sub ignore {
298 0     0 1   my ($self, $exception) = @_;
299              
300 0           return 0;
301             }
302              
303             =head2 on_error
304            
305             $app->sentry->on_error($message, %context)
306              
307             Handle reporting to Sentry error.
308              
309             =cut
310              
311             sub on_error {
312 0     0 1   my ($self, $message) = (shift, shift);
313              
314 0           die "failed to submit event to sentry service:\n" . dump($self->raven->_construct_message_event($message, @_));
315             }
316              
317             1;
318              
319             __END__
320              
321             =pod
322              
323             =head1 NAME
324              
325             Mojolicious::Plugin::GetSentry - Sentry client for Mojolicious
326              
327             =head1 VERSION
328              
329             version 1.0
330              
331             =head1 SYNOPSIS
332            
333             # Mojolicious with config
334             #
335             $self->plugin('sentry' => {
336             # Required field
337             sentry_dsn => 'DSN',
338              
339             # Not required
340             log_levels => ['error', 'fatal'],
341             timeout => 3,
342             logger => 'root',
343             platform => 'perl',
344              
345             # And if you want to use custom handles
346             # this is how you do it
347             stacktrace_context => sub {
348             my ($sentry, $exception) = @_;
349              
350             my $stacktrace = Devel::StackTrace::Extract::extract_stack_trace($exception);
351              
352             $sentry->raven->add_context(
353             $sentry->raven->stacktrace_context($sentry->raven->_get_frames_from_devel_stacktrace($stacktrace))
354             );
355             },
356              
357             exception_context => sub {
358             my ($sentry, $exception) = @_;
359              
360             $sentry->raven->add_context(
361             $sentry->raven->exception_context($exception->message, type => ref($exception))
362             );
363             },
364              
365             user_context => {
366             my ($sentry, $controller) = @_;
367              
368             $sentry->raven->add_context(
369             $sentry->raven->user_context(
370             id => 1,
371             ip_address => '10.10.10.1',
372             )
373             );
374             },
375              
376             request_context => sub {
377             my ($sentry, $controller) = @_;
378              
379             if (defined($controller->req)) {
380             my $request_context = {
381             method => $controller->req->method,
382             headers => $controller->req->headers->to_hash,
383             };
384              
385             $sentry->raven->add_context(
386             $sentry->raven->request_context($controller->url_for->to_abs, %$request_context)
387             );
388              
389             return $request_context;
390             }
391              
392             return {};
393             },
394              
395             tags_context => sub {
396             my ($sentry, $controller) = @_;
397              
398             $sentry->raven->merge_tags(
399             account => $controller->current_user->account_id,
400             );
401             },
402              
403             ignore => sub {
404             my ($sentry, $exception) = @_;
405              
406             return 1 if ($expection->message =~ /Do not log this error/);
407             },
408              
409             on_error => sub {
410             my ($self, $message) = (shift, shift);
411              
412             die "failed to submit event to sentry service:\n" . dump($sentry->raven->_construct_message_event($message, @_));
413             }
414             });
415              
416             # Mojolicious::Lite
417             #
418             plugin 'sentry' => {
419             # Required field
420             sentry_dsn => 'DSN',
421              
422             # Not required
423             log_levels => ['error', 'fatal'],
424             timeout => 3,
425             logger => 'root',
426             platform => 'perl',
427              
428             # And if you want to use custom handles
429             # this is how you do it
430             stacktrace_context => sub {
431             my ($sentry, $exception) = @_;
432              
433             my $stacktrace = Devel::StackTrace::Extract::extract_stack_trace($exception);
434              
435             $sentry->raven->add_context(
436             $sentry->raven->stacktrace_context($sentry->raven->_get_frames_from_devel_stacktrace($stacktrace))
437             );
438             },
439              
440             exception_context => sub {
441             my ($sentry, $exception) = @_;
442              
443             $sentry->raven->add_context(
444             $sentry->raven->exception_context($exception->message, type => ref($exception))
445             );
446             },
447              
448             user_context => {
449             my ($sentry, $controller) = @_;
450              
451             $sentry->raven->add_context(
452             $sentry->raven->user_context(
453             id => 1,
454             ip_address => '10.10.10.1',
455             )
456             );
457             },
458              
459             request_context => sub {
460             my ($sentry, $controller) = @_;
461              
462             if (defined($controller->req)) {
463             my $request_context = {
464             method => $controller->req->method,
465             headers => $controller->req->headers->to_hash,
466             };
467              
468             $sentry->raven->add_context(
469             $sentry->raven->request_context($controller->url_for->to_abs, %$request_context)
470             );
471              
472             return $request_context;
473             }
474              
475             return {};
476             },
477              
478             tags_context => sub {
479             my ($sentry, $controller) = @_;
480              
481             $sentry->raven->merge_tags(
482             account => $controller->current_user->account_id,
483             );
484             },
485              
486             ignore => sub {
487             my ($sentry, $exception) = @_;
488              
489             return 1 if ($expection->message =~ /Do not log this error/);
490             },
491              
492             on_error {
493             my ($sentry, $method) = (shift, shift);
494              
495             die "failed to submit event to sentry service:\n" . dump($sentry->raven->_construct_message_event($message, @_));
496             }
497             };
498              
499             =head1 DESCRIPTION
500              
501             Mojolicious::Plugin::GetSentry is a plugin for the Mojolicious web framework which allow you use Sentry L<https://getsentry.com>.
502             See also L<Sentry::Raven|https://metacpan.org/pod/Sentry::Raven>
503              
504             =head1 ATTRIBUTES
505              
506             L<Mojolicious::Plugin::GetSentry> implements the following attributes.
507              
508             =head2 sentry_dsn
509              
510             Sentry DSN url
511              
512             =head2 timeout
513              
514             Timeout specified in seconds
515              
516             =head2 log_levels
517              
518             Which log levels needs to be sent to Sentry
519             e.g.: ['error', 'fatal']
520              
521             =head2 processors
522              
523             A list of processors to filter down Sentry event
524             See also L<Sentry::Raven->processors|https://metacpan.org/pod/Sentry::Raven#$raven-%3Eadd_processors(-%5B-Sentry::Raven::Processor::RemoveStackVariables,-...-%5D-)>
525              
526             =head2 raven
527              
528             Sentry::Raven instance
529              
530             See also L<Sentry::Raven|https://metacpan.org/pod/Sentry::Raven>
531              
532             =head1 METHODS
533              
534             L<Mojolicious::Plugin::GetSentry> inherits all methods from L<Mojolicious::Plugin> and implements the
535             following new ones.
536              
537             =head1 SOURCE REPOSITORY
538              
539             L<https://github.com/crlcu/Mojolicious-Plugin-GetSentry>
540              
541             =head1 AUTHOR
542              
543             Adrian Crisan, E<lt>adrian.crisan88@gmail.comE<gt>
544              
545             =head1 BUGS
546              
547             Please report any bugs or feature requests to C<bug-mojolicious-plugin-getsentry at rt.cpan.org>, or through
548             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Mojolicious-Plugin-GetSentry>. I will be notified, and then you'll
549             automatically be notified of progress on your bug as I make changes.
550              
551              
552              
553              
554             =head1 SUPPORT
555              
556             You can find documentation for this module with the perldoc command.
557              
558             perldoc Mojolicious::Plugin::GetSentry
559              
560              
561             You can also look for information at:
562              
563             =over 4
564              
565             =item * RT: CPAN's request tracker (report bugs here)
566              
567             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Mojolicious-Plugin-GetSentry>
568              
569             =item * AnnoCPAN: Annotated CPAN documentation
570              
571             L<http://annocpan.org/dist/Mojolicious-Plugin-GetSentry>
572              
573             =item * CPAN Ratings
574              
575             L<http://cpanratings.perl.org/d/Mojolicious-Plugin-GetSentry>
576              
577             =item * Search CPAN
578              
579             L<http://search.cpan.org/dist/Mojolicious-Plugin-GetSentry/>
580              
581             =back
582              
583              
584             =head1 ACKNOWLEDGEMENTS
585              
586              
587             =head1 LICENSE AND COPYRIGHT
588              
589             Copyright 2018 Adrian Crisan.
590              
591             This program is free software; you can redistribute it and/or modify it
592             under the terms of the the Artistic License (2.0). You may obtain a
593             copy of the full license at:
594              
595             L<http://www.perlfoundation.org/artistic_license_2_0>
596              
597             Any use, modification, and distribution of the Standard or Modified
598             Versions is governed by this Artistic License. By using, modifying or
599             distributing the Package, you accept this license. Do not use, modify,
600             or distribute the Package, if you do not accept this license.
601              
602             If your Modified Version has been derived from a Modified Version made
603             by someone other than you, you are nevertheless required to ensure that
604             your Modified Version complies with the requirements of this license.
605              
606             This license does not grant you the right to use any trademark, service
607             mark, tradename, or logo of the Copyright Holder.
608              
609             This license includes the non-exclusive, worldwide, free-of-charge
610             patent license to make, have made, use, offer to sell, sell, import and
611             otherwise transfer the Package with respect to any patent claims
612             licensable by the Copyright Holder that are necessarily infringed by the
613             Package. If you institute patent litigation (including a cross-claim or
614             counterclaim) against any party alleging that the Package constitutes
615             direct or contributory patent infringement, then this Artistic License
616             to you shall terminate on the date that such litigation is filed.
617              
618             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
619             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
620             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
621             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
622             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
623             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
624             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
625             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
626              
627              
628             =cut