2
2
'use strict' ;
3
3
4
4
var TraceKit = require ( '../vendor/TraceKit/tracekit' ) ;
5
+ var consolePlugin = require ( '../plugins/console' ) ;
5
6
var RavenConfigError = require ( './configError' ) ;
6
7
var utils = require ( './utils' ) ;
7
8
@@ -16,6 +17,7 @@ var objectMerge = utils.objectMerge;
16
17
var truncate = utils . truncate ;
17
18
var urlencode = utils . urlencode ;
18
19
var uuid4 = utils . uuid4 ;
20
+ var htmlElementAsString = utils . htmlElementAsString ;
19
21
20
22
var dsnKeys = 'source protocol user pass host port path' . split ( ' ' ) ,
21
23
dsnPattern = / ^ (?: ( \w + ) : ) ? \/ \/ (?: ( \w + ) ( : \w + ) ? @ ) ? ( [ \w \. - ] + ) (?: : ( \d + ) ) ? ( \/ .* ) / ;
@@ -58,6 +60,10 @@ function Raven() {
58
60
this . _plugins = [ ] ;
59
61
this . _startTime = now ( ) ;
60
62
this . _wrappedBuiltIns = [ ] ;
63
+ this . _breadcrumbs = [ ] ;
64
+ this . _breadcrumbLimit = 20 ;
65
+ this . _lastCapturedEvent = null ;
66
+ this . _lastHref = window . location && location . href ;
61
67
62
68
for ( var method in this . _originalConsole ) { // eslint-disable-line guard-for-in
63
69
this . _originalConsoleMethods [ method ] = this . _originalConsole [ method ] ;
@@ -198,11 +204,11 @@ Raven.prototype = {
198
204
*
199
205
* @param {object } options A specific set of options for this context [optional]
200
206
* @param {function } func The function to be wrapped in a new context
207
+ * @param {function } func A function to call before the try/catch wrapper [optional, private]
201
208
* @return {function } The newly wrapped functions with a context
202
209
*/
203
- wrap : function ( options , func ) {
210
+ wrap : function ( options , func , _before ) {
204
211
var self = this ;
205
-
206
212
// 1 argument has been passed, and it's not a function
207
213
// so just return it
208
214
if ( isUndefined ( func ) && ! isFunction ( options ) ) {
@@ -241,9 +247,13 @@ Raven.prototype = {
241
247
function wrapped ( ) {
242
248
var args = [ ] , i = arguments . length ,
243
249
deep = ! options || options && options . deep !== false ;
250
+
251
+ if ( _before && isFunction ( _before ) ) {
252
+ _before . apply ( this , arguments ) ;
253
+ }
254
+
244
255
// Recursively wrap all of a function's arguments that are
245
256
// functions themselves.
246
-
247
257
while ( i -- ) args [ i ] = deep ? self . wrap ( options , arguments [ i ] ) : arguments [ i ] ;
248
258
249
259
try {
@@ -261,10 +271,9 @@ Raven.prototype = {
261
271
wrapped [ property ] = func [ property ] ;
262
272
}
263
273
}
264
- func . __raven_wrapper__ = wrapped ;
265
-
266
274
wrapped . prototype = func . prototype ;
267
275
276
+ func . __raven_wrapper__ = wrapped ;
268
277
// Signal that this function has been wrapped already
269
278
// for both debugging and to prevent it to being wrapped twice
270
279
wrapped . __raven__ = true ;
@@ -345,6 +354,19 @@ Raven.prototype = {
345
354
return this ;
346
355
} ,
347
356
357
+ captureBreadcrumb : function ( type , data ) {
358
+ var crumb = {
359
+ type : type ,
360
+ timestamp : now ( ) / 1000 ,
361
+ data : data
362
+ } ;
363
+
364
+ this . _breadcrumbs . push ( crumb ) ;
365
+ if ( this . _breadcrumbs . length > this . _breadcrumbLimit ) {
366
+ this . _breadcrumbs . shift ( ) ;
367
+ }
368
+ } ,
369
+
348
370
addPlugin : function ( plugin /*arg1, arg2, ... argN*/ ) {
349
371
var pluginArgs = Array . prototype . slice . call ( arguments , 1 ) ;
350
372
@@ -594,6 +616,32 @@ Raven.prototype = {
594
616
}
595
617
} ,
596
618
619
+
620
+ /**
621
+ * Wraps addEventListener to capture breadcrumbs
622
+ * @param evtName the event name (e.g. "click")
623
+ * @param fn the function being wrapped
624
+ * @returns {Function }
625
+ * @private
626
+ */
627
+ _breadcrumbEventHandler : function ( evtName ) {
628
+ var self = this ;
629
+ return function ( evt ) {
630
+ // It's possible this handler might trigger multiple times for the same
631
+ // event (e.g. event propagation through node ancestors). Ignore if we've
632
+ // already captured the event.
633
+ if ( self . _lastCapturedEvent === evt )
634
+ return ;
635
+
636
+ self . _lastCapturedEvent = evt ;
637
+ var elem = evt . target ;
638
+ self . captureBreadcrumb ( 'ui_event' , {
639
+ type : evtName ,
640
+ target : htmlElementAsString ( elem )
641
+ } ) ;
642
+ } ;
643
+ } ,
644
+
597
645
/**
598
646
* Install any queued plugins
599
647
*/
@@ -638,6 +686,13 @@ Raven.prototype = {
638
686
} ) ;
639
687
}
640
688
689
+ // Capture breadcrubms from any click that is unhandled / bubbled up all the way
690
+ // to the document. Do this before we instrument addEventListener.
691
+ if ( this . _hasDocument ) {
692
+ document . addEventListener ( 'click' , self . _breadcrumbEventHandler ( 'click' ) ) ;
693
+
694
+ }
695
+
641
696
// event targets borrowed from bugsnag-js:
642
697
// https://github.com/bugsnag/bugsnag-js/blob/master/src/bugsnag.js#L666
643
698
'EventTarget Window Node ApplicationCache AudioTrackList ChannelMergerNode CryptoOperation EventSource FileReader HTMLUnknownElement IDBDatabase IDBRequest IDBTransaction KeyOperation MediaController MessagePort ModalWindow Notification SVGElementInstance Screen TextTrack TextTrackCue TextTrackList WebSocket WebSocketWorker Worker XMLHttpRequest XMLHttpRequestEventTarget XMLHttpRequestUpload' . replace ( / \w + / g, function ( global ) {
@@ -652,7 +707,14 @@ Raven.prototype = {
652
707
} catch ( err ) {
653
708
// can sometimes get 'Permission denied to access property "handle Event'
654
709
}
655
- return orig . call ( this , evt , self . wrap ( fn ) , capture , secure ) ;
710
+
711
+
712
+ // TODO: more than just click
713
+ var before ;
714
+ if ( ( global === 'EventTarget' || global === 'Node' ) && evt === 'click' ) {
715
+ before = self . _breadcrumbEventHandler ( evt , fn ) ;
716
+ }
717
+ return orig . call ( this , evt , self . wrap ( fn , undefined , before ) , capture , secure ) ;
656
718
} ;
657
719
} ) ;
658
720
fill ( proto , 'removeEventListener' , function ( orig ) {
@@ -665,12 +727,32 @@ Raven.prototype = {
665
727
} ) ;
666
728
667
729
if ( 'XMLHttpRequest' in window ) {
668
- fill ( XMLHttpRequest . prototype , 'send' , function ( origSend ) {
730
+ var xhrproto = XMLHttpRequest . prototype ;
731
+ fill ( xhrproto , 'open' , function ( origOpen ) {
732
+ return function ( method , url ) { // preserve arity
733
+ this . __raven_xhr = {
734
+ method : method ,
735
+ url : url ,
736
+ statusCode : null
737
+ } ;
738
+ return origOpen . apply ( this , arguments ) ;
739
+ } ;
740
+ } ) ;
741
+
742
+ fill ( xhrproto , 'send' , function ( origSend ) {
669
743
return function ( data ) { // preserve arity
670
744
var xhr = this ;
671
745
'onreadystatechange onload onerror onprogress' . replace ( / \w + / g, function ( prop ) {
672
- if ( prop in xhr && Object . prototype . toString . call ( xhr [ prop ] ) === '[object Function]' ) {
746
+ if ( prop in xhr && isFunction ( xhr [ prop ] ) ) {
673
747
fill ( xhr , prop , function ( orig ) {
748
+ if ( prop === 'onreadystatechange' && xhr . __raven_xhr && ( xhr . readyState === 1 || xhr . readyState === 4 ) ) {
749
+ try {
750
+ // touching statusCode in some platforms throws
751
+ // an exception
752
+ xhr . __raven_xhr . statusCode = xhr . status ;
753
+ } catch ( e ) { /* do nothing */ }
754
+ self . captureBreadcrumb ( 'http_request' , xhr . __raven_xhr ) ;
755
+ }
674
756
return self . wrap ( orig ) ;
675
757
} , true /* noUndo */ ) ; // don't track filled methods on XHR instances
676
758
}
@@ -680,6 +762,53 @@ Raven.prototype = {
680
762
} ) ;
681
763
}
682
764
765
+ // record navigation (URL) changes
766
+ if ( 'history' in window && history . pushState ) {
767
+ // TODO: remove onpopstate handler on uninstall()
768
+ var oldOnPopState = window . onpopstate ;
769
+ window . onpopstate = function ( ) {
770
+ self . captureBreadcrumb ( 'navigation' , {
771
+ from : self . _lastHref ,
772
+ to : location . href
773
+ } ) ;
774
+
775
+ // because onpopstate only tells you the "new" (to) value of location.href, and
776
+ // not the previous (from) value, we need to track the value of location.href
777
+ // ourselves
778
+ self . _lastHref = location . href ;
779
+ if ( oldOnPopState ) {
780
+ return oldOnPopState . apply ( this , arguments ) ;
781
+ }
782
+ } ;
783
+
784
+ fill ( history , 'pushState' , function ( origPushState ) {
785
+ // note history.pushState.length is 0; intentionally not declaring
786
+ // params to preserve 0 arity
787
+ return function ( /* state, title, url */ ) {
788
+ var url = arguments . length > 2 ? arguments [ 2 ] : undefined ;
789
+ self . captureBreadcrumb ( 'navigation' , {
790
+ to : url ,
791
+ from : location . href
792
+ } ) ;
793
+ if ( url ) self . _lastHref = url ;
794
+ return origPushState . apply ( this , arguments ) ;
795
+ }
796
+ } ) ;
797
+ }
798
+
799
+ // console
800
+ if ( 'console' in window && console . log ) {
801
+ consolePlugin ( self , console , {
802
+ levels : [ 'debug' , 'info' , 'warn' , 'error' , 'log' ] ,
803
+ callback : function ( msg , data ) {
804
+ self . captureBreadcrumb ( 'message' , {
805
+ level : data . level ,
806
+ message : msg
807
+ } ) ;
808
+ }
809
+ } ) ;
810
+ }
811
+
683
812
var $ = window . jQuery || window . $ ;
684
813
if ( $ && $ . fn && $ . fn . ready ) {
685
814
fill ( $ . fn , 'ready' , function ( orig ) {
@@ -952,6 +1081,12 @@ Raven.prototype = {
952
1081
// Send along our own collected metadata with extra
953
1082
data . extra [ 'session:duration' ] = now ( ) - this . _startTime ;
954
1083
1084
+ if ( this . _breadcrumbs && this . _breadcrumbs . length > 0 ) {
1085
+ data . breadcrumbs = {
1086
+ values : this . _breadcrumbs
1087
+ } ;
1088
+ }
1089
+
955
1090
// If there are no tags/extra, strip the key from the payload alltogther.
956
1091
if ( isEmptyObject ( data . tags ) ) delete data . tags ;
957
1092
@@ -1001,6 +1136,11 @@ Raven.prototype = {
1001
1136
auth . sentry_secret = this . _globalSecret ;
1002
1137
}
1003
1138
1139
+ this . captureBreadcrumb ( 'sentry' , {
1140
+ message : data . message ,
1141
+ eventId : data . event_id
1142
+ } ) ;
1143
+
1004
1144
var url = this . _globalEndpoint ;
1005
1145
( globalOptions . transport || this . _makeRequest ) . call ( this , {
1006
1146
url : url ,
0 commit comments