diff --git a/docs/reference/agent-api.md b/docs/reference/agent-api.md index 9244b1fe4..c399ee912 100644 --- a/docs/reference/agent-api.md +++ b/docs/reference/agent-api.md @@ -215,12 +215,15 @@ Use this method to get the current active transaction. If there is no active tra ## `apm.captureError()` [capture-error] ```js -apm.captureError(error) +apm.captureError(error, options) ``` Arguments: * `error` - An instance of `Error`. +* `options` - The following options are supported: + + * `labels` - Add additional context with labels, these labels will be added to the error along with the labels from the current transaction. Use this method to manually send an error to APM Server: diff --git a/packages/rum-core/src/error-logging/error-logging.js b/packages/rum-core/src/error-logging/error-logging.js index 5f4a5ffc5..e5d499eb5 100644 --- a/packages/rum-core/src/error-logging/error-logging.js +++ b/packages/rum-core/src/error-logging/error-logging.js @@ -24,7 +24,7 @@ */ import { createStackTraces, filterInvalidFrames } from './stack-trace' -import { generateRandomId, merge, extend } from '../common/utils' +import { generateRandomId, merge, extend, setLabel } from '../common/utils' import { getPageContext } from '../common/context' import { truncateModel, ERROR_MODEL } from '../common/truncate' import stackParser from 'error-stack-parser' @@ -77,7 +77,7 @@ class ErrorLogging { /** * errorEvent = { message, filename, lineno, colno, error } */ - createErrorDataModel(errorEvent) { + createErrorDataModel(errorEvent, opts) { const frames = createStackTraces(stackParser, errorEvent) const filteredFrames = filterInvalidFrames(frames) @@ -100,6 +100,11 @@ class ErrorLogging { errorContext.custom = customProperties } } + if (opts && opts.labels) { + var keys = Object.keys(opts.labels) + errorContext.tags = {} + keys.forEach(k => setLabel(k, opts.labels[k], errorContext.tags)) + } if (!errorType) { /** @@ -151,11 +156,11 @@ class ErrorLogging { return truncateModel(ERROR_MODEL, errorObject) } - logErrorEvent(errorEvent) { + logErrorEvent(errorEvent, opts) { if (typeof errorEvent === 'undefined') { return } - var errorObject = this.createErrorDataModel(errorEvent) + var errorObject = this.createErrorDataModel(errorEvent, opts) if (typeof errorObject.exception.message === 'undefined') { return } @@ -194,14 +199,14 @@ class ErrorLogging { this.logErrorEvent(errorEvent) } - logError(messageOrError) { + logError(messageOrError, opts) { let errorEvent = {} if (typeof messageOrError === 'string') { errorEvent.message = messageOrError } else { errorEvent.error = messageOrError } - return this.logErrorEvent(errorEvent) + return this.logErrorEvent(errorEvent, opts) } _parseRejectReason(reason) { diff --git a/packages/rum-core/test/error-logging/error-logging.spec.js b/packages/rum-core/test/error-logging/error-logging.spec.js index 758d78b46..f863e6de0 100644 --- a/packages/rum-core/test/error-logging/error-logging.spec.js +++ b/packages/rum-core/test/error-logging/error-logging.spec.js @@ -73,6 +73,9 @@ describe('ErrorLogging', function () { try { throw new Error('unittest error') } catch (error) { + const opts = { + labels: { testLabelKey: 'testLabelValue' } + } error.test = 'hamid' error.aDate = new Date('2017-01-12T00:00:00.000Z') var obj = { test: 'test' } @@ -80,12 +83,13 @@ describe('ErrorLogging', function () { error.anObject = obj error.aFunction = function noop() {} error.null = null - errorLogging.logErrorEvent({ error }) + errorLogging.logErrorEvent({ error }, opts) const events = getEvents() expect(events.length).toBe(1) const errorData = events[0][ERRORS] expect(errorData.context.custom.test).toBe('hamid') expect(errorData.context.custom.aDate).toBe('2017-01-12T00:00:00.000Z') // toISOString() + expect(errorData.context.tags).toEqual({ testLabelKey: 'testLabelValue' }) expect(errorData.context.custom.anObject).toBeUndefined() expect(errorData.context.custom.aFunction).toBeUndefined() expect(errorData.context.custom.null).toBeUndefined() @@ -172,7 +176,10 @@ describe('ErrorLogging', function () { const errorEvent = { error: new Error(testErrorMessage) } - const errorData = errorLogging.createErrorDataModel(errorEvent) + const opts = { + labels: { testLabelKey: 'testLabelValue' } + } + const errorData = errorLogging.createErrorDataModel(errorEvent, opts) expect(errorData.context).toEqual( jasmine.objectContaining({ page: { @@ -184,7 +191,8 @@ describe('ErrorLogging', function () { foo: 'bar', bar: 20 }, - user: { id: 12, username: 'test' } + user: { id: 12, username: 'test' }, + tags: { testLabelKey: 'testLabelValue' } }) ) transaction.end() @@ -312,6 +320,27 @@ describe('ErrorLogging', function () { } }) + it('should add error with context to queue', function () { + apmServer.init() + configService.setConfig({ + serviceName: 'serviceName' + }) + spyOn(apmServer, 'sendEvents') + try { + throw new Error('error with context') + } catch (error) { + const opts = { + labels: { testLabelKey: 'testLabelValue' } + } + errorLogging.logError('test error', opts) + expect(apmServer.sendEvents).not.toHaveBeenCalled() + expect(apmServer.queue.items.length).toBe(1) + expect(apmServer.queue.items[0].errors.context).toEqual( + jasmine.objectContaining({ tags: { testLabelKey: 'testLabelValue' } }) + ) + } + }) + it('should capture unhandled rejection events', done => { apmServer.init() configService.setConfig({ diff --git a/packages/rum/src/apm-base.js b/packages/rum/src/apm-base.js index 8dcc3ecd0..ec3c89f68 100644 --- a/packages/rum/src/apm-base.js +++ b/packages/rum/src/apm-base.js @@ -316,10 +316,10 @@ export default class ApmBase { } } - captureError(error) { + captureError(error, opts) { if (this.isEnabled()) { var errorLogging = this.serviceFactory.getService(ERROR_LOGGING) - return errorLogging.logError(error) + return errorLogging.logError(error, opts) } } diff --git a/packages/rum/src/index.d.ts b/packages/rum/src/index.d.ts index acdd045a6..d48ddf606 100644 --- a/packages/rum/src/index.d.ts +++ b/packages/rum/src/index.d.ts @@ -110,6 +110,10 @@ declare module '@elastic/apm-rum' { }) => boolean } + export interface ErrorOptions { + labels: Labels + } + type Init = (options?: AgentConfigOptions) => ApmBase const init: Init @@ -142,7 +146,7 @@ declare module '@elastic/apm-rum' { options?: SpanOptions ): Span | undefined getCurrentTransaction(): Transaction | undefined - captureError(error: Error | string): void + captureError(error: Error | string, opts?: ErrorOptions): void addFilter(fn: FilterFn): void } const apmBase: ApmBase diff --git a/packages/rum/test/specs/index.spec.js b/packages/rum/test/specs/index.spec.js index 242e1198c..321ce7e98 100644 --- a/packages/rum/test/specs/index.spec.js +++ b/packages/rum/test/specs/index.spec.js @@ -74,7 +74,9 @@ describe('index', function () { try { throw new Error('ApmBase test error') } catch (error) { - apmBase.captureError(error) + apmBase.captureError(error, { + labels: { testLabelKey: 'testLabelValue' } + }) expect(apmServer.sendEvents).not.toHaveBeenCalled() if (isPlatformSupported()) { @@ -82,6 +84,10 @@ describe('index', function () { setTimeout(() => { expect(apmServer.sendEvents).toHaveBeenCalled() var callData = apmServer.sendEvents.calls.mostRecent() + var eventData = callData.args[0][0] + expect(eventData.errors.context.tags.testLabelKey).toBe( + 'testLabelValue' + ) callData.returnValue.then( () => { // Wait before ending the test to make sure the result are processed by the agent.