bugsnag-js
bugsnag-js copied to clipboard
Customising network breadcrumbs, or hooking Relay
Hi there, we have an application which is using Relay and GraphQL, there are cases where an HTTP request with a 200 response can contain an error message in addition to the data fetched by the response. The default network breadcrumb plugin only returns whether the response was a 200 or not.
Is there a good way we should hook this to get more information, or would a custom fork of the network breadcrumb plugin be the best approach?
Thanks!
I have ended up hooking into Relay elsewhere to produce more relevant breadcrumbs, but Iād love to see Bugsnag get better first-party support for it! šš¼
Hi @ticky - thanks for the report! Have you looked into customising these automatic breadcrumbs via the use of an onBreadcrumb
callback?
https://docs.bugsnag.com/platforms/javascript/customizing-breadcrumbs/#discarding-and-amending-breadcrumbs
Hi @phillipsam, I did look into that, but the onBreadcrumb
callback is not given any more information about the request/response than Bugsnag itself already sees; it only gets passed the couple of bits of metadata that the fetch
/XMLHttpRequest
shims extract into the fields defined, i.e. it gets passed { message: "fetch() succeeded", metadata: { status: 200, request: "GET /api" }, timestamp: Date.now(), type: "request" }
.
In order to add more useful information to it, I would need access to the underlying response object, which either means adding another monkey patch atop your monkey patch, or forking the network breadcrumb plugin.
Hi @ticky - thanks, we will look into how we can improve this to provide raw data relating to the breadcrumbs in the callback.
I wanted to add more info to existing network breadcrumbs based on the response information, and I was surprised that the onBreadcrumb
method did not receive any kind of event object alongside the breadcrumb, from which, based on the event type, we could have gathered more data.
Also, the fact that the network plugin is internal limits a lot how it can be configured or extended.
That being said, I went ahead and created some kind of (convoluted) plugin which:
- caches network breadcrumbs
- prevents them from being sent
- adds the data from its own fetch wrapper
- sends a new modified breadcrumb
Here it is, use at your own risks:
import type {Breadcrumb, Client} from '@bugsnag/core';
import {debounce} from 'lodash-es';
const identity = <T>(value: T) => value;
export const BREADCRUMB_TYPE: Breadcrumb['type'] = 'request';
const BREADCRUMB_MESSAGE_FILTER: ReadonlyArray<string> = [
'fetch() failed',
'fetch() succeeded',
];
const FALLBACK_DELAY = 1000;
export type OnResponseCallback = (
breadcrumb: Breadcrumb,
response: Response,
) => Breadcrumb;
/**
* Enables adding any metadata to a request breadcrumb based on the response.
*
* @see https://github.com/bugsnag/bugsnag-js/blob/ef3835776c0f95f0cdef0a200891e59367298147/packages/plugin-network-breadcrumbs/network-breadcrumbs.js
*/
export const createRequestBreadcrumbPlugin = ({
globalWindow = window,
onResponse = identity,
}: {
globalWindow?: Window;
onResponse?: OnResponseCallback;
}) => ({
load: (client: Client) => {
if (!('fetch' in globalWindow)) return;
const sendBreadcrumb = ({message, metadata, type}: Breadcrumb) => {
// A caveat of the bugsnag API is that we can't send an existing
// breadcrumb instance, which means we may get a slightly different
// timestamp when sending the updated breadcrumb.
client.leaveBreadcrumb(message, metadata, type);
};
const breadcrumbMap = new Map<string, Breadcrumb>();
/**
* As a safety net, if, for some reason, we never catch the response related
* to a request breadcrumb, we still want to make sure every breadcrumb
* makes it to the server.
*/
const sendUnhandledBreadcrumbs = debounce(() => {
for (const [_key, breadcrumb] of breadcrumbMap) {
sendBreadcrumb(breadcrumb);
}
}, FALLBACK_DELAY);
/**
* Intercepts requests breadcrumbs that are missing metadata.
*/
client.addOnBreadcrumb((breadcrumb) => {
const {message, metadata, type} = breadcrumb;
// Only handle completed fetch requests since incomplete requests do not
// have a request id.
if (
type !== BREADCRUMB_TYPE ||
!BREADCRUMB_MESSAGE_FILTER.includes(message)
) {
return;
}
const [_method, url]: string | undefined[] =
metadata.request?.split(' ') || [];
// If this breadcrumb has a different `request` metadata format, we'll
// just ignore it and let it through as-is.
if (!url) return;
// Some breadcrumbs have a relative URL, so we need to convert it to an
// absolute one which includes the origin in order to compare it with the
// request URL.
const urlKey =
url.at(0) === '/' ? `${window.location.origin}${url}` : url;
// We already processed this breadcrumb and it was re-sent with updated
// metadata, so we can remove it and let it through.
const existingBreadcrumb = breadcrumbMap.get(urlKey);
if (existingBreadcrumb) {
// Reassign the initial timestamp to the new breadcrumb to avoid messing
// with the breadcrumb order, which bypasses the caveat mentioned in `sendBreadcrumb`.
breadcrumb.timestamp = existingBreadcrumb.timestamp;
breadcrumbMap.delete(urlKey);
return;
}
// We need to wait for the response to get the request id.
breadcrumbMap.set(urlKey, breadcrumb);
// trigger the debounced fallback just in case we never get the response
sendUnhandledBreadcrumbs();
// Prevent the breadcrumb from being sent to the server.
// This also logs an info message to the console unfortunately.
return false;
});
const handleFetchSuccess = (response: Response) => {
const url = response.url;
const breadcrumb = breadcrumbMap.get(url);
if (!breadcrumb) return;
sendBreadcrumb(onResponse(breadcrumb, response));
};
// Hook ourselves into the fetch API, just like bugsnag does.
const oldFetch = globalWindow.fetch;
globalWindow.fetch = function fetch(...args) {
return oldFetch(...args).then((response) => {
handleFetchSuccess(response);
return response;
});
};
},
});
We use it like this:
const onResponse: OnResponseCallback = (breadcrumb, response) => {
const requestId = response.headers.get(REQUEST_ID_HEADER);
if (requestId) {
breadcrumb.metadata = {
...breadcrumb.metadata,
requestId,
};
}
return breadcrumb;
};
const plugin = createRequestBreadcrumbPlugin({onResponse});
It has some limitations which were not addressed since our needs were limited:
- only accounts for
fetch
requests, completely ignoringXMLHttpRequest
- only offers an
onResponse
callback right now, though it would be easy to add anonRequest
one - only calls
onResponse
for successful requests, fetch failures are ignored - it parses the breadcrumb metadata
request
string, which is unreliable on the long term, if the Bugsnag API ever changes, it could break this.
Other than that, it works really well for our use-case since the Bugsnag JS API doesn't offer any other way right now.