Skip to content

[WIP] Before finish events#5535

Draft
TonyCTHsu wants to merge 2 commits intomasterfrom
tonycthsu/before_finish_event
Draft

[WIP] Before finish events#5535
TonyCTHsu wants to merge 2 commits intomasterfrom
tonycthsu/before_finish_event

Conversation

@TonyCTHsu
Copy link
Copy Markdown
Contributor

@TonyCTHsu TonyCTHsu commented Apr 1, 2026

What does this PR do?

Introduces a before_finish hook on SpanOperation and a corresponding span_before_finish event
on TraceOperation. Moves the _dd.base_service tag-writing logic out of each individual
integration and into a single centralized tracer subscriber that fires on every span finish.

Motivation:

The _dd.base_service tag needs to be set consistently on every span, regardless of which
integration produced it. Before this change, the logic was copy-pasted across ~26 integrations.
This created two problems:

  1. Correctness drift: any integration that omitted or misimplemented the tag would produce
    incorrect service naming without any obvious failure signal.
  2. Wrong timing: integrations set the tag during their own finish logic, before build_span
    freezes the SpanOperation into an immutable Span. The hook makes the timing contract
    explicit — subscribers are guaranteed a write window immediately before build_span is called.

A before_finish hook is the right abstraction here because the tag must be computed from the
final state of the span (service name is known, parent context is resolved) but must be written
before the span is frozen. Neither before_start (service not yet set) nor after_finish (span
is immutable) satisfies this constraint.

Change log entry

None.

Additional Notes:

The hook follows the same pattern as the existing before_start / after_finish / after_stop
events, so no new mechanism is introduced. The SpanOperation::Events::BeforeFinish
TraceOperation::Events::SpanBeforeFinish → tracer subscriber chain mirrors how before_start
propagates today.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

Thank you for updating Change log entry section 👏

Visited at: 2026-04-15 09:21:37 UTC

@github-actions github-actions bot added integrations Involves tracing integrations tracing labels Apr 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

Typing analysis

Note: Ignored files are excluded from the next sections.

steep:ignore comments

This PR introduces 2 steep:ignore comments, and clears 2 steep:ignore comments.

steep:ignore comments (+2-2)Introduced:
lib/datadog/tracing/span_operation.rb:328
lib/datadog/tracing/span_operation.rb:400
Cleared:
lib/datadog/tracing/span_operation.rb:325
lib/datadog/tracing/span_operation.rb:397

Untyped methods

This PR introduces 6 untyped methods and 2 partially typed methods, and clears 6 untyped methods and 2 partially typed methods.

Untyped methods (+6-6)Introduced:
sig/datadog/tracing/span_operation.rbs:145
└── def wrap_default: () { (untyped, untyped) -> untyped } -> untyped
sig/datadog/tracing/span_operation.rbs:166
└── def build_span: () -> untyped
sig/datadog/tracing/span_operation.rbs:168
└── def parent=: (untyped parent) -> untyped
sig/datadog/tracing/span_operation.rbs:170
└── def duration_marker: () -> untyped
sig/datadog/tracing/span_operation.rbs:172
└── def start_time_nano: () -> untyped
sig/datadog/tracing/span_operation.rbs:174
└── def duration_nano: () -> untyped
Cleared:
sig/datadog/tracing/span_operation.rbs:139
└── def wrap_default: () { (untyped, untyped) -> untyped } -> untyped
sig/datadog/tracing/span_operation.rbs:160
└── def build_span: () -> untyped
sig/datadog/tracing/span_operation.rbs:162
└── def parent=: (untyped parent) -> untyped
sig/datadog/tracing/span_operation.rbs:164
└── def duration_marker: () -> untyped
sig/datadog/tracing/span_operation.rbs:166
└── def start_time_nano: () -> untyped
sig/datadog/tracing/span_operation.rbs:168
└── def duration_nano: () -> untyped
Partially typed methods (+2-2)Introduced:
sig/datadog/tracing/span_operation.rbs:143
└── def initialize: (untyped default, ?logger: Core::Logger) -> void
sig/datadog/tracing/span_operation.rbs:147
└── def publish: (*untyped args) -> true
Cleared:
sig/datadog/tracing/span_operation.rbs:137
└── def initialize: (untyped default, ?logger: Core::Logger) -> void
sig/datadog/tracing/span_operation.rbs:141
└── def publish: (*untyped args) -> true

Untyped other declarations

This PR introduces 5 untyped other declarations, and clears 4 untyped other declarations. It decreases the percentage of typed other declarations from 77.96% to 77.92% (-0.04%).

Untyped other declarations (+5-4)Introduced:
sig/datadog/tracing/span_operation.rbs:114
└── attr_reader before_finish: untyped
sig/datadog/tracing/span_operation.rbs:116
└── attr_reader before_start: untyped
sig/datadog/tracing/span_operation.rbs:157
└── attr_reader events: untyped
sig/datadog/tracing/span_operation.rbs:159
└── attr_reader parent: untyped
sig/datadog/tracing/span_operation.rbs:161
└── attr_reader span: untyped
Cleared:
sig/datadog/tracing/span_operation.rbs:114
└── attr_reader before_start: untyped
sig/datadog/tracing/span_operation.rbs:151
└── attr_reader events: untyped
sig/datadog/tracing/span_operation.rbs:153
└── attr_reader parent: untyped
sig/datadog/tracing/span_operation.rbs:155
└── attr_reader span: untyped

If you believe a method or an attribute is rightfully untyped or partially typed, you can add # untyped:accept on the line before the definition to remove it from the stats.

@datadog-prod-us1-6
Copy link
Copy Markdown

datadog-prod-us1-6 bot commented Apr 1, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 96.83%
Overall Coverage: 96.36% (+1.00%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 7ad9047 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

@TonyCTHsu TonyCTHsu force-pushed the tonycthsu/before_finish_event branch from 74333c7 to c6b3f68 Compare April 14, 2026 10:36
@pr-commenter
Copy link
Copy Markdown

pr-commenter bot commented Apr 14, 2026

Benchmarks

Benchmark execution time: 2026-04-14 11:05:11

Comparing candidate commit c6b3f68 in PR branch tonycthsu/before_finish_event with baseline commit 3260714 in branch master.

Found 0 performance improvements and 2 performance regressions! Performance is the same for 43 metrics, 1 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:tracing - 10 span trace - no writer

  • 🟥 throughput [-180.797op/s; -175.679op/s] or [-6.786%; -6.594%]

scenario:tracing - 100 span trace - no writer

  • 🟥 throughput [-24.198op/s; -23.800op/s] or [-7.568%; -7.443%]

`_dd.base_service` is now set via the `before_finish` hook in
`SpanOperation#finish`, so tests must operate on a finished span.

Move `environment service name` and `schema version span` examples from
`describe '#perform'` (where `complete` is mocked and the span is never
finished) to `describe '#complete'` (where the full lifecycle runs).
Remove those two examples from inside the ethon `'span'` shared example,
since `'span'` is also used with open `SpanOperation`s in `#perform`.
Drop the `span.finish` workaround from both shared examples now that no
caller passes an unfinished span.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integrations Involves tracing integrations tracing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants