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 SDK. It allows Sentry to sample traces spanning multiple projects, by resolving the same set of rules based on the starting project.sample_rate
(number, optional) - The sample rate as defined by the user on the SDK where the trace originated.release
(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.
For example, the data
{
"trace_id": "771a43a4192642f0b136d5159a501700",
"public_key": "49d0f7386ad645858ae85020e393bef3",
"release": "1.1.22",
"environment": "dev",
"user": {
"segment": "vip",
"id": "7efa4978da177713df088f846f8c484d"
}
}
would encode as
ewogICJ0cmFjZV9pZCI6ICI3NzFhNDNhNDE5MjY0MmYwYjEzNmQ1MTU5YTUwMTcwMCIsCiAgInB1YmxpY19rZXkiOiAiNDlkMGY3Mzg2YWQ2NDU4NThhZTg1MDIwZTM5M2JlZjMiLAogICJyZWxlYXNlIjogIjEuMS4yMiIsCiAgImVudmlyb25tZW50IjogImRldiIsCiAgInVzZXIiOiB7CiAgICAic2VnbWVudCI6ICJ2aXAiLAogICAgImlkIjogIjdlZmE0OTc4ZGExNzc3MTNkZjA4OGY4NDZmOGM0ODRkIgogIH0KfQ
and result in the header
tracestate: sentry=ewogIC...IH0KfQ,other=[omitted]
(Note the third-party entry at the end of the header; new or modified entries are always added to the lefthand side, so we put the sentry=
value there. Also note that though the encoded value has been elided for legibilty here, in a real header the full value would be used.)
Implementation Guidelines
An SDK supporting this header must:
- Use scope information when creating a new trace context
- Add an envelope header with the trace context for envelopes containing transactions
- Add a
tracestate
HTTP header to outgoing HTTP requests for propagation - Intercept incoming HTTP requests for
tracestate
HTTP headers where applicable and apply them to the local trace context
Background
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.
Client options
While trace contexts are under development, they should be gated behind an internal trace_sampling
boolean client option. The option defaults to false
and should not be documented in Sentry docs.
Based on platform naming guidelines, the option should be cased appropriately:
trace_sampling
in snake casetraceSampling
in camel caseTraceSampling
in pascal casesetTraceSampling
for Java-style setters
Adding the Envelope Header
The SDK should add the envelope header to outgoing envelopes under any of the following conditions:
- The envelope contains a transaction event.
- The scope has a transaction bound.
Specifically, this means even envelopes without transactions can contain the trace
envelope header,
allowing Sentry to eventually sample attachments belonging to a transaction. When the envelope includes
a transaction and the scope has a bound transaction, the SDK should use the transaction of the envelope
to create the trace
envelope header.
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-🙂-è¯»å†™æ±‰å— - å¦ä¸æ–‡"}
Base64 with Command Line Utils
The GNU base64
command line utility comes with a switch to wrap the encoded
string. This is not compatible with the tracestate
header and should be
avoided. If the base64 implementations creates multiple lines, they must be
joined together.