Skip to content

feat(tracing): strict trace continuation#1663

Open
jpnurmi wants to merge 20 commits intomasterfrom
jpnurmi/feat/strict-trace-continuation
Open

feat(tracing): strict trace continuation#1663
jpnurmi wants to merge 20 commits intomasterfrom
jpnurmi/feat/strict-trace-continuation

Conversation

@jpnurmi
Copy link
Copy Markdown
Collaborator

@jpnurmi jpnurmi commented Apr 22, 2026

Comment thread src/sentry_tracing.c Outdated
Comment thread src/sentry_options.c
Comment thread src/sentry_core.c
@jpnurmi jpnurmi force-pushed the jpnurmi/feat/strict-trace-continuation branch from 2286835 to e7befcf Compare April 22, 2026 18:45
Comment thread src/sentry_scope.c
Comment thread src/sentry_core.c Outdated
@jpnurmi jpnurmi force-pushed the jpnurmi/feat/strict-trace-continuation branch from e7befcf to 61bb274 Compare April 22, 2026 19:39
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 61bb274. Configure here.

Comment thread src/sentry_tracing.c Outdated
@jpnurmi jpnurmi force-pushed the jpnurmi/feat/strict-trace-continuation branch 2 times, most recently from 5065a55 to 63a51da Compare May 4, 2026 13:10
@jpnurmi jpnurmi changed the title WIP: feat(tracing): strict trace continuation feat(tracing): strict trace continuation May 4, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 2794991

jpnurmi and others added 19 commits May 4, 2026 17:45
Adds two reusable helpers — `sentry__baggage_iter_next`, which yields
the next W3C baggage member as trimmed slices (with property suffixes
stripped and malformed members skipped), and
`sentry__percent_decode_inplace`, which pct-decodes a buffer in place
with malformed escapes passed through verbatim. Both are covered by
focused unit tests; no production call sites are rewired in this
commit.
Provide an internal object key/value iteration helper with focused unit coverage
so later trace propagation code can iterate DSC values without indexed key/value
access.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Add `sentry_options_set_strict_trace_continuation` / `_get_` as an
experimental API. The option defaults to false and is not wired up to
any propagation logic yet; subsequent commits will consume it when the
trace-continuation decision path is implemented.

Preparation for strict trace continuation:
https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `sentry_options_set_org_id` / `_set_org_id_n` / `_get_org_id` as an
experimental API. Overrides the organization ID derived from the DSN
host, which is required for self-hosted setups whose ingest hostname
does not encode the org. Nothing consumes the option yet; subsequent
commits will route it through the effective-org_id resolver and the
strict-trace-continuation decision.

Preparation for strict trace continuation:
https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`sentry_set_trace` and `sentry_regenerate_trace` updated the scope's
propagation context but left the dynamic sampling context (built once
at `sentry_init`) untouched. The DSC's `sample_rand` therefore stayed
tied to the trace generated at init, even after the caller switched
traces. Outgoing propagation that consumes the scope DSC would emit
stale values mismatched against `sentry-trace`.

Refresh the scope DSC after each trace change.

Surfaced while preparing strict trace continuation, where outgoing
baggage will draw all DSC fields from the scope DSC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `sentry__options_get_effective_org_id` (option > DSN > NULL, empty
treated as absent) and consume it in the dynamic sampling context
builder. The DSC now only carries `org_id` when the SDK actually has
one — the previous code emitted `"org_id":""` for DSNs without an
`o<digits>.` host prefix, which ran counter to the trace-propagation
spec.

Integration and envelope-serialization assertions updated to reflect
the absent field.

Preparation for strict trace continuation:
https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Handle `baggage` alongside `sentry-trace` in
`sentry_transaction_context_update_from_header`. Per W3C baggage /
RFC 7230 syntax: comma-separated members of the form `key=value`
with optional surrounding whitespace. `sentry-*` members are
collected (key stripped of the `sentry-` prefix) and their
percent-encoded values decoded into a new `incoming_dsc` object on
the transaction context's inner state. Non-sentry members are
ignored.

The `incoming_dsc` object is the input to the next step — the strict
trace continuation decision. Nothing consumes it yet.

Preparation for strict trace continuation:
https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the file-static `set_dynamic_sampling_context` from
`sentry_core.c` into `sentry_scope.c` as
`sentry__scope_rebuild_dsc_from_options`. The DSC fundamentally
belongs to the scope, and the upcoming strict-trace-continuation
work needs to call it from outside `sentry_core.c`. No behavior
change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire incoming-baggage org_id and the SDK's effective org_id through
`sentry__trace_continuation_allowed` (the spec truth table) when a
transaction starts:

- Both present and equal, both absent, or only one with strict off:
  continue. The scope DSC is frozen verbatim from the incoming DSC
  and propagated as-is from there on.
- Both present and differing: never continue.
- Exactly one present with strict on: do not continue.

When not continuing, the transaction takes a fresh `trace_id`,
drops `parent_span_id` and any inherited `sampled` flag (the sampler
re-decides), and the scope DSC is rebuilt from the SDK's own
options. The internal `incoming_dsc` carrier is stripped from the
event before sampling so it never reaches the envelope.

Outgoing baggage emission still TODO; spec compliance requires it
(`sentry-org_id` MUST be propagated). Coming next.

https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the long-standing TODO in `sentry__span_iter_headers` with a
proper `baggage` header emitter. The header is built from the scope
DSC: when the scope continued an upstream trace, the DSC was frozen
verbatim and propagation echoes upstream values "as is" per the
trace-propagation spec; otherwise the DSC was rebuilt from the SDK's
own options.

`sentry-trace_id` is always taken from the span's own `trace_id` and
emitted first, so it stays consistent with the `sentry-trace`
header. The remaining DSC fields (including `sentry-org_id` when
present) are appended with values percent-encoded per RFC 3986.

https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/#baggage-header

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Add unit tests for the new continuation pipeline:

- `trace_continuation_truth_table`: pure check of the spec truth
  table.
- `effective_org_id_resolution`: option > DSN > NULL precedence,
  empty option falls back to DSN.
- `parse_baggage_basic_and_filtering`: percent-decoding, OWS
  trimming, non-`sentry-` members ignored, malformed members skipped.
- `strict_continuation_*`: end-to-end via
  `sentry_transaction_context_update_from_header` →
  `sentry_transaction_start`, asserting both the resulting trace
  state (continued vs. forked) and the outgoing baggage emitted via
  `sentry_transaction_iter_headers` (frozen-from-upstream vs.
  rebuilt from options, including spec-required `sentry-org_id`
  propagation).
- `set_trace_rebuilds_dsc_sample_rand`: regression for the earlier
  staleness fix.

Also bumps the unreleased CHANGELOG entry now that the feature is
observable end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the trace-propagation spec, the receiving SDK must treat the
incoming Dynamic Sampling Context as instantly frozen and propagate
its values "as is". `sentry__scope_freeze_dsc_from_incoming` built
the DSC but didn't lock it, so a subsequent `sentry_set_release` /
`sentry_set_environment` call would overwrite the upstream `release`
/ `environment` values in the outgoing `baggage` header. Freeze via
`sentry_value_freeze` after the merge so the setters silently no-op
against the active trace's DSC; the scope's own fields still update
and feed the next trace's rebuilt DSC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the strict-continuation decision forks into a new trace, the
fork branch cleared `sampled` / `parent_span_id` on `tx` but not on
`tx_ctx` (which `parse_sentry_trace` still populated from the
incoming `sentry-trace` header). The subsequent
`sentry__should_send_transaction(tx_ctx, ...)` call would therefore
see the upstream `sampled` flag, treat it as `parent_sampled`, and
short-circuit to the upstream decision — bypassing the local
`traces_sample_rate` / `traces_sampler`.

Pass `tx` (already merged from `tx_ctx` and, in the fork branch,
stripped of `sampled`) to the sampler helper so the fork evaluates
sampling locally. Non-fork paths are unchanged since `tx` agrees
with `tx_ctx` on `sampled` there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`isalnum` from `<ctype.h>` is locale-dependent: in non-"C" locales
(e.g. ISO-8859-1) bytes > 127 — such as UTF-8 continuation bytes in
release / environment values — can be classified as alphanumeric and
left unencoded, producing a malformed baggage header. RFC 3986's
`unreserved` set is strict ASCII by definition, so replace the call
with a small locale-independent ASCII-range helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exercise the transaction example with fixed incoming trace and baggage flags so
integration tests cover continuation, mismatched org IDs, and missing baggage.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Drop the unused public org ID getter and use a single internal helper for the
trace propagation org ID resolved from options or the DSN.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Use shorter internal names for updating the scope DSC from SDK options and
freezing it from incoming trace continuation data.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Use a shorter internal predicate name for deciding whether an incoming trace can
continue under strict trace continuation rules.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Let the trace continuation predicate derive SDK and incoming organization IDs
from the options and incoming DSC so transaction startup only asks whether the
incoming trace can continue.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
@jpnurmi jpnurmi force-pushed the jpnurmi/feat/strict-trace-continuation branch from 63a51da to 2794991 Compare May 4, 2026 15:45
@jpnurmi jpnurmi requested review from JoshuaMoelans and mujacica May 4, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant