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. It allows Sentry to sample traces spanning multiple projects, by resolving the same set of rules based on the starting project.
  • 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.

For example, the data

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

would encode as

Copied
ewogICJ0cmFjZV9pZCI6ICI3NzFhNDNhNDE5MjY0MmYwYjEzNmQ1MTU5YTUwMTcwMCIsCiAgInB1YmxpY19rZXkiOiAiNDlkMGY3Mzg2YWQ2NDU4NThhZTg1MDIwZTM5M2JlZjMiLAogICJyZWxlYXNlIjogIjEuMS4yMiIsCiAgImVudmlyb25tZW50IjogImRldiIsCiAgInVzZXIiOiB7CiAgICAic2VnbWVudCI6ICJ2aXAiLAogICAgImlkIjogIjdlZmE0OTc4ZGExNzc3MTNkZjA4OGY4NDZmOGM0ODRkIgogIH0KfQ

and result in the header

Copied
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 case
  • traceSampling in camel case
  • TraceSampling in pascal case
  • setTraceSampling for Java-style setters

Adding the Envelope Header

The SDK should add the envelope header to outgoing envelopes under any of the following conditions:

  1. The envelope contains a transaction event.
  2. 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.

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-🙂-读写汉字 - 学中文"}

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.

You can edit this page on GitHub.