Skip to content

Commit

Permalink
feat: add a fingerprint to serialized errors
Browse files Browse the repository at this point in the history
This helps us uniquely identify errors and gives us another data point
to group errors by without resorting to very complicated Splunk logic.
We're intending on using this fingerprint to help Customer Care and
Operations more quickly direct us to the root cause of a problem.

You will also be able to more easily see the common errors that your
application throws if you're using the Reliability Kit logging
middleware.
  • Loading branch information
rowanmanning committed Nov 14, 2023
1 parent 564d950 commit fcf1686
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function cleanLogFortesting(log) {

// The error stack properties are not comparable, and won't be
// consistent between machines or local vs CI
delete log.error?.fingerprint;
delete log.error?.stack;
delete log.error_stack;
delete log.nestedError?.stack;
Expand Down
9 changes: 9 additions & 0 deletions packages/serialize-error/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ A utility function to serialize an error object in a way that's friendly to logg
* [Usage](#usage)
* [`serializeError`](#serializeerror)
* [`SerializedError` type](#serializederror-type)
* [`SerializedError.fingerprint`](#serializederrorfingerprint)
* [`SerializedError.name`](#serializederrorname)
* [`SerializedError.code`](#serializederrorcode)
* [`SerializedError.message`](#serializederrormessage)
Expand Down Expand Up @@ -42,6 +43,7 @@ The `serializeError` function accepts an error-like object (e.g. an instance of
```js
serializeError(new Error('example message'));
// {
// fingerprint: '...',
// name: 'Error',
// code: 'UNKNOWN',
// message: 'An error occurred',
Expand All @@ -66,6 +68,13 @@ serializeError({

The `SerializedError` type documents the return value of the [`serializeError` function](#serializeerror). It will always have the following properties, extracting them from a given error object.

#### `SerializedError.fingerprint`

This is a hash of the first part of the error stack, used to help group errors that occurred in the same part of the codebase. The fingerprint is `null` if the error does not include a stack trace.

> **Warning**
> Do not rely on the format or length of the error fingerprint as the underlying hash may change without warning. You _can_ rely on the fingerprint being unique to the type of error being thrown.
#### `SerializedError.name`

This is extracted from the `error.name` property and is always cast to a `String`. It defaults to `"Error"`.
Expand Down
15 changes: 15 additions & 0 deletions packages/serialize-error/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const crypto = require('node:crypto');

/**
* @typedef {object} SerializedError
* @property {string | null} fingerprint
* A hash of the first part of the error stack, used to help group errors that occurred in
* the same part of the codebase. The fingerprint is null if the error does not include a
* stack trace.
* @property {string} name
* The name of the class that the error is an instance of.
* @property {string} code
Expand Down Expand Up @@ -69,6 +75,14 @@ function serializeError(error) {
// Only include error stack if it's a string
if (typeof error.stack === 'string') {
errorProperties.stack = error.stack;

// Calculate the error fingerprint
const errorStackLines = error.stack.split(/[\r\n]+/);
const errorStackHeader = errorStackLines.slice(0, 2).join('\n');
errorProperties.fingerprint = crypto
.createHash('md5')
.update(errorStackHeader)
.digest('hex');
}

// If set, cast the error status code to a number
Expand Down Expand Up @@ -101,6 +115,7 @@ function createSerializedError(properties) {
return Object.assign(
{},
{
fingerprint: null,
name: 'Error',
code: 'UNKNOWN',
message: 'An error occurred',
Expand Down
7 changes: 7 additions & 0 deletions packages/serialize-error/test/unit/lib/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ describe('@dotcom-reliability-kit/serialize-error', () => {

beforeEach(() => {
error = new Error('mock message');
error.stack = 'Error: mock message\nmock line 1\nmock line 2';
});

it('returns the expected serialized error properties', () => {
expect(serializeError(error)).toMatchObject({
// MD5 hash of the first two lines of the mock error stack above
fingerprint: '9b5df16d105739b263ecfbf4e8a31f95',
name: 'Error',
code: 'UNKNOWN',
message: 'mock message',
Expand Down Expand Up @@ -183,6 +186,7 @@ describe('@dotcom-reliability-kit/serialize-error', () => {

it('returns the expected serialized error properties', () => {
expect(serializeError(error)).toMatchObject({
fingerprint: null,
name: 'Error',
code: 'UNKNOWN',
message: 'An error occurred',
Expand Down Expand Up @@ -300,6 +304,7 @@ describe('@dotcom-reliability-kit/serialize-error', () => {
it('returns the expected serialized error properties', () => {
const error = 'mock message';
expect(serializeError(error)).toMatchObject({
fingerprint: null,
name: 'Error',
code: 'UNKNOWN',
message: 'mock message',
Expand All @@ -317,6 +322,7 @@ describe('@dotcom-reliability-kit/serialize-error', () => {
it('returns the expected serialized error properties', () => {
const error = 123;
expect(serializeError(error)).toMatchObject({
fingerprint: null,
name: 'Error',
code: 'UNKNOWN',
message: '123',
Expand All @@ -334,6 +340,7 @@ describe('@dotcom-reliability-kit/serialize-error', () => {
it('returns the expected serialized error properties', () => {
const error = ['mock', 'message'];
expect(serializeError(error)).toMatchObject({
fingerprint: null,
name: 'Error',
code: 'UNKNOWN',
message: 'mock,message',
Expand Down

0 comments on commit fcf1686

Please sign in to comment.