From fc970ef397be2d29c2d2c41c1c4eac6780da745c Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 19 Oct 2026 11:19:06 +0200 Subject: [PATCH 1/2] feat(develop): Adjust DSC spec to be compatible with span streaming --- .../dynamic-sampling-context.mdx | 77 ++++++++++++++++++- develop-docs/sdk/telemetry/spans/index.mdx | 11 ++- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/develop-docs/sdk/foundations/trace-propagation/dynamic-sampling-context.mdx b/develop-docs/sdk/foundations/trace-propagation/dynamic-sampling-context.mdx index b77d5ce1e7bcb7..866b0d32b8a436 100644 --- a/develop-docs/sdk/foundations/trace-propagation/dynamic-sampling-context.mdx +++ b/develop-docs/sdk/foundations/trace-propagation/dynamic-sampling-context.mdx @@ -2,7 +2,7 @@ title: Dynamic Sampling Context (DSC) description: How SDKs propagate Dynamic Sampling Context via baggage headers and envelope headers. spec_id: sdk/foundations/trace-propagation/dynamic-sampling-context -spec_version: 1.3.0 +spec_version: 1.4.0 spec_status: stable spec_depends_on: - id: sdk/foundations/trace-propagation @@ -10,6 +10,9 @@ spec_depends_on: - id: sdk/foundations/transport/envelopes version: ">=1.0.0" spec_changelog: + - version: 1.4.0 + date: 2026-10-19 + summary: Don't freeze an empty DSC when continuing a trace without baggage; create it from the segment span instead (required for span streaming) - version: 1.3.0 date: 2026-02-24 summary: Moved to trace-propagation sub-spec @@ -203,6 +206,7 @@ DSC leaves SDKs in two situations: When an SDK receives an HTTP request that was "instrumented" or "traced" by a Sentry SDK, the receiving SDK **MUST** consider the incoming DSC as instantly frozen. Any values on the DSC **MUST** be propagated "as is" — this includes values like "environment" or "release". +If the incoming request has a `sentry-trace` header but no DSC (no `baggage` header with `sentry-` values), see [Incoming Trace Without DSC](#incoming-trace-without-dsc). SDKs **SHOULD** recognize incoming requests as "instrumented" or "traced" when at least one of the following applies: @@ -217,7 +221,56 @@ After the DSC of a particular trace has been frozen, API calls like `set_user` * ### Unified Propagation Mechanism -SDKs **SHOULD** follow these steps for any incoming and outgoing requests (in python pseudo-code for illustrative purposes): +SDKs **SHOULD** follow these steps for any incoming and outgoing requests (in python pseudo-code for illustrative purposes). For transaction-based SDKs, the segment span is the transaction: + +```python +def collect_dynamic_sampling_context(): + # Placeholder function that collects as many values for Dynamic Sampling Context + # as possible and returns a dict + +def has_sentry_value_in_baggage_header(request): + # Placeholder function that returns True when there is at least one key-value pair in the baggage + # header of `request`, for which the key starts with "sentry-". Otherwise, it returns False. + +def on_incoming_request(request): + # Only applies if the SDK continues the incoming trace (see strict trace continuation) + if request.has_header("sentry-trace") and (not request.has_header("baggage") or not has_sentry_value_in_baggage_header(request)): + # Request comes from an old SDK which doesn't support Dynamic Sampling Context yet + # --> we don't freeze the DSC. Instead, it is created from our own segment span + # as late as possible (on the first outgoing request or when the envelope is sent) + current_segment_span.dynamic_sampling_context_frozen = False + elif request.has_header("sentry-trace") and request.has_header("baggage") and has_sentry_value_in_baggage_header(request): + current_segment_span.dynamic_sampling_context_frozen = True + current_segment_span.dynamic_sampling_context = baggage_header_to_dict(request.headers.baggage) + +def on_outgoing_request(request): + if not current_segment_span.dynamic_sampling_context_frozen: + current_segment_span.dynamic_sampling_context_frozen = True + current_segment_span.dynamic_sampling_context = merge_dicts(collect_dynamic_sampling_context(), current_segment_span.dynamic_sampling_context) + + if not current_segment_span.dynamic_sampling_context: + # Make sure there is at least an empty DSC set on the segment span + # This is independent of whether it is locked or not + current_segment_span.dynamic_sampling_context = {} + + if request.has_header("baggage"): + outgoing_baggage_dict = baggage_header_to_dict(request.headers.baggage) + merged_baggage_dict = merge_dicts(outgoing_baggage_dict, current_segment_span.dynamic_sampling_context) + request.set_header("baggage", dict_to_baggage_header(merged_baggage_dict)) + else: + request.set_header("baggage", dict_to_baggage_header(current_segment_span.dynamic_sampling_context)) + +def on_envelope_send(envelope): + if not current_segment_span.dynamic_sampling_context_frozen: + current_segment_span.dynamic_sampling_context_frozen = True + current_segment_span.dynamic_sampling_context = merge_dicts(collect_dynamic_sampling_context(), current_segment_span.dynamic_sampling_context) + + envelope.headers.trace = current_segment_span.dynamic_sampling_context +``` + + + +Before version 1.4.0, SDKs froze an empty DSC when the incoming request had a `sentry-trace` header but no DSC. SDKs that don't implement [span streaming](/sdk/telemetry/spans/) **MAY** keep following this behavior: ```python def collect_dynamic_sampling_context(): @@ -255,11 +308,29 @@ def on_outgoing_request(request): request.set_header("baggage", dict_to_baggage_header(current_transaction.dynamic_sampling_context)) ``` -While there is no strict necessity for the `current_transaction.dynamic_sampling_context_frozen` flag yet, there is a future use case where we need it: + + +While there is no strict necessity for the `dynamic_sampling_context_frozen` flag yet, there is a future use case where we need it: We might want users to be able to set Dynamic Sampling Context values themselves. The flag becomes relevant after the first propagation, where Dynamic Sampling Context becomes immutable. When users attempt to set DSC afterwards, our SDKs **SHOULD** make this operation a noop. +#### Incoming Trace Without DSC + +If an SDK receives a `sentry-trace` header but no DSC (no `baggage` header with `sentry-` values), continues the trace, and starts a segment span: + +- The SDK **MUST NOT** freeze an empty DSC for the continued trace. +- The SDK **MUST** instead create the DSC from its own segment span, collecting as many [DSC values](#dsc-specification) as possible. `trace_id` **MUST** match the continued trace, and `sampled` **MUST** match the segment span's sampling decision. +- If the SDK inherited the sampling decision from `sentry-trace`, `sample_rate` **MUST** be `1.0` for a positive and `0.0` for a negative decision. Otherwise (a `tracesSampler` was invoked, or `sentry-trace` had no sampling decision), `sample_rate` **MUST** be the rate used for the decision, as for the head of a trace. Either way, the DSC stays consistent with `sample_rand`. +- The SDK **SHOULD** materialize and freeze this DSC as late as possible, either on the first outgoing request or when the first envelope is sent. For span streaming, see [Serialization and Dynamic Sampling Context](/sdk/telemetry/spans/#serialization-and-dynamic-sampling-context). + +SDKs implementing [span streaming](/sdk/telemetry/spans/) **MUST** follow this behavior, because span envelopes require a `trace` envelope header. Other SDKs **SHOULD** follow it, but **MAY** keep the previous behavior from versions 1.0.0 to 1.3.0. + +Whether the SDK continues the trace is decided by [strict trace continuation](/sdk/foundations/trace-propagation/#strict-trace-continuation) first. Since there's no incoming `sentry-org_id`, the SDK starts a new trace if `strictTraceContinuation` is `true` and the SDK has an organization ID. In that case, it's the head of a new trace and this section doesn't apply. Otherwise, it continues the trace and the DSC carries the SDK's own `org_id`, if it has one. + +A common reason for this scenario is old SDKs not compatible with DSC starting traces. Therefore, they +only send the `sentry-trace` but no `baggage` header with `sentry-` values. + --- diff --git a/develop-docs/sdk/telemetry/spans/index.mdx b/develop-docs/sdk/telemetry/spans/index.mdx index 14ede26809eaee..199187d32ad5ad 100644 --- a/develop-docs/sdk/telemetry/spans/index.mdx +++ b/develop-docs/sdk/telemetry/spans/index.mdx @@ -2,14 +2,19 @@ title: Spans description: Span creation, sampling, filtering, buffering, and wire format for the span-first tracing model. spec_id: sdk/telemetry/spans -spec_version: 1.1.0 +spec_version: 1.2.0 spec_status: stable spec_depends_on: - id: sdk/foundations/transport/envelopes version: ">=1.0.0" - id: sdk/foundations/trace-propagation version: ">=1.0.0" + - id: sdk/foundations/trace-propagation/dynamic-sampling-context + version: ">=1.4.0" spec_changelog: + - version: 1.2.0 + date: 2026-10-19 + summary: Require DSC spec 1.4.0 so span envelopes always have a trace envelope header, even for incoming traces without baggage - version: 1.1.0 date: 2026-10-08 summary: Added client reports section listing discard reasons for spans and clarifying that the quantity is the number of discarded spans @@ -477,6 +482,8 @@ SDKs **SHOULD** materialize and freeze the DSC as late as possible: - The buffer **SHOULD** enqueue spans but only create the final envelope at **flush time**. - At flush time, the buffer **SHOULD** materialize and freeze the DSC on the segment span if not already done. That way, the `trace` envelope header (e.g. the transaction name in the DSC) reflects the latest data. +This also applies when the SDK continued an incoming trace that had no DSC (a `sentry-trace` header without `baggage`). See [Incoming Trace Without DSC](/sdk/foundations/trace-propagation/dynamic-sampling-context/#incoming-trace-without-dsc). + @@ -614,6 +621,8 @@ The envelope header **MUST** contain the same properties as previously with tran Unlike transactions, envelope headers for spans **REQUIRE** [Dynamic Sampling Context](/sdk/foundations/trace-propagation/dynamic-sampling-context/). See also [Envelope Headers](/sdk/foundations/envelopes/#headers). +To guarantee a DSC for every span envelope, SDKs implementing span streaming **MUST** follow version 1.4.0 or later of the DSC spec. In particular, if the SDK continues an incoming trace that has a `sentry-trace` header but no DSC, it **MUST** create the DSC from its own segment span instead of freezing an empty one. See [Incoming Trace Without DSC](/sdk/foundations/trace-propagation/dynamic-sampling-context/#incoming-trace-without-dsc). + ### Span v2 Envelope Item Header | Property | Type | Required | Description | From 88d2b2139f7681203027874ff9d47a358b7e4e14 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 19 Oct 2026 11:33:33 +0200 Subject: [PATCH 2/2] Apply batched suggestions from code review Co-authored-by: Lukas Stracke --- develop-docs/sdk/telemetry/spans/index.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/develop-docs/sdk/telemetry/spans/index.mdx b/develop-docs/sdk/telemetry/spans/index.mdx index 199187d32ad5ad..93ecb3d684592b 100644 --- a/develop-docs/sdk/telemetry/spans/index.mdx +++ b/develop-docs/sdk/telemetry/spans/index.mdx @@ -482,8 +482,6 @@ SDKs **SHOULD** materialize and freeze the DSC as late as possible: - The buffer **SHOULD** enqueue spans but only create the final envelope at **flush time**. - At flush time, the buffer **SHOULD** materialize and freeze the DSC on the segment span if not already done. That way, the `trace` envelope header (e.g. the transaction name in the DSC) reflects the latest data. -This also applies when the SDK continued an incoming trace that had no DSC (a `sentry-trace` header without `baggage`). See [Incoming Trace Without DSC](/sdk/foundations/trace-propagation/dynamic-sampling-context/#incoming-trace-without-dsc). - @@ -621,7 +619,7 @@ The envelope header **MUST** contain the same properties as previously with tran Unlike transactions, envelope headers for spans **REQUIRE** [Dynamic Sampling Context](/sdk/foundations/trace-propagation/dynamic-sampling-context/). See also [Envelope Headers](/sdk/foundations/envelopes/#headers). -To guarantee a DSC for every span envelope, SDKs implementing span streaming **MUST** follow version 1.4.0 or later of the DSC spec. In particular, if the SDK continues an incoming trace that has a `sentry-trace` header but no DSC, it **MUST** create the DSC from its own segment span instead of freezing an empty one. See [Incoming Trace Without DSC](/sdk/foundations/trace-propagation/dynamic-sampling-context/#incoming-trace-without-dsc). +To guarantee a DSC for every span envelope, SDKs implementing span streaming **MUST** follow version 1.4.0 or later of the DSC spec. ### Span v2 Envelope Item Header