Guidelines for Tracing Support

This document covers how SDKs should add support for Distributed Tracing.

This should give an overview of the APIs that SDKs need to implement, without mandating internal implementation details.

Reference implementations:

SDK Configuration

A new option tracesSampleRate must be added to sentry.init.

The type is a float and expected values are in the range [0.0, 1.0].
The default value is 0.0.

A tracesSampleRate of 0.0 means no transactions should be sent to Sentry. Conversely, 1.0 means all transactions should be sent. Anything in between means the fraction of uniformly random samples that should be sent. For example, 0.25 means send ~25% of all transactions.

The default value being 0.0 is such that tracing is an opt-in feature.

Event Changes

As of writing, transactions are implemented as an extension of the Event model.

The distinctive feature of a Transaction is type: "transaction".

Apart from that, the Event gets new fields: spans, contexts.TraceContext.

New Span and Transaction Classes

The Span class stores each individual span in a trace.

The Transaction class is like a span, with a few key differences:

  • Transactions have name, spans don't.
  • Calling the finish method on spans record the span's end timestamp. For transactions, the finish method additionally sends an event to Sentry.

The Transaction class may inherit from Span, but that's an implementation detail. Semantically, transactions represent both the top-level span of a span tree as well as the unit of reporting to Sentry.

  • Span Interface

    • When a Span is created, set the startTimestamp to the current time
    • SpanContext is the attribute collection for a Span (Can be an implementation detail)
    • The relation between parent - child is captured in the property parentSpanId
    • Span should have a method called toTraceparent which returns a string sentry-trace that could be sent as a header
    • Similar SpanContext should have a static method called fromTraceparent which prefills a SpanContext with data received from a sentry-trace string
  • Transaction Interface

    • A Transaction internally holds a flat list of child Spans (not a tree structure)
    • Transaction has additionally a setName method the set the name of the transaction
    • Transaction receives a TransactionContext on creation (new property vs. SpanContext is name)
    • Since a Transaction inherits a Span it has all functions available and can be interacted with like it was a Span
  • Span.finish()

    • Just set endTimestamp to the current time (in payload timestamp)
  • Transaction.finish()

    • super.finish() (call finish on Span)
    • Send it to Sentry only if sampled == true
    • A Transaction needs to be wrapped in an Envelope and sent to the Envelope Endpoint
    • The Transport should use the same internal queue for Transactions / Events
    • The Transport should implement category-based rate limiting →
    • The Transport should deal with wrapping a Transaction in an Envelope internally

Header sentry-trace

sentry-trace = traceid-spanid-sampling

With sampling being optional. So at a minimum, it's expected:

sentry-trace = traceid-spanid

To offer a minimal compatibility with W3C traceparent (without the version prefix) and b3 (which considers valid both 64 and 128 bits for traceId) headers the propagation header should have a traceId of 128 bits encoded in 32 hex chars and a spanId of 64 bits encoded in 16 hex chars. To avoid confusion with W3C traceparent due to being similar but not the exactly an implementation of it, we call it simply sentry-trace. No version is being defined in the header.

Sampling

The sampling section is optional. The format is not flags to simplify processing it. It's a single char. The possible values are:

Copied
  - No value means defer

0 - Don't sample

1 - Sampled

Differently than b3 a simple sampling decision as a value to sentry-trace should not be implemented. There are reasons to always include the trace-id and span-id regardless of sampling having been decided by the caller. And also this will simplify the implementation. Besides the usual reasons to use *defer,* in the case of Sentry, a reason would be if a downstream system captures an error event with Sentry. The decision could be done at that point to sample that trace in order to have tracing data available for the reported crash.

sentry-trace = sampled

Which in reality is useful for proxies to set it to 0 and opt out of tracing.

Static API Changes

The Sentry.startTransaction function should take the same arguments as the Transaction constructor.

It creates a Transaction bound to the current hub and returns the instance. Users interact with the instance for creating child spans and, thus, have to keep track of it themselves.

Hub Changes

  • Introduce a method called traceHeaders

    • This function returns a header (string) sentry-trace
    • The value should be the trace header string of the Span that is currently on the Scope
  • Hub → Introduce a method called startTransaction

    • Creates a new Transaction instance
    • This method deals with sampling, and therefore it should take the tracesSampleRate option into account:
      • Depending on the outcome, the sample decision should be stored in the Transaction's sampled property and again forwarded to its children

Scope Changes

The Scope holds a reference to the current Span or Transaction.

  • Scope Introduce setSpan
    • This can be used internally to pass a Span / Transaction around so that integrations can attach children to it
    • Setting the transaction property on the Scope (legacy) should overwrite the name of the Transaction stored in the Scope, if there is one. With that we give users the option to change the transaction name even if they don't have access to the instance of the Transaction directly.

Interaction with beforeSend and Event Processors

The beforeSend callback is a special Event Processor that we consider to be of most prominent use. Proper Event Processors are often considered internal.

Transactions should not go through beforeSend. However, they are still processed by Event Processors. This is a compromise between some flexibility in dealing with the current implementation of transactions as events, and leaving room for different lifetime hooks for transactions and spans.

Motivations:

  1. Future-proofing: if users rely on beforeSend for transactions, that would complicate eventually implementing individual span ingestion without breaking user code. As of writing, a transaction is sent as an event, but that is considered an implementation detail.

  2. API compatibility: users have their existing implementation of beforeSend that only ever had to deal with error events. We introduced transactions as a new type of event. As users upgrade to a new SDK version and start using tracing, their beforeSend would start seeing a new type that their code was not meant to handle. Before transactions, they didn't have to care about different event types at all. There are several possible consequences: breaking user apps; silently and unintentionally dropping transactions; transaction events modified in surprising ways.

  3. In terms of usability, beforeSend is not a perfect fit for dropping transactions like it is for dropping errors. Errors are a point-in-time event. When errors happen, users have full context in beforeSend and can modify/drop the event before it goes to Sentry. With transactions the flow is different. Transactions are created and then they are open for some time while child spans are created and appended to it. Meanwhile outgoing HTTP requests include the sampling decision of the current transaction with other services. After spans and the transaction are finished, dropping the transaction in a beforeSend-like hook would leave orphan transactions from other services in a trace. Similarly, modifying the sampling decision to "yes" at this late stage would also produce inconsistent traces.

You can edit this page on GitHub.