Skip to content

fix(tracing): report spans that outlive their sent parent - #1163

Open
solnic wants to merge 1 commit into
masterfrom
fix/otel-orphan-span-promotion
Open

fix(tracing): report spans that outlive their sent parent#1163
solnic wants to merge 1 commit into
masterfrom
fix/otel-orphan-span-promotion

Conversation

@solnic

@solnic solnic commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

A span that outlives its transaction root (ie async work via Tasks/Broadway/Oban continuing a trace after the root was reported) was either corrupted or lost:

  • If it was still running when the root ended, it was serialized into the root transaction with "timestamp": null — Relay patches that into a zero-duration span with a bogus deadline_exceeded status (first screenshot; the span actually ran for 500ms and succeeded).
  • Once it finished, it was silently dropped.

Unfinished children are now excluded from the transaction payload, and sent transaction roots leave a short-lived marker in span storage. A span ending with a marked parent is promoted to a follow-up transaction in the same trace - same trace_id, parent_span_id pointing at the sent root, tagged sentry.parent_span_already_sent: true.

Before

image image

After

image

Fixes #1011

@solnic solnic changed the title fix(tracing): report spans that outlive their sent parent (#1011) fix(tracing): report spans that outlive their sent parent Aug 7, 2026
@solnic
solnic marked this pull request as ready for review August 7, 2026 13:49
Comment thread lib/sentry/opentelemetry/span_processor.ex Outdated
|> SpanStorage.get_child_spans()
|> Enum.filter(& &1.end_time)

transaction = build_transaction(span_record, child_span_records, opts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Finished grandchildren reported in two transactions

Medium Severity

get_child_spans returns all descendants, so a finished grandchild whose direct parent is still in flight is included in the root transaction even though its parent is filtered out, leaving it orphaned. That record is not removed (removal only touches direct children), so when the in-flight parent later ends and is promoted to a follow-up transaction, the same span is sent a second time.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b822b9b. Configure here.

Comment thread lib/sentry/opentelemetry/span_processor.ex Outdated
build_and_send_transaction(span_record)

true ->
true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deeper orphaned spans still silently dropped

Low Severity

Only transaction roots get a sent marker, so promotion works one level deep. A span whose direct parent was an ordinary child of the sent root finds neither a marker nor a stored parent when it ends, and falls through to the no-op branch. Such spans were previously emitted (with a null timestamp) inside the root transaction and are now lost entirely.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b822b9b. Configure here.

@solnic
solnic force-pushed the fix/otel-orphan-span-promotion branch from b822b9b to 203b3c5 Compare August 7, 2026 14:44
Comment on lines +104 to 108
# that the transaction was finalized locally - not that delivery
# succeeded - since once the records are removed below, later spans can
# never be attached to this transaction either way.
:ok = SpanStorage.mark_span_sent(span_record.span_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Grandchild spans can be duplicated across transactions if their parent span outlives the root span, because the cleanup logic in remove_child_spans is not recursive.
Severity: MEDIUM

Suggested Fix

Update remove_child_spans to recursively remove all descendant spans, not just direct children. This would ensure that when a root transaction is processed, all its finished descendants are cleaned up from storage, preventing them from being included in a subsequent follow-up transaction.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: lib/sentry/opentelemetry/span_processor.ex#L104-L108

Potential issue: In a nested span hierarchy (e.g., root `R`, child `P`, grandchild `G`),
if the grandchild `G` finishes but its parent `P` outlives the root `R`, a duplication
issue occurs. When `R` finishes, `get_child_spans` recursively finds and includes `G` in
the root transaction. However, the cleanup function `remove_child_spans` only removes
direct children, leaving `G` in storage. Later, when `P` finishes and is promoted to a
new transaction, `get_child_spans` is called for `P`, which finds and includes `G` a
second time. This results in the grandchild span appearing in both the root transaction
and the follow-up transaction.

Also affects:

  • lib/sentry/opentelemetry/span_storage.ex:230~243

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 3 total unresolved issues (including 2 from previous reviews).

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 203b3c5. Configure here.


# Parent exists locally - this is a child span, not a transaction root
has_local_parent_span?(span_record.parent_span_id) ->
true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nested late spans lose promotion path

Medium Severity

Promotion only triggers when the immediate parent carries a sent marker. If that parent finished before the root, it gets deleted from storage when the root transaction is sent and is never marked, so a still-running grandchild ends up with neither a stored nor a marked parent and is silently discarded once it finishes — previously it was at least present in the root payload.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 203b3c5. Configure here.

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.

SpanProcessor drops child spans that start after the root ends

1 participant