This is a rebuilt version of the bugsnag-js client with the following goals:
- Reduced bundle size
- Support for non-main thread contexts (e.g. Web workers)
It does this using the following approach:
- Dropping support for older browsers including IE
- Leaning heavily on the plugin approach — every feature is a plugin. As a result unused features are tree-shaked and don't affect your bundle size
- Being written entirely in TypeScript - by doing more checking at build time we can drop some runtime checks
It doesn't include support for quite a number of features simply because we haven't found a need for them yet. Some noteable ones include:
- Sessions
- Inline scripts (these days CSP generally makes inline scripts harder to use anyway)
- Loggers
- RegExps for redacted keys
- Callbacks for breadcrumbs
- User IP collection (we prefer to respect user privacy)
Many of these could be added, if needed, by adding further plugins.
On the other hand, it adds a few other features:
- The ability to substitute in custom delivery providers (e.g. so you can send to an SNS topic).
-
Bugsnag.notify()
returns a Promise so you can wait on it to ensure delivery was successful. -
Bugsnag.notify()
can takemetadata
andseverity
settings as a object rather than you having to provide an on-error callback (see below). - If an
Error
object has ametadata
field, it will be merged into the reported error's metadata. - A
browserHandledRejectionBreadcrumbs
plugin for logging handled rejections. - Post-error callbacks - called after fully preparing the error but just before sending it. This was mostly added as a means of supporting "error" breadcrumbs.
-
redactKeys
exports its functions so you can re-use them for other logging etc.
So far very little effort has been spent on optimizing the code size of the generated code. A little code golf and manual minification could likely reduce the bundle size much further still.
The easiest way to use this for an existing installation is to use the legacy
config helper, fromLegacyConfig
. However, note that doing this will produce
much less significant code savings since much less code can be tree-shaken.
Better still is to manually configure the plugins one-by-one according to your needs. For example,
import Bugsnag, {
appDuration,
browserContext,
browserHandledRejectionBreadcrumbs,
browserNotifyUnhandledExceptions,
browserNotifyUnhandledRejections,
consoleBreadcrumbs,
deviceOrientation,
errorBreadcrumbs,
fetchBreadcrumbs,
interactionBreadcrumbs,
limitEvents,
navigationBreadcrumbs,
ReactPlugin,
redactKeys,
stringifyValues,
} from '@birchill/bugsnag-zero';
const plugins = [
appDuration,
browserContext,
browserHandledRejectionBreadcrumbs,
browserNotifyUnhandledExceptions,
browserNotifyUnhandledRejections,
deviceOrientation,
errorBreadcrumbs,
fetchBreadcrumbs,
interactionBreadcrumbs,
limitEvents(10),
navigationBreadcrumbs,
ReactPlugin,
redactKeys(['accessToken', 'password']),
stringifyValues,
];
if (__RELEASE_STAGE__ !== 'test') {
plugins.push(consoleBreadcrumbs);
}
Bugsnag.start({
apiKey: '<apiKey>',
appType: 'browser',
collectUserIp: false,
enabledReleaseStages: ['prod', 'beta'],
plugins,
releaseStage: __RELEASE_STAGE__,
});
There are a few API differences from the official client. Hopefully the TypeScript interfaces make these more obvious.
For example, we've often found it useful to set the severity of an error while notifying. The official client requires supplying a callback to do this but this module allows specifying it as a property on the second argument to the notify callback:
// bugsnag-js
Bugsnag.notify(message, (event) => {
event.severity = 'warning';
});
// @birchill/bugsnag-zero
Bugsnag.notify(message, { severity: 'warning' });
Similarly, it is possible to specify metadata
directly on this second
argument.
It is, of course, still possible to pass an error callback as per the official client's API.
In order to allow using Preact with the React plugin and to avoid introducing a dependency on React itself just for its types, the typings for the React plugin are a bit convoluted.
For Preact something like the following is needed:
const MyBugsnagErrorBoundary = React.useMemo(
() =>
Bugsnag.getPlugin('react')!.createErrorBoundary<
typeof React.Component,
ComponentType,
VNode
>(React.Component, React.createElement),
[]
);
Furthermore, unlike the official bugsnag-js client, we don't allow passing in
React to the constructor. Instead we always require a call to
createErrorBoundary
.
We don't properly support Node.js at this time. In particular, there's no delivery mechanism defined for it. It would be trivial to write, but we haven't needed it yet.
That said, there are some plugins that should work with node including the
lambdaContext
plugin for logging from AWS Lambda.
At Birchill, we use a custom Delivery
class to post the events to an SNS topic
and have that send to Bugsnag since that's faster than having the Lambda wait on
the Bugsnag server and more flexible too (e.g. you can post your events to Slack
etc. too as needed).
The setup looks something like:
import Bugsnag, {
appDuration,
errorBreadcrumbs,
lambdaContext,
nodeNotifyUnhandledExceptions,
nodeNotifyUnhandledRejections,
redactKeys,
} from '@birchill/bugsnag-zero';
import { lambdaContext } from '@birchill/bugsnag-zero/lambda-context';
Bugsnag.start({
apiKey: '<unused>',
appType: 'nodejs',
plugins: [
appDuration,
errorBreadcrumbs,
lambdaContext(),
nodeNotifyUnhandledExceptions,
nodeNotifyUnhandledRejections,
redactKeys(keysToRedact),
],
});
Bugsnag.setDelivery({
sendEvent: async ({ events }): Promise<void> => {
const errorClass = events[0].exceptions[0]?.errorClass || 'Unknown';
const context = events[0].context;
const subject = `Error: ${errorClass} in ${context}`;
const publishCommand = new PublishCommand({
Subject: subject,
TopicArn: errorTopicArn,
Message: JSON.stringify(events),
});
await snsClient.send(publishCommand);
},
});
// When a Lambda handler is called, update the lambdaContext plugin:
async function handler(event: Event, context: Context): Promise<void> {
Bugsnag.getPlugin('lambdaContext')?.setContext(event, context);
}
Bugsnag's v5 reporting API requires passing in the browser name, browser version, OS name etc. explicitly. In other words, it requires you to parse the user agent string on the client.
By comparison, the v4 API that the official client uses just passes the user agent string to the API and lets the server parse it.
Adding a full-blown user agent string parser would bloat this library a lot so we provide a very simple one that covers the basic cases. For example, it doesn't handle things like bots etc. since hopefully they're probably not going to be triggering your error reporting (and if they are, the raw user agent string is still included so you can detect that).
However, perhaps your app already has a user agent string parser included and
you want to re-use that? You can do that by using the
browserContextWithUaParser
plugin in place of the browserContext
plugin and
supplying a function that takes a string and returns an object of the following
shape:
type UserAgentInfo = {
browserName?: string;
browserVersion?: string;
osName?: string;
osVersion?: string;
manufacturer?: string;
model?: string;
modelNumber?: string;
};
For an unrecognized user agent string, you would just return an empty object
({}
).
For example:
import Bugsnag, { browserContextWithUaParser } from '@birchill/bugsnag-zero';
const myUaParser = new Parser();
const parseUaString = (uaString: string) => {
const result = myUaParser.parse(uaString);
return result
? {
browserName: result.browser,
browserVersion: `${result.major}.${result.minor}`,
}
: {};
};
Bugsnag.start({
apiKey: '<unused>',
appType: 'nodejs',
plugins: [browserContextWithUaParser(parseUaString)],
});
This also gives you full control on how browsers are grouped together (e.g. do you want Chrome on iOS to be treated the same as real Chrome? Do you want an EdgeHTML version of Edge to be grouped together with Chromium Edge?)
Similarly, if you have no need for user agent string parsing and want to use
browserContext
without the user agent string parsing bloating your code, the
following should hopefully mean it gets tree-shaken out:
import Bugsnag, { browserContextWithUaParser } from '@birchill/bugsnag-zero';
Bugsnag.start({
apiKey: '<unused>',
appType: 'nodejs',
plugins: [browserContextWithUaParser(() => {})],
});
Note that none of this is tested at all so let me know if it doesn't work.
yarn build
yarn release
git push --follow-tags
Hopefully GitHub Actions will take care of publishing the release.
(Note that it's going to default to applying the latest
tag so if we ever need
to publish an update to an older version we'll need to do it manually.)