Skip to content

Commit f4310e0

Browse files
committed
use shared tracker in network breadcrumbs package
1 parent 9d447fd commit f4310e0

3 files changed

Lines changed: 53 additions & 171 deletions

File tree

package-lock.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 45 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
const 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

packages/plugin-network-breadcrumbs/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"devDependencies": {
2020
"@bugsnag/core": "^8.6.0"
2121
},
22+
"dependencies": {
23+
"@bugsnag/request-tracker": "^8.0.0"
24+
},
2225
"peerDependencies": {
2326
"@bugsnag/core": "^8.0.0"
2427
}

0 commit comments

Comments
 (0)