Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add callback option to config to capture context when starting transactions and spans #1525

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
62 changes: 62 additions & 0 deletions docs/configuration.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,65 @@ This is useful on scenarios where the APM server is behind a reverse proxy that

NOTE: If APM Server is deployed in an origin different than the page’s origin, you will need to
<<configuring-cors, configure Cross-Origin Resource Sharing (CORS)>>.


[function]
cjr125 marked this conversation as resolved.
Show resolved Hide resolved
[[transaction-context-callback]]
==== `transactionContextCallback`

* *Type:* Function
* *Default:* `null`

`transactionContextCallback` allows the agent to specify a function to be called when starting automatically instrumented transactions and return context to
be set as tags. This enables the agent to capture data such as call stack frames and variable values from the scope 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:

[source,js]
----
var options = {
transactionContextCallback: () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a big fan of this approach and exposing configurations feels unncessary. Why not adding custom properites to transaction context via

apm.SetCustomContext

You can basically call this function apm.SetCustomContext anywhere and get the context added to the transaction/error events

apm.Observe

You can rely on the events when any transaction gets started/ended and add the relevant tags/context specific information.

apm.observe('transaction:start', function (transaction) {
    stack = "" // error stack
    transaction.addLabels({ stack })
})

Let me know what you think.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vigneshshanmugam the callback approach makes an important difference when context needs to be captured from an auto-instrumented transaction which starts in a file which is loaded by the application but does not import the RUM agent library. Using the built-in setCustomContext approach will not show the URL of the file where such a transaction started in a predictable order in the stack trace if we are to capture it as you have described. The callback approach will guarantee it will occur at the top of the stack trace. The use case we are interested in is quickly identifying the developer responsible for maintaining the code where a problematic transaction starts, so the callback approach is the only way to guarantee that we can immediately filter out the other files (when the original file does not import the library). Please let me know if you have other questions.

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 };
}
}
----


[function]
cjr125 marked this conversation as resolved.
Show resolved Hide resolved
[[span-context-callback]]
==== `spanContextCallback`

* *Type:* Function
* *Default:* `null`

`spanContextCallback` allows the agent to specify a function to be called when starting automatically instrumented spans and return context to be set as tags.
This enables the agent to capture data such as call stack frames and variable values from the scope 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:

[source,js]
----
var options = {
spanContextCallback: () => {
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 };
}
}
----
4 changes: 3 additions & 1 deletion packages/rum-core/src/common/config-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ class Config {
context: {},
session: false,
apmRequest: null,
sendCredentials: false
sendCredentials: false,
transactionContextCallback: null,
spanContextCallback: null
}

this.events = new EventHandler()
Expand Down
13 changes: 13 additions & 0 deletions packages/rum-core/src/performance-monitoring/span.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ class Span extends SpanBase {
this.action = fields[2]
}
this.sync = this.options.sync

if (
this.options.spanContextCallback &&
cjr125 marked this conversation as resolved.
Show resolved Hide resolved
typeof this.options.spanContextCallback === 'function'
) {
let tags
try {
tags = this.options.spanContextCallback()
this.addLabels(tags)
} catch (e) {
console.error('Failed to execute span context callback', e)
cjr125 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

end(endTime, data) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,21 @@ class TransactionService {

createOptions(options) {
const config = this._config.config
let presetOptions = { transactionSampleRate: config.transactionSampleRate }
let presetOptions = {
transactionSampleRate: config.transactionSampleRate
}
if (config.transactionContextCallback) {
presetOptions = {
...presetOptions,
cjr125 marked this conversation as resolved.
Show resolved Hide resolved
transactionContextCallback: config.transactionContextCallback
}
}
if (config.spanContextCallback) {
presetOptions = {
...presetOptions,
cjr125 marked this conversation as resolved.
Show resolved Hide resolved
spanContextCallback: config.spanContextCallback
}
}
let perfOptions = extend(presetOptions, options)
if (perfOptions.managed) {
perfOptions = extend(
Expand Down
25 changes: 24 additions & 1 deletion packages/rum-core/src/performance-monitoring/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ class Transaction extends SpanBase {

this.sampleRate = this.options.transactionSampleRate
this.sampled = Math.random() <= this.sampleRate

if (
cjr125 marked this conversation as resolved.
Show resolved Hide resolved
this.options.transactionContextCallback &&
typeof this.options.transactionContextCallback === 'function'
) {
let tags
try {
tags = this.options.transactionContextCallback()
this.addLabels(tags)
} catch (e) {
console.error('Failed to execute transaction context callback', e)
}
}
}

addMarks(obj) {
Expand Down Expand Up @@ -96,7 +109,17 @@ class Transaction extends SpanBase {
if (this.ended) {
return
}
const opts = extend({}, options)
let opts = extend({}, options)

if (
cjr125 marked this conversation as resolved.
Show resolved Hide resolved
this.options.spanContextCallback &&
typeof this.options.spanContextCallback === 'function'
) {
opts = {
...opts,
spanContextCallback: this.options.spanContextCallback
}
}

opts.onEnd = trc => {
this._onSpanEnd(trc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,61 @@ 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()
})
cjr125 marked this conversation as resolved.
Show resolved Hide resolved
})

it('should truncate active spans after transaction ends', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/rum/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ declare module '@elastic/apm-rum' {
method: string
payload?: string
headers?: Record<string, string>
}) => boolean
}) => boolean,
transactionContextCallback?: (...args: any[]) => any,
spanContextCallback?: (...args: any[]) => any
}

type Init = (options?: AgentConfigOptions) => ApmBase
Expand Down