Trace Contexts (Experimental)
This feature is under development and not required for all SDKs supporting Performance Monitoring, yet. Please consider the Performance Guidelines as reference documentation. Anything that contradicts it is a mistake (or an out of date detail) in this document.
In order to sample traces we need to pass along the call chain a trace id together with the necessary information for making a sampling decision, the so-called "trace context".
Protocol
Trace information is passed between SDKs as an encoded tracestate
header, which SDKs are expected to intercept and propagate.
For event submission to sentry, the trace context is sent as JSON object embedded in an Envelope header with the key trace
.
Trace Context
Regardless of the transport mechanism, the trace context is a JSON object with the following fields:
trace_id
(string, required) - UUID V4 encoded as a hexadecimal sequence with no dashes (e.g.771a43a4192642f0b136d5159a501700
) that is a sequence of 32 hexadecimal digits. This must match the trace id of the submitted transaction item.public_key
(string, required) - Public key from the DSN used by the SDKrelease
(string, optional) - The release name as specified in client options, usually:package@x.y.z+build
. This should match therelease
attribute of the transaction event payload.*environment
- The environment name as specified in client options, for examplestaging
. This should match theenvironment
attribute of the transaction event payload.*user
(object, optional) - A subset of the scope's user context containing the following fields:id
(string, optional) - Theid
attribute of the user context.segment
(string, optional) - The value of asegment
attribute in the user's data bag, if it exists. In the future, this field may be promoted to a proper attribute of the user context.
transaction
(string, optional) - The transaction name set on the scope. This should match thetransaction
attribute of the transaction event payload.*
* See "Freezing the Context" for more information on consistency between the trace context and fields in the event payload.
Example:
{
"trace_id": "771a43a4192642f0b136d5159a501700",
"public_key": "49d0f7386ad645858ae85020e393bef3",
"release": "myapp@1.1.2",
"environment": "production",
"user": {
"id": "7efa4978da177713df088f846f8c484d",
"segment": "vip"
},
"transaction": "/api/0/project_details",
}
Envelope Headers
When sending transaction events to Sentry via Envelopes, the trace information must be set in the envelope headers under the trace
field.
Here's an example of a minimal envelope header containing the trace context (Although the header does not contain newlines, in the example below newlines were added for readability):
{
"event_id":"12c2d058d58442709aa2eca08bf20986",
"trace": {
"trace_id": "771a43a4192642f0b136d5159a501700",
"public_key": "49d0f7386ad645858ae85020e393bef3"
// other trace attributes
}
}
Tracestate Headers
When propagating trace contexts to other SDKs, Sentry uses the W3C tracestate
header. See "Trace Propagation" for more information on how to propagate these headers to other SDKs.
Tracestate headers contain several vendor-specific opaque data. As per HTTP spec, these multiple header values can be given in two ways, usually supported by HTTP libraries and framework out-of-the-box:
- Joined by comma:Copied
tracestate: sentry=<data>, other=<data>
- Repetition:Copied
tracestate: sentry=<data> tracestate: other=<data>
To create contents of the tracestate header:
- Serialize the full trace context to JSON, including the trace_id.
- Encode the resulting JSON string as UTF-8, if strings are represented differently on the platform.
- Encode the UTF-8 string with base64.
- Strip trailing padding characters (
=
), since this is a reserved character. - Prepend with
"sentry="
, resulting in"sentry=<base64>"
. - Join into the header as described above.
By stripping trailing padding, default base64 parsers may detect an incomplete payload. Choose a parsing mode that either allows for missing =
or allows truncated payloads.
The following is an example :
{
"trace_id": "771a43a4192642f0b136d5159a501700",
"public_key": "49d0f7386ad645858ae85020e393bef3",
"release": "1.1.22",
"environment": "dev",
"user": {
"segment": "vip",
"id": "7efa4978da177713df088f846f8c484d"
}
}
Would encode as:
ewogICJ0cmFjZV9pZCI6ICI3NzFhNDNhNDE5MjY0MmYwYjEzNmQ1MTU5YTUwMTcwMCIsCiAgInB1YmxpY19rZXkiOiAiNDlkMGY3Mzg2YWQ2NDU4NThhZTg1MDIwZTM5M2JlZjMiLAogICJyZWxlYXNlIjogIjEuMS4yMiIsCiAgImVudmlyb25tZW50IjogImRldiIsCiAgInVzZXIiOiB7CiAgICAic2VnbWVudCI6ICJ2aXAiLAogICAgImlkIjogIjdlZmE0OTc4ZGExNzc3MTNkZjA4OGY4NDZmOGM0ODRkIgogIH0KfQ
Resulting in the header:
tracestate: other=[omitted],sentry=ewogICJ0cmFjZV9pZCI6ICI3NzFhNDNhNDE5MjY0MmYwYjEzNmQ1MTU5YTUwMTcwMCIsCiAgInB1YmxpY19rZXkiOiAiNDlkMGY3Mzg2YWQ2NDU4NThhZTg1MDIwZTM5M2JlZjMiLAogICJyZWxlYXNlIjogIjEuMS4yMiIsCiAgImVudmlyb25tZW50IjogImRldiIsCiAgInVzZXIiOiB7CiAgICAic2VnbWVudCI6ICJ2aXAiLAogICAgImlkIjogIjdlZmE0OTc4ZGExNzc3MTNkZjA4OGY4NDZmOGM0ODRkIgogIH0KfQ
Implementation Guidelines
An SDK supporting this header must:
- Create a new trace context using scope information
- Intercept
tracestate
headers from incoming HTTP requests where applicable and apply them to the local trace context - Add the contents of trace context as
trace
header to Envelopes containing transaction events - Add
tracestate
headers to outgoing HTTP requests for propagation
Backgrounds
This is an extension of trace ID propagation covered by Performance Guidelines. According to the Unified API tracing spec, Sentry SDKs add an HTTP header sentry-trace
to outgoing requests via integrations. Most importantly, this header contains the trace ID, which must match the trace id of the transaction event and also of the trace context below.
The trace context shall be propagated in an additional tracestate
header defined in W3C traceparent header. Note that we must keep compatibility with the W3C spec as opposed to the proprietary sentry-trace
header. The tracestate
header also contains vendor-specific opaque data in addition to the contents placed by the Sentry SDK.
Freezing the Context
To ensure fully consistent trace contexts for all transactions in a trace, the trace context cannot be changed once it is sent over the wire, even if scope or options change afterwards. That is, once computed the trace context is no longer updated. Even if the app calls setRelease
, the old release remains in the context.
To compensate for lazy calls to functions like setTransaction
and setUser
, the trace context can be thought to be in two states: NEW and SENT . Initially, the context is in the NEW state and it is modifiable. Once sent for first time, it becomes SENT and can no longer change.
We recommend the trace context should be computed on-the-fly the first time it is needed in any of:
- Creating an Envelope
- Propagation to an outgoing HTTP request
The trace context must be retained until the user starts a new trace, at which point a new trace context must be computed by the SDK.
It is recommended that SDKs log modifications of attributes that would result in trace context changes like user.id
when the trace context is frozen, in order to simplify debugging of common dynamic sampling pitfalls.
Incoming Contexts
Same as for intercepting trace IDs from inbound HTTP requests, SDKs should read tracestate
headers and assume the Sentry trace context, if specified. Such a context is immediately frozen in the SENT state and should no longer allow for modifications.
Platform Specifics
Encoding in JavaScript
As mentioned, we need to encode the JSON trace context using UTF-8 strings. JavaScript internally uses UTF16 so we need to work a bit to do the transformation.
The basic idea is presented in this article (and in other places).
In short here's the function that converts a context into a base64 string that can be saved in tracestate
. In the end we went with a simpler implementation, but the idea is the same:
// Compact form
function objToB64(obj) {
const utf16Json = JSON.stringify(obj)
const b64 = btoa(encodeURIComponent(utf16Json).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode('0x' + p1);
}))
const len = b64.length
if (b64[len - 2] === '=')
return b64.substr(0, len - 2)
else if (b64[len - 1] === '=')
return b64.substr(0, len - 1)
return b64
}
// Commented
function objToB64(obj) {
// object to JSON string
const utf16Json = JSON.stringify(obj)
// still utf16 string but with non ASCI escaped as UTF-8 numbers)
const encodedUtf8 =encodeURIComponent(utf16Json);
// replace the escaped code points with utf16
// in the first 256 code points (the most wierd part)
const b64 = btoa(endcodedUtf8.replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode('0x' + p1);
}))
// drop the '=' or '==' padding from base64
const len = b64.length
if (b64[len - 2] === '=')
return b64.substr(0, len - 2)
else if (b64[len - 1] === '=')
return b64.substr(0, len - 1)
return b64
}
// const test = {"x":"a-🙂-è¯»å†™æ±‰å— - å¦ä¸æ–‡"}
// objToB64(test)
// "eyJ4IjoiYS3wn5mCLeivu+WGmeaxieWtlyAtIOWtpuS4reaWhyJ9"
And here's the function that accepts a base64 string (with or without '=' padding) and returns an object
function b64ToObj(b64) {
utf16 = decodeURIComponent(atob(b64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(utf16)
}
// b64ToObj("eyJ4IjoiYS3wn5mCLeivu+WGmeaxieWtlyAtIOWtpuS4reaWhyJ9")
// {"x":"a-🙂-è¯»å†™æ±‰å— - å¦ä¸æ–‡"}