diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 740f59c2c..a8b387639 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -363,3 +363,30 @@ If APM Server is deployed in an origin different than the page’s origin, you w :::: +### `transactionContextCallback` [transaction-context-callback] + +* **Type:** Function +* **Default:** `null` + +`transactionContextCallback` allows the agent to specify a function to be called when starting automatically instrumented transactions and spans and return +context to be set as tags. This enables the agent to capture the context when instrumented events are fired from files which do not import the RUM agent library. + +The following example illustrates an example which captures the stack trace: + +```js +var options = { + transactionContextCallback: () => { + let stack + try { + throw new Error('') + } + catch (error) { + stack = (error as Error).stack || '' + } + stack = stack.split('\n').map(function (line) { return line.trim(); }) + return { stack }; + } +} +``` + + diff --git a/packages/rum-core/src/common/config-service.js b/packages/rum-core/src/common/config-service.js index a11a52677..a0e7c1cd1 100644 --- a/packages/rum-core/src/common/config-service.js +++ b/packages/rum-core/src/common/config-service.js @@ -95,7 +95,9 @@ class Config { context: {}, session: false, apmRequest: null, - sendCredentials: false + sendCredentials: false, + transactionContextCallback: null, + spanContextCallback: null } this.events = new EventHandler() diff --git a/packages/rum-core/src/performance-monitoring/span.js b/packages/rum-core/src/performance-monitoring/span.js index d584b1070..8120cab7c 100644 --- a/packages/rum-core/src/performance-monitoring/span.js +++ b/packages/rum-core/src/performance-monitoring/span.js @@ -39,6 +39,10 @@ class Span extends SpanBase { this.action = fields[2] } this.sync = this.options.sync + + if (this.options.spanContextCallback) { + this.addLabels(this.options.spanContextCallback()) + } } end(endTime, data) { diff --git a/packages/rum-core/src/performance-monitoring/transaction-service.js b/packages/rum-core/src/performance-monitoring/transaction-service.js index 981c2fe60..d546e4fd6 100644 --- a/packages/rum-core/src/performance-monitoring/transaction-service.js +++ b/packages/rum-core/src/performance-monitoring/transaction-service.js @@ -101,7 +101,32 @@ class TransactionService { createOptions(options) { const config = this._config.config - let presetOptions = { transactionSampleRate: config.transactionSampleRate } + const logger = this._logger + let presetOptions = { + transactionSampleRate: config.transactionSampleRate + } + if (typeof config.transactionContextCallback === 'function') { + presetOptions.transactionContextCallback = function () { + let tags = {} + try { + tags = config.transactionContextCallback() + } catch (err) { + logger.error('Failed to execute transaction context callback', err) + } + return tags + } + } + if (typeof config.spanContextCallback === 'function') { + presetOptions.spanContextCallback = function () { + let tags = {} + try { + tags = config.spanContextCallback() + } catch (err) { + logger.error('Failed to execute span context callback', err) + } + return tags + } + } let perfOptions = extend(presetOptions, options) if (perfOptions.managed) { perfOptions = extend( @@ -284,7 +309,7 @@ class TransactionService { if (name === NAME_UNKNOWN && pageLoadTransactionName) { tr.name = pageLoadTransactionName } - + /** * Capture the TBT as span after observing for all long task entries * and once performance observer is disconnected diff --git a/packages/rum-core/src/performance-monitoring/transaction.js b/packages/rum-core/src/performance-monitoring/transaction.js index 09f2d34f8..0effed85a 100644 --- a/packages/rum-core/src/performance-monitoring/transaction.js +++ b/packages/rum-core/src/performance-monitoring/transaction.js @@ -54,6 +54,10 @@ class Transaction extends SpanBase { this.sampleRate = this.options.transactionSampleRate this.sampled = Math.random() <= this.sampleRate + + if (this.options.transactionContextCallback) { + this.addLabels(this.options.transactionContextCallback()) + } } addMarks(obj) { @@ -96,7 +100,11 @@ class Transaction extends SpanBase { if (this.ended) { return } - const opts = extend({}, options) + let opts = extend({}, options) + + if (this.options.spanContextCallback) { + opts.spanContextCallback = this.options.spanContextCallback + } opts.onEnd = trc => { this._onSpanEnd(trc) diff --git a/packages/rum-core/test/performance-monitoring/transaction-service.spec.js b/packages/rum-core/test/performance-monitoring/transaction-service.spec.js index 09a270b6c..913df9fc2 100644 --- a/packages/rum-core/test/performance-monitoring/transaction-service.spec.js +++ b/packages/rum-core/test/performance-monitoring/transaction-service.spec.js @@ -651,6 +651,95 @@ describe('TransactionService', function () { transaction.end(pageLoadTime + 1000) }) + + it('should capture tags from transaction dispatch context', done => { + config.setConfig({ + transactionContextCallback: () => { + let stack + try { + throw new Error('') + } catch (error) { + stack = error.stack || '' + } + stack = stack.split('\n').map(function (line) { + return line.trim() + }) + return { stack } + } + }) + const transactionService = new TransactionService(logger, config) + + const tr1 = transactionService.startTransaction( + 'transaction1', + 'transaction' + ) + + tr1.onEnd = () => { + expect(tr1.context.tags.stack).toBeTruthy() + done() + } + tr1.end() + }) + + it('should capture tags from span dispatch context', done => { + config.setConfig({ + spanContextCallback: () => { + let stack + try { + throw new Error('') + } catch (error) { + stack = error.stack || '' + } + stack = stack.split('\n').map(function (line) { + return line.trim() + }) + return { stack } + } + }) + const transactionService = new TransactionService(logger, config) + + const sp1 = transactionService.startSpan('span1', 'span') + + sp1.onEnd = () => { + expect(sp1.context.tags.stack).toBeTruthy() + done() + } + sp1.end() + }) + + it('should safely catch and log errors for an invalid callback', () => { + logger = new LoggingService() + spyOn(logger, 'error') + + config.setConfig({ + transactionContextCallback: () => { + throw new Error('Error in transaction callback') + }, + spanContextCallback: () => { + throw new Error('Error in span callback') + } + }) + const transactionService = new TransactionService(logger, config) + + const tr1 = transactionService.startTransaction( + 'transaction1', + 'transaction' + ) + expect(logger.error).toHaveBeenCalledWith( + 'Failed to execute transaction context callback', + new Error('Error in transaction callback') + ) + logger.error.calls.reset() + + const sp1 = tr1.startSpan('span1', 'span') + expect(logger.error).toHaveBeenCalledWith( + 'Failed to execute span context callback', + new Error('Error in span callback') + ) + + sp1.end() + tr1.end() + }) }) it('should truncate active spans after transaction ends', () => { diff --git a/packages/rum/src/index.d.ts b/packages/rum/src/index.d.ts index acdd045a6..ae4540975 100644 --- a/packages/rum/src/index.d.ts +++ b/packages/rum/src/index.d.ts @@ -108,6 +108,8 @@ declare module '@elastic/apm-rum' { payload?: string headers?: Record }) => boolean + transactionContextCallback?: () => Labels + spanContextCallback?: () => Labels } type Init = (options?: AgentConfigOptions) => ApmBase