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