Trace Contexts (Experimental)

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
  • release (string, optional) - The release name as specified in client options, usually: package@x.y.z+build. This should match the release attribute of the transaction event payload.*
  • environment - The environment name as specified in client options, for example staging. This should match the environment attribute of the transaction event payload.*
  • user (object, optional) - A subset of the scope's user context containing the following fields:
    • id (string, optional) - The id attribute of the user context.
    • segment (string, optional) - The value of a segment 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 the transaction 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:

Copied
{
  "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):

Copied
{
  "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:

  1. Serialize the full trace context to JSON, including the trace_id.
  2. Encode the resulting JSON string as UTF-8, if strings are represented differently on the platform.
  3. Encode the UTF-8 string with base64.
  4. Strip trailing padding characters (=), since this is a reserved character.
  5. Prepend with "sentry=", resulting in "sentry=<base64>".
  6. Join into the header as described above.

The following is an example :

Copied
{
  "trace_id": "771a43a4192642f0b136d5159a501700",
  "public_key": "49d0f7386ad645858ae85020e393bef3",
  "release": "1.1.22",
  "environment": "dev",
  "user": {
    "segment": "vip",
    "id": "7efa4978da177713df088f846f8c484d"
  }
}

Would encode as:

Copied
ewogICJ0cmFjZV9pZCI6ICI3NzFhNDNhNDE5MjY0MmYwYjEzNmQ1MTU5YTUwMTcwMCIsCiAgInB1YmxpY19rZXkiOiAiNDlkMGY3Mzg2YWQ2NDU4NThhZTg1MDIwZTM5M2JlZjMiLAogICJyZWxlYXNlIjogIjEuMS4yMiIsCiAgImVudmlyb25tZW50IjogImRldiIsCiAgInVzZXIiOiB7CiAgICAic2VnbWVudCI6ICJ2aXAiLAogICAgImlkIjogIjdlZmE0OTc4ZGExNzc3MTNkZjA4OGY4NDZmOGM0ODRkIgogIH0KfQ

Resulting in the header:

Copied
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.

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:

Copied
// 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

Copied
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-🙂-读写汉字 - 学中文"}
You can edit this page on GitHub.