├── .gitignore ├── .travis.yml ├── Changes ├── INSTALL ├── LICENSE ├── MANIFEST ├── Makefile.PL ├── README ├── lib └── Sentry │ ├── Raven.pm │ └── Raven │ ├── Processor.pod │ └── Processor │ └── RemoveStackVariables.pm └── t ├── 00-load.t ├── 10-dsn.t ├── 11-generic-event.t ├── 12-specialized-event.t ├── 13-service-call.t ├── 14-does-not-die.t ├── 15-error-handler.t ├── 16-processors.t ├── 21-stacktrace-failures.t ├── 90-perl-critic.t ├── 91-pod-coverage.t ├── 92-pod-syntax.t ├── 93-spelling.t ├── 94-changes.t └── 99-todo.t /.gitignore: -------------------------------------------------------------------------------- 1 | /MANIFEST.bak 2 | /META.json 3 | /META.yml 4 | /MYMETA.* 5 | /Makefile 6 | /Makefile.old 7 | /blib/ 8 | /pm_to_blib 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: perl 2 | perl: 3 | - "5.30" 4 | - "5.16" 5 | - "5.14" 6 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | 1.14 2020-10-11 2 | - Do not fail when attempting to read unreadable files (thanks Gianni 3 | Ceccarelli) 4 | 5 | 1.13 2020-06-08 6 | - Fixing precidence of sentry dsn parameters (thanks Nowaker) 7 | - Displays source code in sentry event (thanks dariuszpiwowarski) 8 | 9 | 1.12 2020-02-02 10 | - Sending messages fall back to interface contexts (thanks jrubinator) 11 | 12 | 1.11 2019-06-29 13 | - Updating documentation 14 | 15 | 1.10 2017-06-12 16 | - Improving user context (thanks Steve Wooster) 17 | 18 | 1.09 2017-02-19 19 | - Increase MAX_EXCEPTION_VALUE to 4096 (thanks Michael Wiencek) 20 | - Using Time::Piece (thanks Ryutaro Mizokami) 21 | 22 | 1.08 2016-08-05 23 | - Fixing perl 5.8 compatibility 24 | 25 | 1.07 2016-08-04 26 | - Adding environment context parameter (thanks William Travis Holton) 27 | 28 | 1.06 2016-07-06 29 | - Adding release context parameter (thanks Slobodan Mišković) 30 | 31 | 1.05 2016-05-25 32 | - Fixing stack frame context (thanks Steve Wooster) 33 | 34 | 1.04 2016-05-10 35 | - Adding event fingerprinting (thanks Steve Wooster) 36 | 37 | 1.03 2016-04-15 38 | - Regain ability to set encoding (thanks Stuart Skelton) 39 | 40 | 1.02 2016-01-12 41 | - Move project to getsentry github repo 42 | - Change parameter reporting (thanks dakkar) 43 | 44 | 1.01 2015-08-20 45 | - Clean up encoding error in Sentry 46 | 47 | 1.00 2015-07-13 48 | - Major release 49 | - using stacktrace abs_path and filename correctly 50 | 51 | 0.09 2015-01-20 52 | - Reuse TCP connections when possible 53 | - Use LWP's built-in gzip compression 54 | 55 | 0.08 2014-09-22 56 | - Trimming fields to avoid 413 Request Entity Too Large response 57 | - Fixing Http interface documentation for data field 58 | 59 | 0.07 2014-08-11 60 | - Use IO::Compress::Gzip to compress http posts 61 | 62 | 0.06 2014-07-14 63 | - Report post errors in capture_errors 64 | - Added travis config 65 | 66 | 0.05 2014-06-12 67 | - Windows test fixes 68 | 69 | 0.04 2014-06-11 70 | - Event processor support 71 | - Stacktrace context now accepts Devel::StackTrace objects 72 | - Improving error handler behavior and documentation 73 | - Better context usability for extra and tags 74 | 75 | 0.03 2014-05-29 76 | - Context decorators 77 | - Post users (sentry interface type) 78 | - Post queries (sentry interface type) 79 | - Modified api for capture_exception 80 | 81 | 0.02 2014-05-23 82 | - Override event defaults in the constructor 83 | - Automatically post stack traces with the error handler 84 | - Post stack traces (sentry interface type) 85 | - Post http requests (sentry interface type) 86 | - Package improvements 87 | 88 | 0.01 2014-05-07 89 | - Initial release 90 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/perl-raven/2ed38633696f04ffd471e5b0809f3fed9c74cc4b/INSTALL -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Matt Harrington 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. 22 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | Changes 3 | INSTALL 4 | LICENSE 5 | MANIFEST 6 | Makefile.PL 7 | README 8 | lib/Sentry/Raven.pm 9 | lib/Sentry/Raven/Processor.pod 10 | lib/Sentry/Raven/Processor/RemoveStackVariables.pm 11 | t/00-load.t 12 | t/10-dsn.t 13 | t/11-generic-event.t 14 | t/12-specialized-event.t 15 | t/13-service-call.t 16 | t/14-does-not-die.t 17 | t/15-error-handler.t 18 | t/16-processors.t 19 | t/21-stacktrace-failures.t 20 | t/90-perl-critic.t 21 | t/91-pod-coverage.t 22 | t/92-pod-syntax.t 23 | t/93-spelling.t 24 | t/94-changes.t 25 | t/99-todo.t 26 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | use ExtUtils::MakeMaker; 2 | 3 | WriteMakefile1( 4 | NAME => 'Sentry::Raven', 5 | ABSTRACT_FROM => 'lib/Sentry/Raven.pm', 6 | VERSION_FROM => 'lib/Sentry/Raven.pm', 7 | MIN_PERL_VERSION => '5.008', 8 | AUTHOR => 'Matt Harrington', 9 | LICENSE => 'mit', 10 | 11 | PREREQ_PM => { 12 | 'Data::Dump' => 0, 13 | 'Devel::StackTrace' => 0, 14 | 'English' => 0, 15 | 'File::Basename' => 0, 16 | 'File::Slurp' => 0, 17 | 'HTTP::Request::Common' => 0, 18 | 'HTTP::Status' => 0, 19 | 'JSON::XS' => 0, 20 | 'List::Util' => 0, 21 | 'LWP::Protocol::https' => 0, 22 | 'LWP::UserAgent' => 0, 23 | 'Moo' => 0, 24 | 'MooX::Types::MooseLike::Base' => 0, 25 | 'Sys::Hostname' => 0, 26 | 'Time::Piece' => 0, 27 | 'URI' => 0, 28 | 'UUID::Tiny' => 0, 29 | }, 30 | 31 | TEST_REQUIRES => { 32 | 'File::Spec' => 0, 33 | 'HTTP::Response' => 0, 34 | 'IO::Uncompress::Gunzip' => 0, 35 | 'Test::CPAN::Changes::ReallyStrict' => 0, 36 | 'Test::LWP::UserAgent' => 0, 37 | 'Test::More' => '0.88', 38 | 'Test::Perl::Critic' => '1.03', 39 | 'Test::Warn' => '0.30', 40 | }, 41 | 42 | META_MERGE => { 43 | 'meta-spec' => { 44 | version => '2', 45 | url => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec', 46 | }, 47 | 48 | homepage => 'https://github.com/getsentry/perl-raven', 49 | resources => { 50 | repository => { 51 | type => 'git', 52 | url => 'git@github.com:getsentry/perl-raven.git', 53 | web => 'https://github.com/getsentry/perl-raven', 54 | }, 55 | }, 56 | }, 57 | ); 58 | 59 | 60 | sub WriteMakefile1 { # Cribbed from eumm-upgrade by Alexandr Ciornii 61 | my %params = @_; 62 | my $eumm_version = $ExtUtils::MakeMaker::VERSION; 63 | $eumm_version = eval $eumm_version; 64 | 65 | if ($params{TEST_REQUIRES} and $eumm_version < 6.6303) { 66 | $params{BUILD_REQUIRES}={ %{$params{BUILD_REQUIRES} || {}} , %{$params{TEST_REQUIRES}} }; 67 | delete $params{TEST_REQUIRES}; 68 | } 69 | if ($params{BUILD_REQUIRES} and $eumm_version < 6.5503) { 70 | #EUMM 6.5502 has problems with BUILD_REQUIRES 71 | $params{PREREQ_PM}={ %{$params{PREREQ_PM} || {}} , %{$params{BUILD_REQUIRES}} }; 72 | delete $params{BUILD_REQUIRES}; 73 | } 74 | delete $params{CONFIGURE_REQUIRES} if $eumm_version < 6.52; 75 | delete $params{MIN_PERL_VERSION} if $eumm_version < 6.48; 76 | delete $params{META_MERGE} if $eumm_version < 6.46; 77 | delete $params{META_ADD} if $eumm_version < 6.46; 78 | delete $params{LICENSE} if $eumm_version < 6.31; 79 | delete $params{AUTHOR} if $] < 5.005; 80 | delete $params{ABSTRACT_FROM} if $] < 5.005; 81 | delete $params{BINARY_LOCATION} if $] < 5.005; 82 | 83 | WriteMakefile(%params); 84 | } 85 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Sentry::Raven version 1.14 2 | 3 | Sentry::Raven is a module for sending events to a sentry service. 4 | 5 | SYNOPSIS 6 | 7 | use Sentry::Raven; 8 | my $raven = Sentry::Raven->new(sentry_dsn => 'https://:@sentry.io/' ); 9 | $raven->capture_message('The sky is falling'); 10 | 11 | INSTALLATION 12 | 13 | To install this module type the following: 14 | 15 | perl Makefile.PL 16 | make 17 | make test 18 | make install 19 | 20 | DEPENDENCIES 21 | 22 | Perl 5.10 or later. 23 | 24 | COPYRIGHT AND LICENSE 25 | 26 | Copyright (C) 2020 by Matt Harrington 27 | 28 | The full text of this license can be found in the LICENSE file included with this module. 29 | -------------------------------------------------------------------------------- /lib/Sentry/Raven.pm: -------------------------------------------------------------------------------- 1 | package Sentry::Raven; 2 | 3 | use 5.010; 4 | use strict; 5 | use warnings; 6 | use Moo; 7 | use MooX::Types::MooseLike::Base qw/ ArrayRef HashRef Int Str /; 8 | 9 | our $VERSION = '1.14'; 10 | 11 | use Data::Dump 'dump'; 12 | use Devel::StackTrace; 13 | use English '-no_match_vars'; 14 | use File::Basename 'basename'; 15 | use HTTP::Request::Common 'POST'; 16 | use HTTP::Status ':constants'; 17 | use JSON::XS; 18 | use LWP::UserAgent; 19 | use Sys::Hostname; 20 | use Time::Piece; 21 | use URI; 22 | use UUID::Tiny ':std'; 23 | use File::Slurp; 24 | use List::Util qw(min max); 25 | 26 | # constants from server-side sentry code 27 | use constant { 28 | MAX_CULPRIT => 200, 29 | MAX_MESSAGE => 2048, 30 | 31 | MAX_EXCEPTION_TYPE => 128, 32 | MAX_EXCEPTION_VALUE => 4096, 33 | 34 | MAX_HTTP_QUERY_STRING => 1024, 35 | MAX_HTTP_DATA => 2048, 36 | 37 | MAX_QUERY_ENGINE => 128, 38 | MAX_QUERY_QUERY => 1024, 39 | 40 | MAX_STACKTRACE_FILENAME => 256, 41 | MAX_STACKTRACE_PACKAGE => 256, 42 | MAX_STACKTRACE_SUBROUTUNE => 256, 43 | 44 | MAX_USER_EMAIL => 128, 45 | MAX_USER_ID => 128, 46 | MAX_USER_USERNAME => 128, 47 | MAX_USER_IP_ADDRESS => 45, # RFC 4291, section 2.2.3 48 | }; 49 | 50 | # self-imposed constants 51 | use constant { 52 | MAX_HTTP_COOKIES => 1024, 53 | MAX_HTTP_URL => 1024, 54 | 55 | MAX_STACKTRACE_VARS => 1024, 56 | }; 57 | 58 | =head1 NAME 59 | 60 | Sentry::Raven - A perl sentry client 61 | 62 | =head1 VERSION 63 | 64 | Version 1.14 65 | 66 | =head1 SYNOPSIS 67 | 68 | my $raven = Sentry::Raven->new( sentry_dsn => 'https://:@sentry.io/' ); 69 | 70 | # capture all errors 71 | $raven->capture_errors( sub { 72 | ..do something here.. 73 | } ); 74 | 75 | # capture an individual event 76 | $raven->capture_message('The sky is falling'); 77 | 78 | # annotate an event with context 79 | $raven->capture_message( 80 | 'The sky is falling', 81 | Sentry::Raven->exception_context('SkyException', 'falling'), 82 | ); 83 | 84 | =head1 DESCRIPTION 85 | 86 | This module implements the recommended raven interface for posting events to a sentry service. 87 | 88 | =head1 CONSTRUCTOR 89 | 90 | =head2 my $raven = Sentry::Raven->new( %options, %context ) 91 | 92 | Create a new sentry interface object. It accepts the following named options: 93 | 94 | =over 95 | 96 | =item C<< sentry_dsn => 'http://:@sentry.io/' >> 97 | 98 | The DSN for your sentry service. Get this from the client configuration page for your project. 99 | 100 | =item C<< timeout => 5 >> 101 | 102 | Do not wait longer than this number of seconds when attempting to send an event. 103 | 104 | =back 105 | 106 | =cut 107 | 108 | has [qw/ post_url public_key secret_key /] => ( 109 | is => 'ro', 110 | isa => Str, 111 | required => 1, 112 | ); 113 | 114 | has sentry_version => ( 115 | is => 'ro', 116 | isa => Int, 117 | default => 7, 118 | ); 119 | 120 | has timeout => ( 121 | is => 'ro', 122 | isa => Int, 123 | default => 5, 124 | ); 125 | 126 | has json_obj => ( 127 | is => 'ro', 128 | builder => '_build_json_obj', 129 | lazy => 1, 130 | ); 131 | 132 | has ua_obj => ( 133 | is => 'ro', 134 | builder => '_build_ua_obj', 135 | lazy => 1, 136 | ); 137 | 138 | has valid_levels => ( 139 | is => 'ro', 140 | isa => ArrayRef[Str], 141 | default => sub { [qw/ fatal error warning info debug /] }, 142 | ); 143 | 144 | has valid_interfaces => ( 145 | is => 'ro', 146 | isa => ArrayRef[Str], 147 | default => sub { [qw/ 148 | sentry.interfaces.Exception sentry.interfaces.Http 149 | sentry.interfaces.Stacktrace sentry.interfaces.User 150 | sentry.interfaces.Query 151 | /] }, 152 | ); 153 | 154 | has context => ( 155 | is => 'rw', 156 | isa => HashRef[], 157 | default => sub { { } }, 158 | ); 159 | 160 | has processors => ( 161 | is => 'rw', 162 | isa => ArrayRef[], 163 | default => sub { [] }, 164 | ); 165 | 166 | has encoding => ( 167 | is => 'rw', 168 | isa => Str, 169 | default => 'gzip', 170 | ); 171 | 172 | around BUILDARGS => sub { 173 | my ($orig, $class, %args) = @_; 174 | 175 | my $sentry_dsn = $args{sentry_dsn} || $ENV{SENTRY_DSN} 176 | or die "must pass sentry_dsn or set SENTRY_DSN envirionment variable\n"; 177 | 178 | delete($args{sentry_dsn}); 179 | 180 | my $uri = URI->new($sentry_dsn); 181 | 182 | die "unable to parse sentry dsn: $sentry_dsn\n" 183 | unless defined($uri) && $uri->can('userinfo'); 184 | 185 | die "unable to parse public and secret keys from: $sentry_dsn\n" 186 | unless defined($uri->userinfo()) && $uri->userinfo() =~ m/:/; 187 | 188 | my @path = split(m{/}, $uri->path()); 189 | my ($public_key, $secret_key) = $uri->userinfo() =~ m/(.*):(.*)/; 190 | my $project_id = pop(@path); 191 | 192 | my $post_url = 193 | $uri->scheme().'://'.$uri->host().':'.$uri->port() . 194 | join('/', @path).'/api/'.$project_id.'/store/' 195 | ; 196 | 197 | my $timeout = delete($args{timeout}); 198 | my $ua_obj = delete($args{ua_obj}); 199 | my $processors = delete($args{processors}) || []; 200 | my $encoding = delete($args{encoding}); 201 | 202 | return $class->$orig( 203 | post_url => $post_url, 204 | public_key => $public_key, 205 | secret_key => $secret_key, 206 | context => \%args, 207 | processors => $processors, 208 | 209 | (defined($encoding) ? (encoding => $encoding) : ()), 210 | (defined($timeout) ? (timeout => $timeout) : ()), 211 | (defined($ua_obj) ? (ua_obj => $ua_obj) : ()), 212 | ); 213 | }; 214 | 215 | sub _trim { 216 | my ($string, $length) = @_; 217 | return defined($string) 218 | ? substr($string, 0, $length) 219 | : undef; 220 | } 221 | 222 | =head1 ERROR HANDLERS 223 | 224 | These methods are designed to capture events and handle them automatically. 225 | 226 | =head2 $raven->capture_errors( $subref, %context ) 227 | 228 | Execute the $subref and report any exceptions (die) back to the sentry service. If it is unable to submit an event (capture_message return undef), it will die and include the event details in the die message. This automatically includes a stacktrace unless C<$SIG{__DIE__}> has been overridden in subsequent code. 229 | 230 | =cut 231 | 232 | sub capture_errors { 233 | my ($self, $subref, %context) = @_; 234 | 235 | my $wantarray = wantarray(); 236 | 237 | my ($stacktrace, @retval); 238 | eval { 239 | local $SIG{__DIE__} = sub { $stacktrace = Devel::StackTrace->new(skip_frames => 1) }; 240 | 241 | if ($wantarray) { 242 | @retval = $subref->(); 243 | } else { 244 | $retval[0] = $subref->(); 245 | } 246 | }; 247 | 248 | my $eval_error = $EVAL_ERROR; 249 | 250 | if ($eval_error) { 251 | my $message = $eval_error; 252 | chomp($message); 253 | 254 | my %stacktrace_context = $stacktrace 255 | ? $self->stacktrace_context( 256 | $self->_get_frames_from_devel_stacktrace($stacktrace), 257 | ) 258 | : (); 259 | 260 | %context = ( 261 | culprit => $PROGRAM_NAME, 262 | %context, 263 | $self->exception_context($message), 264 | %stacktrace_context, 265 | ); 266 | 267 | my $event_id = $self->capture_message($message, %context); 268 | 269 | if (!defined($event_id)) { 270 | die "failed to submit event to sentry service:\n" . dump($self->_construct_message_event($message, %context)); 271 | } 272 | } 273 | 274 | return $wantarray ? @retval : $retval[0]; 275 | }; 276 | 277 | sub _get_frames_from_devel_stacktrace { 278 | my ($self, $stacktrace) = @_; 279 | 280 | my @frames = map { 281 | my $frame = $_; 282 | { 283 | abs_path => _trim($frame->filename(), MAX_STACKTRACE_FILENAME), 284 | filename => _trim(basename($frame->filename()), MAX_STACKTRACE_FILENAME), 285 | function => _trim($frame->subroutine(), MAX_STACKTRACE_SUBROUTUNE), 286 | lineno => $frame->line(), 287 | module => _trim($frame->package(), MAX_STACKTRACE_PACKAGE), 288 | vars => { 289 | '@_' => [ 290 | map { _trim(dump($_), MAX_STACKTRACE_VARS) } $frame->args(), 291 | ], 292 | }, 293 | $self->_get_lines_from_file($frame->filename(), $frame->line()), 294 | } 295 | } $stacktrace->frames(); 296 | 297 | # Devel::Stacktrace::Frame's subroutine() and args() describe what's being called by the current frame, 298 | # whereas Sentry expects function and vars to describe the current frame. 299 | for my $i (0..$#frames) { 300 | my $frame = $frames[$i]; 301 | my $parent = defined($frames[$i + 1]) 302 | ? $frames[$i + 1] 303 | : {}; 304 | @$frame{'function', 'vars'} = @$parent{'function', 'vars'}; 305 | } 306 | 307 | return [ reverse(@frames) ]; 308 | } 309 | 310 | sub _get_lines_from_file { 311 | my ($self, $abs_path, $lineno) = @_; 312 | 313 | my @lines = eval { read_file($abs_path) }; 314 | chomp(@lines); 315 | return () unless @lines; 316 | return () unless @lines >= $lineno; 317 | 318 | my $context_lines = 5; 319 | my $lower_bound = max(0, $lineno - $context_lines); 320 | my $upper_bound = min($lineno + $context_lines, scalar @lines); 321 | 322 | return ( 323 | context_line => $lines[ $lineno - 1 ], 324 | pre_context => [ @lines[ $lower_bound - 1 .. $lineno - 2 ]], 325 | post_context => [ @lines[ $lineno .. $upper_bound - 1 ]], 326 | ); 327 | } 328 | 329 | =head1 METHODS 330 | 331 | These methods are for generating individual events. 332 | 333 | =head2 $raven->capture_message( $message, %context ) 334 | 335 | Post a string message to the sentry service. Returns the event id. 336 | 337 | =cut 338 | 339 | sub capture_message { 340 | my ($self, $message, %context) = @_; 341 | return $self->_post_event($self->_construct_message_event($message, %context)); 342 | } 343 | 344 | sub _construct_message_event { 345 | my ($self, $message, %context) = @_; 346 | return $self->_construct_event(message => $message, %context); 347 | } 348 | 349 | =head2 $raven->capture_exception( $exception_value, %exception_context, %context ) 350 | 351 | Post an exception type and value to the sentry service. Returns the event id. 352 | 353 | C<%exception_context> can contain: 354 | 355 | =over 356 | 357 | =item C<< type => $type >> 358 | 359 | =back 360 | 361 | =cut 362 | 363 | sub capture_exception { 364 | my ($self, $value, %context) = @_; 365 | return $self->_post_event($self->_construct_exception_event($value, %context)); 366 | }; 367 | 368 | sub _construct_exception_event { 369 | my ($self, $value, %context) = @_; 370 | return $self->_construct_event( 371 | %context, 372 | $self->exception_context($value, %context), 373 | ); 374 | }; 375 | 376 | =head2 $raven->capture_request( $url, %request_context, %context ) 377 | 378 | Post a web url request to the sentry service. Returns the event id. 379 | 380 | C<%request_context> can contain: 381 | 382 | =over 383 | 384 | =item C<< method => 'GET' >> 385 | 386 | =item C<< data => 'foo=bar' >> 387 | 388 | =item C<< query_string => 'foo=bar' >> 389 | 390 | =item C<< cookies => 'foo=bar' >> 391 | 392 | =item C<< headers => { 'Content-Type' => 'text/html' } >> 393 | 394 | =item C<< env => { REMOTE_ADDR => '192.168.0.1' } >> 395 | 396 | =back 397 | 398 | =cut 399 | 400 | sub capture_request { 401 | my ($self, $url, %context) = @_; 402 | return $self->_post_event($self->_construct_request_event($url, %context)); 403 | }; 404 | 405 | sub _construct_request_event { 406 | my ($self, $url, %context) = @_; 407 | 408 | return $self->_construct_event( 409 | %context, 410 | $self->request_context($url, %context), 411 | ); 412 | }; 413 | 414 | =head2 $raven->capture_stacktrace( $frames, %context ) 415 | 416 | Post a stacktrace to the sentry service. Returns the event id. 417 | 418 | C<$frames> can be either a Devel::StackTrace object, or an arrayref of hashrefs with each hashref representing a single frame. 419 | 420 | my $frames = [ 421 | { 422 | filename => 'my/file1.pl', 423 | function => 'function1', 424 | vars => { foo => 'bar' }, 425 | lineno => 10, 426 | }, 427 | { 428 | filename => 'my/file2.pl', 429 | function => 'function2', 430 | vars => { bar => 'baz' }, 431 | lineno => 20, 432 | }, 433 | ]; 434 | 435 | The first frame should be the oldest frame. Frames must contain at least one of C, C, or C. These additional attributes are also supported: 436 | 437 | =over 438 | 439 | =item C<< filename => $file_name >> 440 | 441 | =item C<< function => $function_name >> 442 | 443 | =item C<< module => $module_name >> 444 | 445 | =item C<< lineno => $line_number >> 446 | 447 | =item C<< colno => $column_number >> 448 | 449 | =item C<< abs_path => $absolute_path_file_name >> 450 | 451 | =item C<< context_line => $line_of_code >> 452 | 453 | =item C<< pre_context => [ $previous_line1, $previous_line2 ] >> 454 | 455 | =item C<< post_context => [ $next_line1, $next_line2 ] >> 456 | 457 | =item C<< in_app => $one_if_not_external_library >> 458 | 459 | =item C<< vars => { $variable_name => $variable_value } >> 460 | 461 | =back 462 | 463 | =cut 464 | 465 | sub capture_stacktrace { 466 | my ($self, $frames, %context) = @_; 467 | return $self->_post_event($self->_construct_stacktrace_event($frames, %context)); 468 | }; 469 | 470 | sub _construct_stacktrace_event { 471 | my ($self, $frames, %context) = @_; 472 | 473 | return $self->_construct_event( 474 | %context, 475 | $self->stacktrace_context($frames), 476 | ); 477 | }; 478 | 479 | =head2 $raven->capture_user( %user_context, %context ) 480 | 481 | Post a user to the sentry service. Returns the event id. 482 | 483 | C<%user_context> can contain: 484 | 485 | =over 486 | 487 | =item C<< id => $unique_id >> 488 | 489 | =item C<< username => $username >> 490 | 491 | =item C<< email => $email >> 492 | 493 | =item C<< ip_address => $ip_address >> 494 | 495 | =back 496 | 497 | =cut 498 | 499 | sub capture_user { 500 | my ($self, %context) = @_; 501 | return $self->_post_event($self->_construct_user_event(%context)); 502 | }; 503 | 504 | sub _construct_user_event { 505 | my ($self, %context) = @_; 506 | 507 | return $self->_construct_event( 508 | %context, 509 | $self->user_context( 510 | map { $_ => $context{$_} } qw/email id username ip_address/ 511 | ), 512 | ); 513 | }; 514 | 515 | =head2 $raven->capture_query( $query, %query_context, %context ) 516 | 517 | Post a query to the sentry service. Returns the event id. 518 | 519 | C<%query_context> can contain: 520 | 521 | =over 522 | 523 | =item C<< engine => $engine' >> 524 | 525 | =back 526 | 527 | =cut 528 | 529 | sub capture_query { 530 | my ($self, $query, %context) = @_; 531 | return $self->_post_event($self->_construct_query_event($query, %context)); 532 | }; 533 | 534 | sub _construct_query_event { 535 | my ($self, $query, %context) = @_; 536 | 537 | return $self->_construct_event( 538 | %context, 539 | $self->query_context($query, %context), 540 | ); 541 | }; 542 | 543 | sub _post_event { 544 | my ($self, $event) = @_; 545 | 546 | $event = $self->_process_event($event); 547 | 548 | my ($response, $response_code, $response_content); 549 | 550 | eval { 551 | my $event_json = $self->json_obj()->encode( $event ); 552 | 553 | $self->ua_obj()->timeout($self->timeout()); 554 | 555 | my $request = POST( 556 | $self->post_url(), 557 | 'X-Sentry-Auth' => $self->_generate_auth_header(), 558 | Content => $event_json, 559 | ); 560 | $request->encode( $self->encoding() ); 561 | $response = $self->ua_obj()->request($request); 562 | 563 | $response_code = $response->code(); 564 | $response_content = $response->content(); 565 | }; 566 | 567 | warn "$EVAL_ERROR\n" if $EVAL_ERROR; 568 | 569 | if (defined($response_code) && $response_code == HTTP_OK) { 570 | return $self->json_obj()->decode($response_content)->{id}; 571 | } else { 572 | if ($response) { 573 | warn "Unsuccessful Response Posting Sentry Event:\n"._trim($response->as_string(), 1000)."\n"; 574 | } 575 | return; 576 | } 577 | } 578 | 579 | sub _process_event { 580 | my ($self, $event) = @_; 581 | 582 | foreach my $processor (@{$self->processors()}) { 583 | my $processed_event = $processor->process($event); 584 | if ($processed_event) { 585 | $event = $processed_event; 586 | } else { 587 | die "processor $processor did not return an event"; 588 | } 589 | } 590 | 591 | return $event; 592 | } 593 | 594 | sub _generate_id { 595 | (my $uuid = create_uuid_as_string(UUID_V4)) =~ s/-//g; 596 | return $uuid; 597 | } 598 | 599 | sub _construct_event { 600 | my ($self, %context) = @_; 601 | 602 | my $event = { 603 | event_id => $context{event_id} || $self->context()->{event_id} || _generate_id(), 604 | timestamp => $context{timestamp} || $self->context()->{timestamp} || gmtime->datetime(), 605 | logger => $context{logger} || $self->context()->{logger} || 'root', 606 | server_name => $context{server_name} || $self->context()->{server_name} || hostname(), 607 | platform => $context{platform} || $self->context()->{platform} || 'perl', 608 | 609 | release => $context{release} || $self->context()->{release}, 610 | 611 | message => $context{message} || $self->context()->{message}, 612 | culprit => $context{culprit} || $self->context()->{culprit}, 613 | 614 | extra => $self->_merge_hashrefs($self->context()->{extra}, $context{extra}), 615 | tags => $self->_merge_hashrefs($self->context()->{tags}, $context{tags}), 616 | fingerprint => $context{fingerprint} || $self->context()->{fingerprint} || ['{{ default }}'], 617 | 618 | level => $self->_validate_level($context{level}) || $self->context()->{level} || 'error', 619 | environment => $context{environment} || $self->context()->{environment} || $ENV{SENTRY_ENVIRONMENT}, 620 | }; 621 | 622 | $event->{message} = _trim($event->{message}, MAX_MESSAGE); 623 | $event->{culprit} = _trim($event->{culprit}, MAX_CULPRIT); 624 | 625 | my $instance_ctx = $self->context(); 626 | foreach my $interface (@{ $self->valid_interfaces() }) { 627 | my $interface_ctx = $context{$interface} || $instance_ctx->{$interface}; 628 | $event->{$interface} = $interface_ctx 629 | if $interface_ctx; 630 | } 631 | 632 | return $event; 633 | } 634 | 635 | sub _merge_hashrefs { 636 | my ($self, $hash1, $hash2) = @_; 637 | 638 | return { 639 | ($hash1 ? %{ $hash1 } : ()), 640 | ($hash2 ? %{ $hash2 } : ()), 641 | }; 642 | }; 643 | 644 | sub _validate_level { 645 | my ($self, $level) = @_; 646 | 647 | return unless defined($level); 648 | 649 | my %level_hash = map { $_ => 1 } @{ $self->valid_levels() }; 650 | 651 | if (exists($level_hash{$level})) { 652 | return $level; 653 | } else { 654 | warn "unknown level: $level\n"; 655 | return; 656 | } 657 | }; 658 | 659 | sub _generate_auth_header { 660 | my ($self) = @_; 661 | 662 | my %fields = ( 663 | sentry_version => $self->sentry_version(), 664 | sentry_client => "raven-perl/$VERSION", 665 | sentry_timestamp => time(), 666 | 667 | sentry_key => $self->public_key(), 668 | sentry_secret => $self->secret_key(), 669 | ); 670 | 671 | return 'Sentry ' . join(', ', map { $_ . '=' . $fields{$_} } sort keys %fields); 672 | } 673 | 674 | sub _build_json_obj { JSON::XS->new()->utf8(1)->pretty(1)->allow_nonref(1) } 675 | sub _build_ua_obj { 676 | return LWP::UserAgent->new( 677 | keep_alive => 1, 678 | ); 679 | } 680 | 681 | =head1 EVENT CONTEXT 682 | 683 | These methods are for annotating events with additional context, such as stack traces or HTTP requests. Simply pass their output to any other method accepting C<%context>. They accept all of the same arguments as their C counterparts. 684 | 685 | $raven->capture_message( 686 | 'The sky is falling', 687 | Sentry::Raven->exception_context('falling', type => 'SkyException'), 688 | ); 689 | 690 | =head2 Sentry::Raven->exception_context( $value, %exception_context ) 691 | 692 | =cut 693 | 694 | sub exception_context { 695 | my ($class, $value, %exception_context) = @_; 696 | 697 | return ( 698 | 'sentry.interfaces.Exception' => { 699 | value => _trim($value, MAX_EXCEPTION_VALUE), 700 | type => _trim($exception_context{type}, MAX_EXCEPTION_TYPE), 701 | } 702 | ); 703 | }; 704 | 705 | =head2 Sentry::Raven->request_context( $url, %request_context ) 706 | 707 | =cut 708 | 709 | sub request_context { 710 | my ($class, $url, %context) = @_; 711 | 712 | return ( 713 | 'sentry.interfaces.Http' => { 714 | url => _trim($url, MAX_HTTP_URL), 715 | method => $context{method}, 716 | data => _trim($context{data}, MAX_HTTP_DATA), 717 | query_string => _trim($context{query_string}, MAX_HTTP_QUERY_STRING), 718 | cookies => _trim($context{cookies}, MAX_HTTP_COOKIES), 719 | headers => $context{headers}, 720 | env => $context{env}, 721 | } 722 | ); 723 | }; 724 | 725 | =head2 Sentry::Raven->stacktrace_context( $frames ) 726 | 727 | =cut 728 | 729 | sub stacktrace_context { 730 | my ($class, $frames) = @_; 731 | 732 | eval { 733 | $frames = $class->_get_frames_from_devel_stacktrace($frames) 734 | if $frames->isa('Devel::StackTrace'); 735 | }; 736 | 737 | return ( 738 | 'sentry.interfaces.Stacktrace' => { 739 | frames => $frames, 740 | } 741 | ); 742 | }; 743 | 744 | =head2 Sentry::Raven->user_context( %user_context ) 745 | 746 | =cut 747 | 748 | sub user_context { 749 | my ($class, %user_context) = @_; 750 | my ($email, $id, $username, $ip_address) = delete @user_context{qw/email id username ip_address/}; 751 | 752 | return ( 753 | 'sentry.interfaces.User' => { 754 | email => _trim($email, MAX_USER_EMAIL), 755 | id => _trim($id, MAX_USER_ID), 756 | username => _trim($username, MAX_USER_USERNAME), 757 | ip_address => _trim($ip_address, MAX_USER_IP_ADDRESS), 758 | %user_context, 759 | } 760 | ); 761 | }; 762 | 763 | =head2 Sentry::Raven->query_context( $query, %query_context ) 764 | 765 | =cut 766 | 767 | sub query_context { 768 | my ($class, $query, %query_context) = @_; 769 | 770 | return ( 771 | 'sentry.interfaces.Query' => { 772 | query => _trim($query, MAX_QUERY_QUERY), 773 | engine => _trim($query_context{engine}, MAX_QUERY_ENGINE), 774 | } 775 | ); 776 | }; 777 | 778 | =pod 779 | 780 | The default context can be modified with the following accessors: 781 | 782 | =head2 my %context = $raven->get_context(); 783 | 784 | =cut 785 | 786 | sub get_context { 787 | my ($self) = @_; 788 | return %{ $self->context() }; 789 | }; 790 | 791 | =head2 $raven->add_context( %context ) 792 | 793 | =cut 794 | 795 | sub add_context { 796 | my ($self, %context) = @_; 797 | $self->context()->{$_} = $context{$_} 798 | for keys %context; 799 | }; 800 | 801 | =head2 $raven->merge_tags( %tags ) 802 | 803 | Merge additional tags into any existing tags in the current context. 804 | 805 | =cut 806 | 807 | sub merge_tags { 808 | my ($self, %tags) = @_; 809 | $self->context()->{tags} = $self->_merge_hashrefs($self->context()->{tags}, \%tags); 810 | }; 811 | 812 | =head2 $raven->merge_extra( %tags ) 813 | 814 | Merge additional extra into any existing extra in the current context. 815 | 816 | =cut 817 | 818 | sub merge_extra { 819 | my ($self, %extra) = @_; 820 | $self->context()->{extra} = $self->_merge_hashrefs($self->context()->{extra}, \%extra); 821 | }; 822 | 823 | =head2 $raven->clear_context() 824 | 825 | =cut 826 | 827 | sub clear_context { 828 | my ($self) = @_; 829 | $self->context({}); 830 | }; 831 | 832 | =head1 EVENT PROCESSORS 833 | 834 | Processors are a mechanism for modifying events after they are generated but before they are posted to the sentry service. They are useful for scrubbing sensitive data, such as passwords, as well as adding additional context. If the processor fails (dies or returns undef), the failure will be passed to the caller. 835 | 836 | See L for information on creating new processors. 837 | 838 | Available processors: 839 | 840 | =over 841 | 842 | =item L 843 | 844 | =back 845 | 846 | =head2 $raven->add_processors( [ Sentry::Raven::Processor::RemoveStackVariables, ... ] ) 847 | 848 | =cut 849 | 850 | sub add_processors { 851 | my ($self, @processors) = @_; 852 | push @{ $self->processors() }, @processors; 853 | }; 854 | 855 | =head2 $raven->clear_processors( [ Sentry::Raven::Processor::RemoveStackVariables, ... ] ) 856 | 857 | =cut 858 | 859 | sub clear_processors { 860 | my ($self) = @_; 861 | $self->processors([]); 862 | }; 863 | 864 | =head1 STANDARD OPTIONS 865 | 866 | These options can be passed to all methods accepting %context. Passing context to the constructor overrides defaults. 867 | 868 | =over 869 | 870 | =item C<< culprit => 'Some::Software' >> 871 | 872 | The source of the event. Defaults to C. 873 | 874 | =item C<< event_id => '534188f7c1ff4ff280c2e1206c9e0548' >> 875 | 876 | The unique identifier string for an event, usually UUID v4. Max 32 characters. Defaults to a new unique UUID for each event. Invalid ids may be discarded silently. 877 | 878 | =item C<< extra => { key1 => 'val1', ... } >> 879 | 880 | Arbitrary key value pairs with extra information about an event. Defaults to C<{}>. 881 | 882 | =item C<< level => 'error' >> 883 | 884 | Event level of an event. Acceptable values are C, C, C, C, and C. Defaults to C. 885 | 886 | =item C<< logger => 'root' >> 887 | 888 | The creator of an event. Defaults to 'root'. 889 | 890 | =item C<< platform => 'perl' >> 891 | 892 | The platform (language) in which an event occurred. Defaults to C. 893 | 894 | =item C<< release => 'ec899ea' >> 895 | 896 | Track the release version of your application. 897 | 898 | =item C<< processors => [ Sentry::Raven::Processor::RemoveStackVariables, ... ] >> 899 | 900 | A set or processors to be applied to events before they are posted. See L for more information. This can only be set during construction and not on other methods accepting %context. 901 | 902 | =item C<< server_name => 'localhost.example.com' >> 903 | 904 | The hostname on which an event occurred. Defaults to the system hostname. 905 | 906 | =item C<< tags => { key1 => 'val1, ... } >> 907 | 908 | Arbitrary key value pairs with tags for categorizing an event. Defaults to C<{}>. 909 | 910 | =item C<< fingerprint => [ 'val1', 'val2', ... } >> 911 | 912 | Array of strings used to control how events aggregate in the sentry web interface. The string C<'{{ default }}'> has special meaning when used as the first value; it indicates that sentry should use the default aggregation method in addition to any others specified (useful for fine-grained aggregation). Defaults to C<['{{ default }}']>. 913 | 914 | =item C<< timestamp => '1970-01-01T00:00:00' >> 915 | 916 | Timestamp of an event. ISO 8601 format. Defaults to the current time. Invalid values may be discarded silently. 917 | 918 | =item C<< environment => 'production' >> 919 | 920 | Specify the environment (i.e. I, I, etc.) that your project is deployed in. More information 921 | can be found on the L. 922 | 923 | =back 924 | 925 | =head1 CONFIGURATION AND ENVIRONMENT 926 | 927 | =over 928 | 929 | =item SENTRY_DSN=C<< http://:@sentry.io/ >> 930 | 931 | A default DSN to be used if sentry_dsn is not passed to c. 932 | 933 | =back 934 | 935 | =head1 LICENSE 936 | 937 | Copyright (C) 2019 by Matt Harrington 938 | 939 | The full text of this license can be found in the LICENSE file included with this module. 940 | 941 | =cut 942 | 943 | 1; 944 | -------------------------------------------------------------------------------- /lib/Sentry/Raven/Processor.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Sentry::Raven::Processor - Sentry event processors 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Sentry::Raven; 8 | use Sentry::Raven::Processor::RemoveStackVariables; 9 | 10 | my $raven = Sentry::Raven->new( 11 | processors => [ Sentry::Raven::Processor::RemoveStackVariables ], 12 | ); 13 | 14 | =head1 DESCRIPTION 15 | 16 | Processors are a mechanism for modifying events after they are generated but before they are posted to the sentry service. They are useful for scrubbing sensitive data, such as passwords, as well as adding additional context. 17 | 18 | =head1 STANDARD PROCESSORS 19 | 20 | =head1 IMPLEMENTING A PROCESSOR 21 | 22 | Processors must have the following class methods: 23 | 24 | =head2 $processed_event = My::Processor->process($event) 25 | 26 | This is the interface for processing events. Returns a modified version of the event. 27 | 28 | -------------------------------------------------------------------------------- /lib/Sentry/Raven/Processor/RemoveStackVariables.pm: -------------------------------------------------------------------------------- 1 | package Sentry::Raven::Processor::RemoveStackVariables; 2 | 3 | use 5.008; 4 | use strict; 5 | use warnings; 6 | 7 | =head1 NAME 8 | 9 | Sentry::Raven::Processor::RemoveStackVariables - Remove stack variables from stack traces in events 10 | 11 | =head1 SYNOPSIS 12 | 13 | use Sentry::Raven; 14 | use Sentry::Raven::Processor::RemoveStackVariables; 15 | 16 | my $raven = Sentry::Raven->new( 17 | processors => [ Sentry::Raven::Processor::RemoveStackVariables ], 18 | ); 19 | 20 | =head1 DESCRIPTION 21 | 22 | This processor removes variables from stack traces before they are posted to the sentry service. This prevents sensitive values from being exposed, such as passwords or credit card numbers. 23 | 24 | =head1 METHODS 25 | 26 | =head2 my $processed_event = Sentry::Raven::Processor::RemoveStackVariables->process( $event ) 27 | 28 | Process an event. 29 | 30 | =cut 31 | 32 | sub process { 33 | my ($class, $event) = @_; 34 | if ($event->{'sentry.interfaces.Stacktrace'}) { 35 | my $num_frames = scalar(@{$event->{'sentry.interfaces.Stacktrace'}->{frames}}); 36 | delete($event->{'sentry.interfaces.Stacktrace'}->{frames}->[$_]->{vars}) for 0..($num_frames-1); 37 | } 38 | return $event; 39 | } 40 | 41 | 1; 42 | -------------------------------------------------------------------------------- /t/00-load.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More tests => 1; 7 | 8 | use_ok( 'Sentry::Raven' ); 9 | -------------------------------------------------------------------------------- /t/10-dsn.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | use English '-no_match_vars'; 9 | use Sentry::Raven; 10 | 11 | my $dsn = 'http://key:secret@somewhere.com:9000/foo/123'; 12 | 13 | is(Sentry::Raven->new(sentry_dsn => $dsn)->post_url(), 'http://somewhere.com:9000/foo/api/123/store/'); 14 | 15 | { 16 | local $ENV{SENTRY_DSN} = $dsn; 17 | is(Sentry::Raven->new()->post_url(), 'http://somewhere.com:9000/foo/api/123/store/'); 18 | } 19 | 20 | eval { Sentry::Raven->new() }; 21 | is($EVAL_ERROR, "must pass sentry_dsn or set SENTRY_DSN envirionment variable\n"); 22 | 23 | eval { Sentry::Raven->new(sentry_dsn => 'not a uri') }; 24 | is($EVAL_ERROR, "unable to parse sentry dsn: not a uri\n"); 25 | 26 | eval { Sentry::Raven->new(sentry_dsn => 'http://missing.userinfo.com') }; 27 | is($EVAL_ERROR, "unable to parse public and secret keys from: http://missing.userinfo.com\n"); 28 | 29 | done_testing(); 30 | -------------------------------------------------------------------------------- /t/11-generic-event.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | use Sentry::Raven; 9 | use Sys::Hostname; 10 | use UUID::Tiny ':std'; 11 | 12 | local $ENV{SENTRY_DSN} = 'http://key:secret@somewhere.com:9000/foo/123'; 13 | my $raven = Sentry::Raven->new(); 14 | 15 | subtest 'defaults' => sub { 16 | 17 | is($raven->encoding, 'gzip'); 18 | is($raven->encoding('base64'), 'base64'); 19 | is($raven->encoding, 'base64'); 20 | 21 | my $event = $raven->_construct_event(); 22 | 23 | is($event->{level}, 'error'); 24 | is($event->{logger}, 'root'); 25 | is($event->{platform}, 'perl'); 26 | is($event->{culprit}, undef); 27 | is($event->{message}, undef); 28 | is($event->{release}, undef); 29 | is($event->{environment}, undef); 30 | 31 | is_deeply($event->{extra}, {}); 32 | is_deeply($event->{tags}, {}); 33 | is_deeply($event->{fingerprint}, ['{{ default }}']); 34 | 35 | ok(string_to_uuid($event->{event_id})); 36 | like($event->{timestamp}, qr/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d$/); 37 | is($event->{server_name}, hostname()); 38 | }; 39 | 40 | subtest 'modifying defaults' => sub { 41 | my $raven = Sentry::Raven->new( 42 | level => 'warning', 43 | logger => 'mylogger', 44 | platform => 'myplatform', 45 | culprit => 'myculprit', 46 | message => 'mymessage', 47 | encoding => 'base64', 48 | release => 'ec899ea', 49 | environment => 'testing', 50 | 51 | extra => { 52 | key1 => 'value1', 53 | key2 => 'value2', 54 | }, 55 | tags => { 56 | tag1 => 'value1', 57 | tag2 => 'value2', 58 | }, 59 | fingerprint => [ 60 | 'new', 61 | 'fingerprint', 62 | ], 63 | 64 | event_id => 'myeventid', 65 | timestamp => 'mytimestamp', 66 | server_name => 'myservername', 67 | ); 68 | 69 | is($raven->encoding, 'base64'); 70 | 71 | my $event = $raven->_construct_event(); 72 | 73 | is($event->{level}, 'warning'); 74 | is($event->{logger}, 'mylogger'); 75 | is($event->{platform}, 'myplatform'); 76 | is($event->{culprit}, 'myculprit'); 77 | is($event->{message}, 'mymessage'); 78 | is($event->{release}, 'ec899ea'); 79 | is($event->{environment}, 'testing'); 80 | 81 | is_deeply( 82 | $event->{extra}, 83 | { 84 | key1 => 'value1', 85 | key2 => 'value2', 86 | }, 87 | ); 88 | 89 | is_deeply( 90 | $event->{tags}, 91 | { 92 | tag1 => 'value1', 93 | tag2 => 'value2', 94 | }, 95 | ); 96 | 97 | is_deeply( 98 | $event->{fingerprint}, 99 | [ 100 | 'new', 101 | 'fingerprint', 102 | ], 103 | ); 104 | 105 | is($event->{event_id}, 'myeventid'); 106 | is($event->{timestamp}, 'mytimestamp'); 107 | is($event->{server_name}, 'myservername'); 108 | 109 | 110 | $raven->add_context( 111 | level => 'error', 112 | logger => 'yourlogger', 113 | ); 114 | 115 | $event = $raven->_construct_event(); 116 | 117 | is($event->{level}, 'error'); 118 | is($event->{logger}, 'yourlogger'); 119 | 120 | 121 | my %context = $raven->get_context(); 122 | 123 | is($context{level}, 'error'); 124 | is($context{logger}, 'yourlogger'); 125 | 126 | 127 | $raven->clear_context(); 128 | 129 | %context = $raven->get_context(); 130 | 131 | is($context{level}, undef); 132 | is($context{logger}, undef); 133 | 134 | 135 | $raven->add_context(tags => { a => 1, b => 2 }); 136 | $raven->merge_tags(a => 10, c => 30); 137 | 138 | is_deeply( 139 | $raven->context()->{tags}, 140 | { 141 | a => 10, 142 | b => 2, 143 | c => 30, 144 | }, 145 | ); 146 | 147 | 148 | $raven->add_context(extra => { a => 1, b => 2 }); 149 | $raven->merge_extra(a => 10, c => 30); 150 | 151 | is_deeply( 152 | $raven->context()->{extra}, 153 | { 154 | a => 10, 155 | b => 2, 156 | c => 30, 157 | }, 158 | ); 159 | }; 160 | 161 | subtest 'overriding defaults' => sub { 162 | my $event = $raven->_construct_event( 163 | level => 'warning', 164 | logger => 'mylogger', 165 | platform => 'myplatform', 166 | culprit => 'myculprit', 167 | message => 'mymessage', 168 | 169 | extra => { 170 | key1 => 'value1', 171 | key2 => 'value2', 172 | }, 173 | tags => { 174 | tag1 => 'value1', 175 | tag2 => 'value2', 176 | }, 177 | fingerprint => [ 178 | 'new', 179 | 'fingerprint', 180 | ], 181 | 182 | event_id => 'myeventid', 183 | timestamp => 'mytimestamp', 184 | server_name => 'myservername', 185 | ); 186 | 187 | is($event->{level}, 'warning'); 188 | is($event->{logger}, 'mylogger'); 189 | is($event->{platform}, 'myplatform'); 190 | is($event->{culprit}, 'myculprit'); 191 | is($event->{message}, 'mymessage'); 192 | 193 | is_deeply( 194 | $event->{extra}, 195 | { 196 | key1 => 'value1', 197 | key2 => 'value2', 198 | }, 199 | ); 200 | 201 | is_deeply( 202 | $event->{tags}, 203 | { 204 | tag1 => 'value1', 205 | tag2 => 'value2', 206 | }, 207 | ); 208 | 209 | is_deeply( 210 | $event->{fingerprint}, 211 | [ 212 | 'new', 213 | 'fingerprint', 214 | ], 215 | ); 216 | 217 | is($event->{event_id}, 'myeventid'); 218 | is($event->{timestamp}, 'mytimestamp'); 219 | is($event->{server_name}, 'myservername'); 220 | }; 221 | 222 | subtest 'overriding modified defaults' => sub { 223 | my $raven = Sentry::Raven->new( 224 | level => 'warning', 225 | extra => { 226 | key1 => 'value1', 227 | }, 228 | tags => { 229 | tag1 => 'value1', 230 | }, 231 | fingerprint => [ 232 | 'value1', 233 | ], 234 | ); 235 | 236 | my $event = $raven->_construct_event( 237 | level => 'fatal', 238 | 239 | extra => { 240 | key2 => 'value2', 241 | }, 242 | tags => { 243 | tag2 => 'value2', 244 | }, 245 | fingerprint => [ 246 | 'value2', 247 | ], 248 | ); 249 | 250 | is($event->{level}, 'fatal'); 251 | 252 | is_deeply( 253 | $event->{extra}, 254 | { 255 | key1 => 'value1', 256 | key2 => 'value2', 257 | }, 258 | ); 259 | 260 | is_deeply( 261 | $event->{tags}, 262 | { 263 | tag1 => 'value1', 264 | tag2 => 'value2', 265 | }, 266 | ); 267 | 268 | is_deeply( 269 | $event->{fingerprint}, 270 | [ 271 | 'value2', 272 | ], 273 | ); 274 | }; 275 | 276 | subtest 'invalid context' => sub { 277 | my $warn_message; 278 | local $SIG{__WARN__} = sub { $warn_message = $_[0] }; 279 | 280 | my $event = $raven->_construct_event( 281 | level => 'not-a-level', 282 | ); 283 | 284 | is($event->{level}, 'error'); 285 | is($warn_message, "unknown level: not-a-level\n"); 286 | }; 287 | 288 | subtest 'generic interfaces' => sub { 289 | # request_context is probably a more real-world example of this 290 | # but exception_context is conveniently easy to create 291 | my %exception_context = Sentry::Raven->exception_context("error message"); 292 | $raven = Sentry::Raven->new( %exception_context ); 293 | 294 | my $event = $raven->_construct_event(); 295 | 296 | for my $exception_key ( keys %exception_context ) { 297 | is_deeply $event->{ $exception_key }, 298 | $exception_context{ $exception_key }, 299 | "instance-level $exception_key appears in events"; 300 | } 301 | }; 302 | 303 | done_testing(); 304 | -------------------------------------------------------------------------------- /t/12-specialized-event.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | use File::Spec; 9 | use Sentry::Raven; 10 | use Devel::StackTrace; 11 | use File::Slurp; 12 | 13 | local $ENV{SENTRY_DSN} = 'http://key:secret@somewhere.com:9000/foo/123'; 14 | my $raven = Sentry::Raven->new(); 15 | 16 | my $trace; 17 | sub a { $trace = Devel::StackTrace->new() } 18 | a(1,"x"); 19 | 20 | subtest 'message' => sub { 21 | my $event = $raven->_construct_message_event('mymessage', level => 'info'); 22 | 23 | is($event->{message}, 'mymessage'); 24 | is($event->{level}, 'info'); 25 | }; 26 | 27 | subtest 'exception' => sub { 28 | my $event = $raven->_construct_exception_event('Operation completed successfully', type => 'OperationFailedException', level => 'info'); 29 | 30 | is($event->{level}, 'info'); 31 | is_deeply( 32 | $event->{'sentry.interfaces.Exception'}, 33 | { 34 | type => 'OperationFailedException', 35 | value => 'Operation completed successfully', 36 | }, 37 | ); 38 | }; 39 | 40 | subtest 'request' => sub { 41 | my $event = $raven->_construct_request_event( 42 | 'http://google.com', 43 | method => 'GET', 44 | data => 'foo=bar', 45 | query_string => 'foo=bar', 46 | cookies => 'foo=bar', 47 | headers => { 'Content-Type' => 'text/html' }, 48 | env => { REMOTE_ADDR => '192.168.0.1' }, 49 | level => 'info', 50 | ); 51 | 52 | is($event->{level}, 'info'); 53 | is_deeply( 54 | $event->{'sentry.interfaces.Http'}, 55 | { 56 | url => 'http://google.com', 57 | method => 'GET', 58 | data => 'foo=bar', 59 | query_string => 'foo=bar', 60 | cookies => 'foo=bar', 61 | headers => { 'Content-Type' => 'text/html' }, 62 | env => { REMOTE_ADDR => '192.168.0.1' }, 63 | }, 64 | ); 65 | }; 66 | 67 | subtest 'stacktrace' => sub { 68 | my $frames = [ 69 | { 70 | filename => 'filename1', 71 | function => 'function1', 72 | module => 'module1', 73 | lineno => 10, 74 | colno => 20, 75 | abs_path => '/tmp/filename1', 76 | context_line => 'my $foo = "bar";', 77 | pre_context => [ 'sub function1 {' ], 78 | post_context => [ 'print $foo' ], 79 | in_app => 1, 80 | vars => { foo => 'bar' }, 81 | }, 82 | { 83 | filename => 'my/file2.pl', 84 | }, 85 | ]; 86 | 87 | my $event = $raven->_construct_stacktrace_event($frames, level => 'info'); 88 | 89 | is($event->{level}, 'info'); 90 | is_deeply( 91 | $event->{'sentry.interfaces.Stacktrace'}, 92 | { frames => $frames }, 93 | ); 94 | 95 | my @file_lines = read_file(File::Spec->catfile('t', '12-specialized-event.t')); 96 | chomp(@file_lines); 97 | 98 | $frames = [ 99 | { 100 | abs_path => File::Spec->catfile('t', '12-specialized-event.t'), 101 | filename => '12-specialized-event.t', 102 | function => undef, 103 | lineno => 18, 104 | module => 'main', 105 | vars => undef, 106 | context_line => $file_lines[ 17 ], 107 | pre_context => [ @file_lines[ 12 .. 16 ] ], 108 | post_context => [ @file_lines [ 18 .. 22 ] ], 109 | }, 110 | { 111 | abs_path => File::Spec->catfile('t', '12-specialized-event.t'), 112 | filename => '12-specialized-event.t', 113 | function => 'main::a', 114 | lineno => 17, 115 | module => 'main', 116 | vars => { 117 | '@_' => ['1','"x"'], 118 | }, 119 | context_line => $file_lines[ 16 ], 120 | pre_context => [ @file_lines[ 11 .. 15 ] ], 121 | post_context => [ @file_lines [ 17 .. 21 ] ], 122 | }, 123 | ]; 124 | 125 | is_deeply( 126 | $raven->_construct_stacktrace_event($trace)->{'sentry.interfaces.Stacktrace'}, 127 | { frames => $frames }, 128 | ); 129 | }; 130 | 131 | subtest 'user' => sub { 132 | my $event = $raven->_construct_user_event(id => 'myid', username => 'myusername', email => 'my@email.com', ip_address => '192.168.0.1', level => 'info'); 133 | 134 | is($event->{level}, 'info'); 135 | is_deeply( 136 | $event->{'sentry.interfaces.User'}, 137 | { 138 | id => 'myid', 139 | username => 'myusername', 140 | email => 'my@email.com', 141 | ip_address => '192.168.0.1', 142 | }, 143 | ); 144 | }; 145 | 146 | subtest 'user_context arbitray data' => sub { 147 | my $event = {$raven->user_context(arbitrary_key => 'arbitrary value')}; 148 | is($event->{'sentry.interfaces.User'}{arbitrary_key}, 'arbitrary value'); 149 | }; 150 | 151 | subtest 'query' => sub { 152 | my $event = $raven->_construct_query_event( 'select 1', engine => 'DBD::Pg', level => 'info'); 153 | 154 | is($event->{level}, 'info'); 155 | is_deeply( 156 | $event->{'sentry.interfaces.Query'}, 157 | { 158 | query => 'select 1', 159 | engine => 'DBD::Pg', 160 | }, 161 | ); 162 | }; 163 | 164 | done_testing(); 165 | -------------------------------------------------------------------------------- /t/13-service-call.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | use HTTP::Response; 9 | use Sentry::Raven; 10 | use Test::LWP::UserAgent; 11 | use IO::Uncompress::Gunzip 'gunzip'; 12 | 13 | my $ua = Test::LWP::UserAgent->new(); 14 | $ua->map_response( 15 | qr//, 16 | HTTP::Response->new( 17 | '200', 18 | undef, 19 | undef, 20 | '{ "id": "some-uuid-string" }', 21 | ), 22 | ); 23 | 24 | local $ENV{SENTRY_DSN} = 'http://key:secret@somewhere.com:9000/foo/123'; 25 | 26 | subtest 'keep_alive' => sub { 27 | ok(Sentry::Raven->new()->ua_obj()->conn_cache()); 28 | }; 29 | 30 | subtest 'json' => sub { 31 | my $raven = Sentry::Raven->new( 32 | ua_obj => $ua, 33 | ); 34 | $raven->encoding('text'); 35 | my $event_id = $raven->capture_message('HELO'); 36 | my $request = $ua->last_http_request_sent(); 37 | 38 | is( 39 | $request->method(), 40 | 'POST', 41 | ); 42 | 43 | is( 44 | $event_id, 45 | 'some-uuid-string', 46 | ); 47 | 48 | like( 49 | $request->header('x-sentry-auth'), 50 | qr{^Sentry sentry_client=raven-perl/[\d.]+, sentry_key=key, sentry_secret=secret, sentry_timestamp=\d+, sentry_version=\d+$}, 51 | ); 52 | 53 | is($ua->last_useragent()->timeout(), 5); 54 | 55 | my $event = $raven->json_obj()->decode($request->content()); 56 | is($event->{message}, 'HELO'); 57 | }; 58 | 59 | subtest 'gzip' => sub { 60 | my $raven = Sentry::Raven->new( 61 | ua_obj => $ua, 62 | ); 63 | 64 | my $event_id = $raven->capture_message('HELO'); 65 | my $request = $ua->last_http_request_sent(); 66 | 67 | is($request->header('content-encoding'), 'gzip'); 68 | 69 | my $compressed_content = $request->content(); 70 | my $content; 71 | 72 | gunzip(\$compressed_content, \$content); 73 | 74 | my $event = $raven->json_obj()->decode($content); 75 | is($event->{message}, 'HELO'); 76 | }; 77 | 78 | subtest 'timeout' => sub { 79 | my $raven = Sentry::Raven->new(ua_obj => $ua, timeout => 10); 80 | $raven->capture_message('HELO'); 81 | 82 | is($ua->last_useragent()->timeout(), 10); 83 | }; 84 | 85 | done_testing(); 86 | -------------------------------------------------------------------------------- /t/14-does-not-die.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | package FailingRaven; 7 | use Moo; 8 | extends 'Sentry::Raven'; 9 | 10 | sub json_obj { die "something is super wrong" } 11 | 12 | 13 | package main; 14 | 15 | use Test::More; 16 | 17 | local $ENV{SENTRY_DSN} = 'http://key:secret@somewhere.com:9000/foo/123'; 18 | my $failing_raven = FailingRaven->new(); 19 | 20 | my $warn_message; 21 | local $SIG{__WARN__} = sub { $warn_message = $_[0] }; 22 | 23 | is($failing_raven->_post_event("{ 'foo': 'bar' }"), undef); 24 | like($warn_message, qr/something is super wrong/); 25 | 26 | done_testing(); 27 | -------------------------------------------------------------------------------- /t/15-error-handler.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | use Test::Warn; 8 | 9 | use English '-no_match_vars'; 10 | use HTTP::Response; 11 | use Sentry::Raven; 12 | use Test::LWP::UserAgent; 13 | 14 | my $ua = Test::LWP::UserAgent->new(); 15 | $ua->map_response( 16 | qr//, 17 | HTTP::Response->new( 18 | '200', 19 | undef, 20 | undef, 21 | '{ "id": "some-uuid-string" }', 22 | ), 23 | ); 24 | 25 | local $ENV{SENTRY_DSN} = 'http://key:secret@somewhere.com:9000/foo/123'; 26 | my $raven = Sentry::Raven->new( 27 | ua_obj => $ua, 28 | ); 29 | $raven->encoding('text'); 30 | 31 | sub a { b() } 32 | sub b { c() } 33 | sub c { die "it was not meant to be" } 34 | 35 | $raven->capture_errors( 36 | sub { a() }, 37 | level => 'fatal', 38 | ); 39 | 40 | my $request = $ua->last_http_request_sent(); 41 | my $json = $request->content(); 42 | my $event = $raven->json_obj()->decode($json); 43 | 44 | subtest 'event' => sub { 45 | is($event->{level}, 'fatal'); 46 | is($event->{culprit}, 't/15-error-handler.t'); 47 | like($event->{message}, qr/it was not meant to be/); 48 | }; 49 | 50 | subtest 'exception' => sub { 51 | like($event->{'sentry.interfaces.Exception'}->{value}, qr/it was not meant to be/); 52 | }; 53 | 54 | subtest 'stacktrace' => sub { 55 | my @frames = @{ $event->{'sentry.interfaces.Stacktrace'}->{frames} }; 56 | 57 | cmp_ok(scalar(@frames), '>=', 5); 58 | 59 | ok(defined $frames[-1]->{function}); 60 | ok(defined $frames[-1]->{module}); 61 | ok(defined $frames[-1]->{abs_path}); 62 | ok(defined $frames[-1]->{filename}); 63 | ok(defined $frames[-1]->{lineno}); 64 | }; 65 | 66 | subtest 'dies when unable to submit event' => sub { 67 | my $failing_ua = Test::LWP::UserAgent->new(); 68 | $failing_ua->map_response( 69 | qr//, 70 | HTTP::Response->new( 71 | '500', 72 | ), 73 | ); 74 | 75 | eval { 76 | local $SIG{__WARN__} = sub {}; 77 | Sentry::Raven->new(ua_obj => $failing_ua)->capture_errors( sub { a() } ); 78 | }; 79 | 80 | my $eval_error = $EVAL_ERROR; 81 | 82 | like($eval_error, qr/failed to submit event to sentry service/); 83 | like($eval_error, qr/"level"\s*=>\s*"error"/); 84 | }; 85 | 86 | subtest 'warn when unable to capture message' => sub{ 87 | my $failing_ua = Test::LWP::UserAgent->new(); 88 | $failing_ua->map_response( 89 | qr//, 90 | HTTP::Response->new( 91 | '500', 92 | ), 93 | ); 94 | my $raven = Sentry::Raven->new( ua_obj => $failing_ua ); 95 | warning_like { $raven->capture_message('Irrelevant') } qr/Unsuccessful/ , "Good warning"; 96 | ok(1); 97 | }; 98 | 99 | 100 | done_testing(); 101 | -------------------------------------------------------------------------------- /t/16-processors.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | use Sentry::Raven; 9 | use Sentry::Raven::Processor::RemoveStackVariables; 10 | use Test::LWP::UserAgent; 11 | 12 | my $ua = Test::LWP::UserAgent->new(); 13 | $ua->map_response( 14 | qr//, 15 | HTTP::Response->new( 16 | '200', 17 | undef, 18 | undef, 19 | '{ "id": "some-uuid-string" }', 20 | ), 21 | ); 22 | 23 | local $ENV{SENTRY_DSN} = 'http://key:secret@somewhere.com:9000/foo/123'; 24 | my $raven = Sentry::Raven->new( 25 | ua_obj => $ua, 26 | ); 27 | $raven->encoding('text'); 28 | 29 | subtest 'processor accessors' => sub { 30 | $raven->add_processors('Sentry::Raven::Processor::P1', 'Sentry::Raven::Processor::P2'); 31 | 32 | is_deeply( 33 | $raven->processors(), 34 | ['Sentry::Raven::Processor::P1', 'Sentry::Raven::Processor::P2'], 35 | ); 36 | 37 | $raven->clear_processors(); 38 | 39 | is_deeply($raven->processors(), []); 40 | }; 41 | 42 | subtest 'processes events' => sub { 43 | $raven->clear_processors(); 44 | $raven->add_processors('ReverseMessage'); 45 | $raven->capture_message('HELO'); 46 | 47 | my $event = $raven->json_obj()->decode($ua->last_http_request_sent()->content()); 48 | is($event->{message}, 'OLEH'); 49 | }; 50 | 51 | 52 | subtest 'remove stack variables' => sub { 53 | $raven->clear_processors(); 54 | $raven->add_processors('Sentry::Raven::Processor::RemoveStackVariables'); 55 | 56 | my $frames = [ 57 | { 58 | filename => 'filename1', 59 | lineno => 10, 60 | vars => { 1 => 10, 2 => 20 }, 61 | }, 62 | { 63 | filename => 'filename2', 64 | lineno => 20, 65 | vars => { 1 => 100, 2 => 200 }, 66 | }, 67 | ]; 68 | 69 | $raven->capture_stacktrace($frames); 70 | 71 | delete($frames->[$_]->{vars}) for 0..1; 72 | 73 | my $event = $raven->json_obj()->decode($ua->last_http_request_sent()->content()); 74 | is_deeply($event->{'sentry.interfaces.Stacktrace'}->{frames}, $frames); 75 | }; 76 | 77 | done_testing(); 78 | 79 | package ReverseMessage; 80 | 81 | use strict; 82 | use warnings; 83 | 84 | sub process { 85 | my ($class, $event) = @_; 86 | $event->{message} = reverse($event->{message}); 87 | return $event; 88 | }; 89 | -------------------------------------------------------------------------------- /t/21-stacktrace-failures.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | use File::Slurp; 9 | use File::Spec; 10 | use Sentry::Raven; 11 | use Devel::StackTrace; 12 | 13 | local $ENV{SENTRY_DSN} = 'http://key:secret@somewhere.com:9000/foo/123'; 14 | my $raven = Sentry::Raven->new(); 15 | 16 | my $trace; 17 | a(1,"x"); 18 | 19 | my @file_lines = read_file(File::Spec->catfile('t', '21-stacktrace-failures.t')); 20 | chomp(@file_lines); 21 | 22 | my $frames = [ 23 | { 24 | abs_path => File::Spec->catfile('t', '21-stacktrace-failures.t'), 25 | filename => '21-stacktrace-failures.t', 26 | function => undef, 27 | lineno => 17, 28 | module => 'main', 29 | vars => undef, 30 | context_line => $file_lines[ 16 ], 31 | pre_context => [ @file_lines[ 11 .. 15 ] ], 32 | post_context => [ @file_lines [ 17 .. 21 ] ], 33 | }, 34 | { 35 | abs_path => '/bad/path/NoSuchFileEver.pm', 36 | filename => 'NoSuchFileEver.pm', 37 | function => 'main::a', 38 | lineno => 127, 39 | module => 'main', 40 | vars => { 41 | '@_' => ['1','"x"'], 42 | }, 43 | }, 44 | ]; 45 | 46 | my $context = $raven->_construct_stacktrace_event($trace)->{'sentry.interfaces.Stacktrace'}; 47 | 48 | is_deeply( 49 | $context, 50 | { frames => $frames }, 51 | ); 52 | 53 | done_testing; 54 | 55 | # line 127 "/bad/path/NoSuchFileEver.pm" 56 | sub a { $trace = Devel::StackTrace->new() } 57 | -------------------------------------------------------------------------------- /t/90-perl-critic.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | SKIP: { 9 | skip 'Skipping release tests', 1 unless $ENV{RELEASE_TESTING}; 10 | 11 | require Test::Perl::Critic; 12 | 13 | Test::Perl::Critic->import( 14 | -verbose => 10, 15 | -severity => 'gentle', 16 | -force => 0, 17 | ); 18 | 19 | all_critic_ok(); 20 | } 21 | 22 | done_testing() unless $ENV{RELEASE_TESTING}; 23 | -------------------------------------------------------------------------------- /t/91-pod-coverage.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | SKIP: { 9 | skip 'Skipping release tests', 1 unless $ENV{RELEASE_TESTING}; 10 | 11 | require Test::Pod::Coverage; 12 | Test::Pod::Coverage->import(); 13 | all_pod_coverage_ok({ trustme => [qr/UUID_V4/, qr/BUILDARGS/] }); 14 | } 15 | 16 | done_testing(); 17 | -------------------------------------------------------------------------------- /t/92-pod-syntax.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | SKIP: { 9 | skip 'Skipping release tests', 1 unless $ENV{RELEASE_TESTING}; 10 | 11 | require Test::Pod; 12 | Test::Pod->import(); 13 | all_pod_files_ok(); 14 | } 15 | 16 | done_testing(); 17 | -------------------------------------------------------------------------------- /t/93-spelling.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | SKIP: { 9 | skip 'Skipping release tests', 1 unless $ENV{RELEASE_TESTING}; 10 | 11 | require Test::Spellunker; 12 | Test::Spellunker->import(); 13 | all_pod_files_spelling_ok(); 14 | } 15 | 16 | done_testing(); 17 | -------------------------------------------------------------------------------- /t/94-changes.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | SKIP: { 9 | skip 'Skipping release tests', 1 unless $ENV{RELEASE_TESTING}; 10 | 11 | eval "use Test::CPAN::Changes::ReallyStrict"; 12 | changes_ok(); 13 | } 14 | 15 | done_testing(); 16 | -------------------------------------------------------------------------------- /t/99-todo.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -T 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | TODO: { 9 | local $TODO = 'unimplemented features'; 10 | 11 | ok(undef, 'figure out why the =over lists are not indented (and look like junk on search.cpan)'); 12 | } 13 | 14 | done_testing(); 15 | --------------------------------------------------------------------------------