Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

215 changes: 45 additions & 170 deletions packages/plugin-network-breadcrumbs/network-breadcrumbs.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const BREADCRUMB_TYPE = 'request'

const includes = require('@bugsnag/core/lib/es-utils/includes')

/*
* Leaves breadcrumbs when network requests occur
*/
Expand All @@ -11,184 +9,63 @@ module.exports = (_ignoredUrls = [], win = window) => {
load: client => {
if (!client._isBreadcrumbTypeEnabled('request')) return

const ignoredUrls = [
client._config.endpoints.notify,
client._config.endpoints.sessions
].concat(_ignoredUrls)

monkeyPatchXMLHttpRequest()
monkeyPatchFetch()

// XMLHttpRequest monkey patch
function monkeyPatchXMLHttpRequest () {
if (!('addEventListener' in win.XMLHttpRequest.prototype) || !('WeakMap' in win)) return

const trackedRequests = new WeakMap()
const requestHandlers = new WeakMap()

const originalOpen = win.XMLHttpRequest.prototype.open
win.XMLHttpRequest.prototype.open = function open (method, url) {
// it's possible for `this` to be `undefined`, which is not a valid key for a WeakMap
if (this) {
trackedRequests.set(this, { method, url })
}
originalOpen.apply(this, arguments)
}

const originalSend = win.XMLHttpRequest.prototype.send
win.XMLHttpRequest.prototype.send = function send (body) {
const requestData = trackedRequests.get(this)
if (requestData) {
// if we have already setup listeners then this request instance is being reused,
// so we need to remove the listeners from the previous send
const listeners = requestHandlers.get(this)
if (listeners) {
this.removeEventListener('load', listeners.load)
this.removeEventListener('error', listeners.error)
}

const requestStart = new Date()
const error = () => handleXHRError(requestData.method, requestData.url, getDuration(requestStart))
const load = () => handleXHRLoad(requestData.method, requestData.url, this.status, getDuration(requestStart))

this.addEventListener('load', load)
this.addEventListener('error', error)
// it's possible for `this` to be `undefined`, which is not a valid key for a WeakMap
if (this) {
requestHandlers.set(this, { load, error })
}
}

originalSend.apply(this, arguments)
}

if (process.env.NODE_ENV !== 'production') {
restoreFunctions.push(() => {
win.XMLHttpRequest.prototype.open = originalOpen
win.XMLHttpRequest.prototype.send = originalSend
})
// Try to get existing request tracker
let requestTrackerPlugin = client.getPlugin('requestTracker')

// Auto-load request tracker if not present
if (!requestTrackerPlugin) {
try {
const { createRequestTrackerPlugin } = require('@bugsnag/request-tracker')
const trackerPlugin = createRequestTrackerPlugin(_ignoredUrls, win)
client._loadPlugin(trackerPlugin)
requestTrackerPlugin = client.getPlugin('requestTracker')
} catch (error) {
client._logger.warn('Failed to auto-load request tracker, falling back to direct monkey-patching:', error.message)
}
}

function handleXHRLoad (method, url, status, duration) {
if (url === undefined) {
client._logger.warn('The request URL is no longer present on this XMLHttpRequest. A breadcrumb cannot be left for this request.')
return
}

// an XMLHttpRequest's URL can be an object as long as its 'toString'
// returns a URL, e.g. a HTMLAnchorElement
if (typeof url === 'string' && includes(ignoredUrls, url.replace(/\?.*$/, ''))) {
// don't leave a network breadcrumb from bugsnag notify calls
return
}
const metadata = {
status,
method: String(method),
url: String(url),
duration: duration
}
if (status >= 400) {
// contacted server but got an error response
client.leaveBreadcrumb('XMLHttpRequest failed', metadata, BREADCRUMB_TYPE)
} else {
client.leaveBreadcrumb('XMLHttpRequest succeeded', metadata, BREADCRUMB_TYPE)
}
if (requestTrackerPlugin) {
return useSharedRequestTracker(requestTrackerPlugin)
}

function handleXHRError (method, url, duration) {
if (url === undefined) {
client._logger.warn('The request URL is no longer present on this XMLHttpRequest. A breadcrumb cannot be left for this request.')
return
}

if (typeof url === 'string' && includes(ignoredUrls, url.replace(/\?.*$/, ''))) {
// don't leave a network breadcrumb from bugsnag notify calls
return
}

// failed to contact server
client.leaveBreadcrumb('XMLHttpRequest error', {
method: String(method),
url: String(url),
duration: duration
}, BREADCRUMB_TYPE)
}

// window.fetch monkey patch
function monkeyPatchFetch () {
// only patch it if it exists and if it is not a polyfill (patching a polyfilled
// fetch() results in duplicate breadcrumbs for the same request because the
// implementation uses XMLHttpRequest which is also patched)
if (!('fetch' in win) || win.fetch.polyfill) return

const oldFetch = win.fetch
win.fetch = function fetch () {
const urlOrRequest = arguments[0]
const options = arguments[1]

let method
let url = null

if (urlOrRequest && typeof urlOrRequest === 'object') {
url = urlOrRequest.url
if (options && 'method' in options) {
method = options.method
} else if (urlOrRequest && 'method' in urlOrRequest) {
method = urlOrRequest.method
}
} else {
url = urlOrRequest
if (options && 'method' in options) {
method = options.method
function useSharedRequestTracker (trackerPlugin) {
const { fetchTracker, xhrTracker, urlFilter, getDuration } = trackerPlugin

const handleRequest = (startContext) => {
if (urlFilter(startContext.url)) return

return {
onRequestEnd: (response) => {
const duration = getDuration(startContext.startTime)
const metadata = {
method: startContext.method,
status: response.status,
url: startContext.url,
duration
}

const request = startContext.type === 'fetch' ? 'fetch()' : 'XMLHttpRequest'

if (response.state === 'error') {
client.leaveBreadcrumb(`${request} error`, { method: startContext.method, url: startContext.url, duration }, BREADCRUMB_TYPE)
} else if (response.status >= 400) {
client.leaveBreadcrumb(`${request} failed`, metadata, BREADCRUMB_TYPE)
} else {
client.leaveBreadcrumb(`${request} succeeded`, metadata, BREADCRUMB_TYPE)
}
}
}

if (method === undefined) {
method = 'GET'
}

return new Promise((resolve, reject) => {
const requestStart = new Date()

// pass through to native fetch
oldFetch(...arguments)
.then(response => {
handleFetchSuccess(response, method, url, getDuration(requestStart))
resolve(response)
})
.catch(error => {
handleFetchError(method, url, getDuration(requestStart))
reject(error)
})
})
}

if (process.env.NODE_ENV !== 'production') {
restoreFunctions.push(() => {
win.fetch = oldFetch
})
}
}

const handleFetchSuccess = (response, method, url, duration) => {
const metadata = {
method: String(method),
status: response.status,
url: String(url),
duration: duration
if (fetchTracker) {
fetchTracker.onStart(handleRequest)
restoreFunctions.push(fetchTracker._restore)
}
if (response.status >= 400) {
// when the request comes back with a 4xx or 5xx status it does not reject the fetch promise,
client.leaveBreadcrumb('fetch() failed', metadata, BREADCRUMB_TYPE)
} else {
client.leaveBreadcrumb('fetch() succeeded', metadata, BREADCRUMB_TYPE)
if (xhrTracker) {
xhrTracker.onStart(handleRequest)
restoreFunctions.push(xhrTracker._restore)
}
}

const handleFetchError = (method, url, duration) => {
client.leaveBreadcrumb('fetch() error', { method: String(method), url: String(url), duration: duration }, BREADCRUMB_TYPE)
}
}
}

Expand All @@ -201,5 +78,3 @@ module.exports = (_ignoredUrls = [], win = window) => {

return plugin
}

const getDuration = (startTime) => startTime && new Date() - startTime
3 changes: 3 additions & 0 deletions packages/plugin-network-breadcrumbs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"devDependencies": {
"@bugsnag/core": "^8.6.0"
},
"dependencies": {
"@bugsnag/request-tracker": "^8.0.0"
},
"peerDependencies": {
"@bugsnag/core": "^8.0.0"
}
Expand Down
3 changes: 2 additions & 1 deletion packages/request-tracker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"access": "public"
},
"files": [
"*.js"
"*.js",
"lib/"
],
"author": "Bugsnag",
"license": "MIT",
Expand Down
3 changes: 2 additions & 1 deletion scripts/generate-react-native-fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const replacementFilesDir = resolve(ROOT_DIR, 'test/react-native/features/fixtur
const INTERNAL_DEPENDENCIES = [
'@bugsnag/react-native',
'@bugsnag/plugin-react-navigation',
'@bugsnag/plugin-react-native-navigation'
'@bugsnag/plugin-react-native-navigation',
'@bugsnag/request-tracker'
]

// make sure we install a compatible versions of peer dependencies
Expand Down