diff --git a/docs/integrations/honeybadger.md b/docs/integrations/honeybadger.md index 7e47570d..662734ac 100644 --- a/docs/integrations/honeybadger.md +++ b/docs/integrations/honeybadger.md @@ -17,7 +17,12 @@ Just setting the env var in config will suffice. You can use orka's logger see [logs](https://workable.github.io/orka/logs) to log errors to HB ```js -logger.error(error, 'augment error message', { customKey: {}, action: 'action', component: 'logger-category' }); +logger.error(error, 'augment error message', { customKey: {}, action: 'action', component: 'logger-category', tags: ['tag'] }); ``` any custom keys given will be added to context sent to HB. + +You can add tags in 3 ways: + - The error has a tags property + - You pass tags like above + - You add honeybadgerTags key with array of tags in (requestContext)[https://github.com/Workable/orka/blob/b76ca8da9fbc87aa2368f8fe3338d7cfdccac64d/docs/request-context.md?plain=1#L135] diff --git a/src/initializers/log4js/honeybadger-appender.ts b/src/initializers/log4js/honeybadger-appender.ts index ad6dbe7c..13129eae 100644 --- a/src/initializers/log4js/honeybadger-appender.ts +++ b/src/initializers/log4js/honeybadger-appender.ts @@ -1,62 +1,77 @@ import * as Honeybadger from '@honeybadger-io/js'; +import { getRequestContext } from '../../builder'; const Levels = require('log4js/lib/levels'); const log4jsErrorLevel = Levels.ERROR.level; function notifyHoneybadger(categoryName, error, ...rest) { + if (!error) return; + error = buildError(error, rest); + + let context = buildContext(rest); + let payload = buildPayload(categoryName, error, context); + + Honeybadger.notify(error, payload); +} + +function buildError(error: Error | string, rest: any[]) { if (typeof error === 'string') { error = new Error(error); } - if (!error) { - return; - } + const message = rest.filter(r => typeof r === 'string').join('. '); + if (message) error.message = `${error.message}. ${message}`; - let context = {} as any; - let message = error.message; - rest.forEach(r => { - if (typeof r === 'string') { - message += `. ${r}`; - } else { - Object.assign(context, r); - } - }); + return error; +} +function buildContext(rest: any[]) { + return rest.filter(r => typeof r !== 'string').reduce((acc, r) => Object.assign(acc, r), {}); +} + +function buildPayload(categoryName, error, context) { let actionFallback = context.action; - let componentFallback = context.component; + let componentFallback = context.component || categoryName; + let tags: string[] = context.tags || []; delete context.action; delete context.component; + delete context.tags; - let { headers = {}, action = actionFallback, component = componentFallback, params = {}, name } = error; + let { headers = {}, params = {} } = error; + error.action ||= actionFallback; + error.component ||= componentFallback; Object.assign(context, error.context); + Object.assign(headers, context.headers); + Object.assign(params, context.params); + tags = tags.concat(error.tags || []); + tags = tags.concat(getRequestContext()?.get('honeybadgerTags') || []); - const computedComponent = component || categoryName; - - const fingerprint = context.fingerprint || generateFingerprint(name, computedComponent, action) || categoryName; + const fingerprint = generateFingerprint(categoryName, error, context); delete context.fingerprint; - Honeybadger.notify( - { stack: error.stack, message, name }, - { - context, - headers, - cgiData: { - 'server-software': `Node ${process.version}` - }, - action, - component: computedComponent, - params, - fingerprint - } - ); + return { + context, + headers, + cgiData: { + 'server-software': `Node ${process.version}` + }, + action: error.action, + component: error.component, + params, + tags, + fingerprint + }; } -const generateFingerprint = (name, component, action) => { - if (!action || !component) { - return; - } +const generateFingerprint = (categoryName, error, context) => { + if (context.fingerprint) return context.fingerprint; + const action = error.action; + const component = error.component; + const name = error.name; + + if (!action || !component) return categoryName; if (name && name !== 'Error') { return `${name}_${component}_${action}`; diff --git a/test/examples/json-appender.test.ts b/test/examples/json-appender.test.ts index f0efd515..aa3b139e 100644 --- a/test/examples/json-appender.test.ts +++ b/test/examples/json-appender.test.ts @@ -120,7 +120,7 @@ describe('json-appender', function () { text.should.eql('default body'); const cleanStack = msg => { const stack = JSON.parse(msg); - stack.stack_trace = stack.stack_trace.substring(0, 31); + stack.stack_trace = stack.stack_trace?.substring(0, 31); return stack; }; logSpy.args @@ -134,35 +134,35 @@ describe('json-appender', function () { message: 'test - this was a test warning', stack_trace: 'Error: test\n at /logWarning ', context: pickBy({ - propagatedHeaders: { 'x-orka-request-id': 'test-id' }, - requestId: 'test-id', - context: 'foo' + propagatedHeaders: { 'x-orka-request-id': 'test-id' }, + requestId: 'test-id', + context: 'foo' }) - } - ], - [ - { - timestamp: '2019-01-01T00:00:00.000Z', - severity: 'ERROR', - categoryName: 'orka.errorHandler', - message: 'test', - stack_trace: 'Error: test\n at /logWarning ', - context: pickBy( + } + ], + [ { - expose: false, - statusCode: 505, - status: 505, - component: 'koa', - action: '/logWarning', - params: { path: {}, query: {}, body: {}, requestId: 'test-id' }, - propagatedHeaders: { 'x-orka-request-id': 'test-id' }, - state: { riviereStartedAt: 1546300800000, requestId: 'test-id' }, - requestId: 'test-id' - }, - _ => _ !== undefined - ) - } - ] - ]); + timestamp: '2019-01-01T00:00:00.000Z', + severity: 'ERROR', + categoryName: 'orka.errorHandler', + message: 'test', + stack_trace: 'Error: test\n at /logWarning ', + context: pickBy( + { + expose: false, + statusCode: 505, + status: 505, + component: 'koa', + action: '/logWarning', + params: { path: {}, query: {}, body: {}, requestId: 'test-id' }, + propagatedHeaders: { 'x-orka-request-id': 'test-id' }, + state: { riviereStartedAt: 1546300800000, requestId: 'test-id' }, + requestId: 'test-id' + }, + _ => _ !== undefined + ) + } + ] + ]); }); }); diff --git a/test/initializers/log4js/honeybadger-appender.test.ts b/test/initializers/log4js/honeybadger-appender.test.ts index cc3d0912..5b3414f3 100644 --- a/test/initializers/log4js/honeybadger-appender.test.ts +++ b/test/initializers/log4js/honeybadger-appender.test.ts @@ -2,6 +2,7 @@ import * as appender from '../../../src/initializers/log4js/honeybadger-appender import 'should'; import * as Honeybadger from '@honeybadger-io/js'; import * as sinon from 'sinon'; +import { runWithContext } from '../../../src/builder'; const sandbox = sinon.createSandbox(); @@ -46,6 +47,7 @@ describe('log4js_honeybadger_appender', () => { action: undefined, component: 'testCategoryName', params: {}, + tags: [], fingerprint: 'testCategoryName' }); }); @@ -82,6 +84,7 @@ describe('log4js_honeybadger_appender', () => { action: '/test/endpoint', component: 'testCategoryName', params: {}, + tags: [], fingerprint: 'testCategoryName_/test/endpoint' }); }); @@ -135,7 +138,7 @@ describe('log4js_honeybadger_appender', () => { level: 40000 }, categoryName: 'testCategoryName', - data: [err, {fingerprint: 'CustomError'}] + data: [err, { fingerprint: 'CustomError' }] }); notifySpy.callCount.should.equal(1); notifySpy.args[0][1].fingerprint.should.equal('CustomError'); @@ -161,44 +164,49 @@ describe('log4js_honeybadger_appender', () => { it('should assign any additional json values to the context', () => { const err = new Error('omg') as any; - err.status = 200; - err.component = 'testController'; - err.action = '/test/endpoint'; - err.context = { - something: 'ok' - }; - appender.configure()({ - level: { - level: 40000 - }, - categoryName: 'testCategoryName', - data: [ - err, - { + runWithContext(new Map([['honeybadgerTags', ['context-tag']]]), () => { + err.status = 200; + err.tags = ['error-tag']; + err.component = 'testController'; + err.action = '/test/endpoint'; + err.context = { + something: 'ok' + }; + appender.configure()({ + level: { + level: 40000 + }, + categoryName: 'testCategoryName', + data: [ + err, + { + a: 'aOK', + b: 'bOK' + }, + { + c: 'cOK' + }, + { tags: ['tag3'] } + ] + }); + notifySpy.callCount.should.equal(1); + notifySpy.args[0][1].should.eql({ + context: { a: 'aOK', - b: 'bOK' + b: 'bOK', + c: 'cOK', + something: 'ok' }, - { - c: 'cOK' - } - ] - }); - notifySpy.callCount.should.equal(1); - notifySpy.args[0][1].should.eql({ - context: { - a: 'aOK', - b: 'bOK', - c: 'cOK', - something: 'ok' - }, - headers: {}, - cgiData: { - 'server-software': `Node ${process.version}` - }, - action: '/test/endpoint', - component: 'testController', - params: {}, - fingerprint: 'testController_/test/endpoint' + headers: {}, + cgiData: { + 'server-software': `Node ${process.version}` + }, + action: '/test/endpoint', + component: 'testController', + params: {}, + tags: ['tag3', 'error-tag', 'context-tag'], + fingerprint: 'testController_/test/endpoint' + }); }); }); });