11const BREADCRUMB_TYPE = 'request'
22
3- const includes = require ( '@bugsnag/core/lib/es-utils/includes' )
4-
53/*
64 * Leaves breadcrumbs when network requests occur
75 */
@@ -11,184 +9,63 @@ module.exports = (_ignoredUrls = [], win = window) => {
119 load : client => {
1210 if ( ! client . _isBreadcrumbTypeEnabled ( 'request' ) ) return
1311
14- const ignoredUrls = [
15- client . _config . endpoints . notify ,
16- client . _config . endpoints . sessions
17- ] . concat ( _ignoredUrls )
18-
19- monkeyPatchXMLHttpRequest ( )
20- monkeyPatchFetch ( )
21-
22- // XMLHttpRequest monkey patch
23- function monkeyPatchXMLHttpRequest ( ) {
24- if ( ! ( 'addEventListener' in win . XMLHttpRequest . prototype ) || ! ( 'WeakMap' in win ) ) return
25-
26- const trackedRequests = new WeakMap ( )
27- const requestHandlers = new WeakMap ( )
28-
29- const originalOpen = win . XMLHttpRequest . prototype . open
30- win . XMLHttpRequest . prototype . open = function open ( method , url ) {
31- // it's possible for `this` to be `undefined`, which is not a valid key for a WeakMap
32- if ( this ) {
33- trackedRequests . set ( this , { method, url } )
34- }
35- originalOpen . apply ( this , arguments )
36- }
37-
38- const originalSend = win . XMLHttpRequest . prototype . send
39- win . XMLHttpRequest . prototype . send = function send ( body ) {
40- const requestData = trackedRequests . get ( this )
41- if ( requestData ) {
42- // if we have already setup listeners then this request instance is being reused,
43- // so we need to remove the listeners from the previous send
44- const listeners = requestHandlers . get ( this )
45- if ( listeners ) {
46- this . removeEventListener ( 'load' , listeners . load )
47- this . removeEventListener ( 'error' , listeners . error )
48- }
49-
50- const requestStart = new Date ( )
51- const error = ( ) => handleXHRError ( requestData . method , requestData . url , getDuration ( requestStart ) )
52- const load = ( ) => handleXHRLoad ( requestData . method , requestData . url , this . status , getDuration ( requestStart ) )
53-
54- this . addEventListener ( 'load' , load )
55- this . addEventListener ( 'error' , error )
56- // it's possible for `this` to be `undefined`, which is not a valid key for a WeakMap
57- if ( this ) {
58- requestHandlers . set ( this , { load, error } )
59- }
60- }
61-
62- originalSend . apply ( this , arguments )
63- }
64-
65- if ( process . env . NODE_ENV !== 'production' ) {
66- restoreFunctions . push ( ( ) => {
67- win . XMLHttpRequest . prototype . open = originalOpen
68- win . XMLHttpRequest . prototype . send = originalSend
69- } )
12+ // Try to get existing request tracker
13+ let requestTrackerPlugin = client . getPlugin ( 'requestTracker' )
14+
15+ // Auto-load request tracker if not present
16+ if ( ! requestTrackerPlugin ) {
17+ try {
18+ const { createRequestTrackerPlugin } = require ( '@bugsnag/request-tracker' )
19+ const trackerPlugin = createRequestTrackerPlugin ( _ignoredUrls , win )
20+ client . _loadPlugin ( trackerPlugin )
21+ requestTrackerPlugin = client . getPlugin ( 'requestTracker' )
22+ } catch ( error ) {
23+ client . _logger . warn ( 'Failed to auto-load request tracker, falling back to direct monkey-patching:' , error . message )
7024 }
7125 }
7226
73- function handleXHRLoad ( method , url , status , duration ) {
74- if ( url === undefined ) {
75- client . _logger . warn ( 'The request URL is no longer present on this XMLHttpRequest. A breadcrumb cannot be left for this request.' )
76- return
77- }
78-
79- // an XMLHttpRequest's URL can be an object as long as its 'toString'
80- // returns a URL, e.g. a HTMLAnchorElement
81- if ( typeof url === 'string' && includes ( ignoredUrls , url . replace ( / \? .* $ / , '' ) ) ) {
82- // don't leave a network breadcrumb from bugsnag notify calls
83- return
84- }
85- const metadata = {
86- status,
87- method : String ( method ) ,
88- url : String ( url ) ,
89- duration : duration
90- }
91- if ( status >= 400 ) {
92- // contacted server but got an error response
93- client . leaveBreadcrumb ( 'XMLHttpRequest failed' , metadata , BREADCRUMB_TYPE )
94- } else {
95- client . leaveBreadcrumb ( 'XMLHttpRequest succeeded' , metadata , BREADCRUMB_TYPE )
96- }
27+ if ( requestTrackerPlugin ) {
28+ return useSharedRequestTracker ( requestTrackerPlugin )
9729 }
9830
99- function handleXHRError ( method , url , duration ) {
100- if ( url === undefined ) {
101- client . _logger . warn ( 'The request URL is no longer present on this XMLHttpRequest. A breadcrumb cannot be left for this request.' )
102- return
103- }
104-
105- if ( typeof url === 'string' && includes ( ignoredUrls , url . replace ( / \? .* $ / , '' ) ) ) {
106- // don't leave a network breadcrumb from bugsnag notify calls
107- return
108- }
109-
110- // failed to contact server
111- client . leaveBreadcrumb ( 'XMLHttpRequest error' , {
112- method : String ( method ) ,
113- url : String ( url ) ,
114- duration : duration
115- } , BREADCRUMB_TYPE )
116- }
117-
118- // window.fetch monkey patch
119- function monkeyPatchFetch ( ) {
120- // only patch it if it exists and if it is not a polyfill (patching a polyfilled
121- // fetch() results in duplicate breadcrumbs for the same request because the
122- // implementation uses XMLHttpRequest which is also patched)
123- if ( ! ( 'fetch' in win ) || win . fetch . polyfill ) return
124-
125- const oldFetch = win . fetch
126- win . fetch = function fetch ( ) {
127- const urlOrRequest = arguments [ 0 ]
128- const options = arguments [ 1 ]
129-
130- let method
131- let url = null
132-
133- if ( urlOrRequest && typeof urlOrRequest === 'object' ) {
134- url = urlOrRequest . url
135- if ( options && 'method' in options ) {
136- method = options . method
137- } else if ( urlOrRequest && 'method' in urlOrRequest ) {
138- method = urlOrRequest . method
139- }
140- } else {
141- url = urlOrRequest
142- if ( options && 'method' in options ) {
143- method = options . method
31+ function useSharedRequestTracker ( trackerPlugin ) {
32+ const { fetchTracker, xhrTracker, urlFilter, getDuration } = trackerPlugin
33+
34+ const handleRequest = ( startContext ) => {
35+ if ( urlFilter ( startContext . url ) ) return
36+
37+ return {
38+ onRequestEnd : ( response ) => {
39+ const duration = getDuration ( startContext . startTime )
40+ const metadata = {
41+ method : startContext . method ,
42+ status : response . status ,
43+ url : startContext . url ,
44+ duration
45+ }
46+
47+ const request = startContext . type === 'fetch' ? 'fetch()' : 'XMLHttpRequest'
48+
49+ if ( response . status >= 400 ) {
50+ client . leaveBreadcrumb ( `${ request } failed` , metadata , BREADCRUMB_TYPE )
51+ } else if ( response . state === 'error' ) {
52+ client . leaveBreadcrumb ( `${ request } error` , metadata , BREADCRUMB_TYPE )
53+ } else {
54+ client . leaveBreadcrumb ( `${ request } succeeded` , metadata , BREADCRUMB_TYPE )
55+ }
14456 }
14557 }
146-
147- if ( method === undefined ) {
148- method = 'GET'
149- }
150-
151- return new Promise ( ( resolve , reject ) => {
152- const requestStart = new Date ( )
153-
154- // pass through to native fetch
155- oldFetch ( ...arguments )
156- . then ( response => {
157- handleFetchSuccess ( response , method , url , getDuration ( requestStart ) )
158- resolve ( response )
159- } )
160- . catch ( error => {
161- handleFetchError ( method , url , getDuration ( requestStart ) )
162- reject ( error )
163- } )
164- } )
165- }
166-
167- if ( process . env . NODE_ENV !== 'production' ) {
168- restoreFunctions . push ( ( ) => {
169- win . fetch = oldFetch
170- } )
17158 }
172- }
17359
174- const handleFetchSuccess = ( response , method , url , duration ) => {
175- const metadata = {
176- method : String ( method ) ,
177- status : response . status ,
178- url : String ( url ) ,
179- duration : duration
60+ if ( fetchTracker ) {
61+ fetchTracker . onStart ( handleRequest )
62+ restoreFunctions . push ( fetchTracker . _restore )
18063 }
181- if ( response . status >= 400 ) {
182- // when the request comes back with a 4xx or 5xx status it does not reject the fetch promise,
183- client . leaveBreadcrumb ( 'fetch() failed' , metadata , BREADCRUMB_TYPE )
184- } else {
185- client . leaveBreadcrumb ( 'fetch() succeeded' , metadata , BREADCRUMB_TYPE )
64+ if ( xhrTracker ) {
65+ xhrTracker . onStart ( handleRequest )
66+ restoreFunctions . push ( xhrTracker . _restore )
18667 }
18768 }
188-
189- const handleFetchError = ( method , url , duration ) => {
190- client . leaveBreadcrumb ( 'fetch() error' , { method : String ( method ) , url : String ( url ) , duration : duration } , BREADCRUMB_TYPE )
191- }
19269 }
19370 }
19471
@@ -201,5 +78,3 @@ module.exports = (_ignoredUrls = [], win = window) => {
20178
20279 return plugin
20380}
204-
205- const getDuration = ( startTime ) => startTime && new Date ( ) - startTime
0 commit comments