Skip to content

Type trace and span id signatures as Integer - #6124

Open
p-datadog wants to merge 8 commits into
masterfrom
type-trace-span-id
Open

Type trace and span id signatures as Integer#6124
p-datadog wants to merge 8 commits into
masterfrom
type-trace-span-id

Conversation

@p-datadog

@p-datadog p-datadog commented Jul 29, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Types the trace/span id family in the RBS signatures.

The type is Integer. After LLM back and forth, the actual values appear to always be integers, and are converted to strings only when sent to agent. Hence, the type is integer and not string.

Motivation:

MOAR TYPES

Change log entry

None.

How to test the change?

Existing CI

The trace/span id family was declared untyped in the RBS signatures even though
SpanOperation's constructor already typed its id params Integer. Type the
readers/ivars concretely:

- SpanOperation#id / #parent_id / #trace_id: Integer
- TraceOperation#id: Integer; #parent_span_id: Integer?
- TraceSegment#id / #root_span_id: Integer?
- TraceDigest#span_id / #trace_id: Integer?

Signature-only change; no runtime code changes. Full steep check clean.

SpanLink's ids are left untyped here: typing them makes steep infer its
to_hash payload hash as Hash[Symbol, Integer], which conflicts with the
heterogeneous values it also assigns; that needs a companion to_hash typing
change and is deferred.
@p-datadog p-datadog added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Jul 29, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Typing analysis

Note: Ignored files are excluded from the next sections.

Untyped methods

This PR introduces 2 untyped methods and 10 partially typed methods, and clears 14 untyped methods and 7 partially typed methods. It increases the percentage of typed methods from 69.55% to 69.93% (+0.38%).

Untyped methods (+2-14)Introduced:
sig/datadog/tracing/correlation.rbs:38
└── def to_h: () -> untyped
sig/datadog/tracing/correlation.rbs:39
└── def to_log_format: () -> untyped
Cleared:
sig/datadog/tracing/correlation.rbs:38
└── def initialize: (?env: untyped?, ?service: untyped?, ?span_id: untyped?, ?trace_id: untyped?, ?version: untyped?) -> void
sig/datadog/tracing/correlation.rbs:40
└── def to_h: () -> untyped
sig/datadog/tracing/correlation.rbs:41
└── def to_log_format: () -> untyped
sig/datadog/tracing/correlation.rbs:43
└── def trace_id: () -> untyped
sig/datadog/tracing/correlation.rbs:45
└── def self?.identifier_from_digest: (untyped digest) -> untyped
sig/datadog/tracing/correlation.rbs:47
└── def self?.format_trace_id: (untyped trace_id) -> untyped
sig/datadog/tracing/correlation.rbs:49
└── def self?.format_trace_id_128: (untyped trace_id) -> untyped
sig/datadog/tracing/span_link.rbs:53
└── def initialize: (untyped digest, ?attributes: untyped?) -> void
sig/datadog/tracing/span_operation.rbs:165
└── def parent=: (untyped parent) -> untyped
sig/datadog/tracing/trace_segment.rbs:85
└── def initialize: (untyped spans, ?agent_sample_rate: untyped?, ?hostname: untyped?, ?id: untyped?, ?lang: untyped?, ?name: untyped?, ?origin: untyped?, ?process_id: untyped?, ?rate_limiter_rate: untyped?, ?resource: untyped?, ?root_span_id: untyped?, ?rule_sample_rate: untyped?, ?runtime_id: untyped?, ?sample_rate: untyped?, ?sampling_priority: untyped?, ?service: untyped?, ?tags: untyped?, ?metrics: untyped?, ?profiling_enabled: untyped?, ?apm_tracing_enabled: untyped?) -> void
sig/datadog/tracing/utils.rbs:12
└── def self.next_id: () -> untyped
sig/datadog/tracing/utils.rbs:14
└── def self.id_rng: () -> untyped
sig/datadog/tracing/utils.rbs:16
└── def self.reset!: () -> untyped
sig/datadog/tracing/utils.rbs:22
└── def self?.next_id: () -> untyped
Partially typed methods (+10-7)Introduced:
sig/datadog/tracing/correlation.rbs:36
└── def initialize: (?env: untyped?, ?service: untyped?, ?span_id: Integer?, ?trace_id: Integer?, ?version: untyped?) -> void
sig/datadog/tracing/distributed/b3_multi.rbs:18
└── def inject!: (TraceDigest? digest, ?::Hash[untyped, untyped] data) -> (nil | untyped)
sig/datadog/tracing/distributed/b3_multi.rbs:20
└── def extract: (untyped data) -> TraceDigest?
sig/datadog/tracing/distributed/b3_single.rbs:12
└── def inject!: (TraceDigest? digest, untyped env) -> (nil | untyped)
sig/datadog/tracing/distributed/b3_single.rbs:14
└── def extract: (untyped env) -> TraceDigest?
sig/datadog/tracing/distributed/helpers.rbs:7
└── def self.parse_decimal_id: (untyped value) -> ::Integer?
sig/datadog/tracing/distributed/helpers.rbs:9
└── def self.parse_hex_id: (untyped value) -> ::Integer?
sig/datadog/tracing/span_link.rbs:53
└── def initialize: (TraceDigest digest, ?attributes: untyped?) -> void
sig/datadog/tracing/trace_digest.rbs:27
└── def initialize: (?span_id: Integer?, ?span_name: untyped?, ?span_resource: untyped?, ?span_service: untyped?, ?span_type: untyped?, ?span_links: ::Array[SpanLink]?, ?trace_distributed_tags: untyped?, ?trace_hostname: untyped?, ?trace_id: Integer?, ?trace_name: untyped?, ?trace_origin: untyped?, ?trace_process_id: untyped?, ?trace_resource: untyped?, ?trace_runtime_id: untyped?, ?trace_sampling_priority: untyped?, ?trace_service: untyped?, ?trace_distributed_id: untyped?, ?trace_flags: untyped?, ?trace_state: untyped?, ?trace_state_unknown_fields: untyped?, ?span_remote: untyped?, ?baggage: untyped?) -> void
sig/datadog/tracing/trace_segment.rbs:85
└── def initialize: (untyped spans, ?agent_sample_rate: untyped?, ?hostname: untyped?, ?id: Integer?, ?lang: untyped?, ?name: untyped?, ?origin: untyped?, ?process_id: untyped?, ?rate_limiter_rate: untyped?, ?resource: untyped?, ?root_span_id: Integer?, ?rule_sample_rate: untyped?, ?runtime_id: untyped?, ?sample_rate: untyped?, ?sampling_priority: untyped?, ?service: untyped?, ?tags: untyped?, ?metrics: untyped?, ?profiling_enabled: untyped?, ?apm_tracing_enabled: untyped?) -> void
Cleared:
sig/datadog/tracing/distributed/b3_multi.rbs:18
└── def inject!: (untyped digest, ?::Hash[untyped, untyped] data) -> (nil | untyped)
sig/datadog/tracing/distributed/b3_multi.rbs:20
└── def extract: (untyped data) -> (nil | untyped)
sig/datadog/tracing/distributed/b3_single.rbs:12
└── def inject!: (untyped digest, untyped env) -> (nil | untyped)
sig/datadog/tracing/distributed/b3_single.rbs:14
└── def extract: (untyped env) -> (nil | untyped)
sig/datadog/tracing/distributed/helpers.rbs:7
└── def self.parse_decimal_id: (untyped value) -> (nil | untyped)
sig/datadog/tracing/distributed/helpers.rbs:9
└── def self.parse_hex_id: (untyped value) -> (nil | untyped)
sig/datadog/tracing/trace_digest.rbs:27
└── def initialize: (?span_id: untyped?, ?span_name: untyped?, ?span_resource: untyped?, ?span_service: untyped?, ?span_type: untyped?, ?span_links: ::Array[SpanLink]?, ?trace_distributed_tags: untyped?, ?trace_hostname: untyped?, ?trace_id: untyped?, ?trace_name: untyped?, ?trace_origin: untyped?, ?trace_process_id: untyped?, ?trace_resource: untyped?, ?trace_runtime_id: untyped?, ?trace_sampling_priority: untyped?, ?trace_service: untyped?, ?trace_distributed_id: untyped?, ?trace_flags: untyped?, ?trace_state: untyped?, ?trace_state_unknown_fields: untyped?, ?span_remote: untyped?, ?baggage: untyped?) -> void

Untyped other declarations

This PR clears 13 untyped other declarations. It increases the percentage of typed other declarations from 84.88% to 85.51% (+0.63%).

Untyped other declarations (+0-13)Cleared:
sig/datadog/tracing/span_operation.rbs:24
└── attr_reader id: untyped
sig/datadog/tracing/span_operation.rbs:28
└── attr_reader parent_id: untyped
sig/datadog/tracing/span_operation.rbs:36
└── attr_reader trace_id: untyped
sig/datadog/tracing/span_operation.rbs:159
└── attr_reader parent: untyped
sig/datadog/tracing/trace_digest.rbs:4
└── attr_reader span_id: untyped
sig/datadog/tracing/trace_digest.rbs:12
└── attr_reader trace_id: untyped
sig/datadog/tracing/trace_segment.rbs:4
└── @id: untyped
sig/datadog/tracing/trace_segment.rbs:6
└── @root_span_id: untyped
sig/datadog/tracing/trace_segment.rbs:50
└── attr_reader id: untyped
sig/datadog/tracing/trace_segment.rbs:102
└── attr_reader root_span_id: untyped
sig/datadog/tracing/utils.rbs:6
└── RUBY_MAX_ID: untyped
sig/datadog/tracing/utils.rbs:8
└── EXTERNAL_MAX_ID: untyped
sig/datadog/tracing/utils.rbs:21
└── MAX: 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.

p-ddsign added 2 commits July 29, 2026 13:14
Corrects @trace_id ivar from String to Integer (it holds trace_id || 0),
types Identifier#initialize span_id/trace_id params, the trace_id reader,
and the format_trace_id/format_trace_id_128/identifier_from_digest signatures.
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 90.02% (+0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: c3d4cd8 | Docs | Datadog PR Page | Give us feedback!

p-ddsign added 4 commits July 29, 2026 13:18
Types inject! digest params as TraceDigest? and extract returns as
TraceDigest?, matching the _Propagator interface and the Datadog propagator,
so digest.trace_id/span_id reads see the concrete Integer? id fields.
Reads parent.id/parent.trace_id (both Integer) when setting @parent_id/@trace_id.
Makes digest.span_id/trace_id reads resolve to the concrete Integer? id fields;
SpanLink's own span_id/trace_id readers stay untyped (deferred by the base PR).
@p-datadog
p-datadog marked this pull request as ready for review July 29, 2026 21:20
@p-datadog

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 86fd9b2184

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI 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.

Pull request overview

This PR tightens the RBS signatures around trace/span identifier handling in Datadog::Tracing, making IDs consistently typed as Integer/Integer? across tracing utilities, core trace/span data structures, and distributed propagation helpers.

Changes:

  • Types trace/span ID constants and generators in Datadog::Tracing::Utils / TraceId as Integer.
  • Types trace/span IDs on key tracing structures (TraceOperation, TraceSegment, TraceDigest, SpanOperation) as Integer/Integer?.
  • Narrows distributed propagation and linking interfaces to return/accept TraceDigest and parse IDs as Integer?.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
sig/datadog/tracing/utils.rbs Types ID constants and ID generator methods as Integer and Random.
sig/datadog/tracing/trace_segment.rbs Types trace segment id / root_span_id as Integer? and updates initializer accordingly.
sig/datadog/tracing/trace_operation.rbs Types trace operation id as Integer and parent_span_id as Integer?.
sig/datadog/tracing/trace_digest.rbs Types trace_id / span_id as Integer? and updates initializer accordingly.
sig/datadog/tracing/span_operation.rbs Types span operation IDs as Integer and tightens the parent relationship types.
sig/datadog/tracing/span_link.rbs Narrows SpanLink#initialize to accept a TraceDigest.
sig/datadog/tracing/distributed/trace_context.rbs Types inject!/extract to accept/return TraceDigest? consistently.
sig/datadog/tracing/distributed/helpers.rbs Types parse_decimal_id / parse_hex_id to return Integer?.
sig/datadog/tracing/distributed/b3_single.rbs Types propagation inject!/extract around TraceDigest?.
sig/datadog/tracing/distributed/b3_multi.rbs Types propagation inject!/extract around TraceDigest?.
sig/datadog/tracing/correlation.rbs Types correlation identifier trace/span id-related fields (with one signature narrowness issue noted).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


attr_reader version: String
def initialize: (?env: untyped?, ?service: untyped?, ?span_id: untyped?, ?trace_id: untyped?, ?version: untyped?) -> void
def initialize: (?env: untyped?, ?service: untyped?, ?span_id: Integer?, ?trace_id: Integer?, ?version: untyped?) -> void

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point on the runtime — #initialize did stringify through (span_id || 0).to_s, so it tolerated any #to_s-able value. That was incidental permissiveness rather than a supported contract, though: #initialize is @!visibility private, every construction site passes an Integer (TraceOperation#to_correlation@active_span&.id; identifier_from_digestTraceDigest#span_id), and a span id is an Integer everywhere else it's held (Span#id, SpanOperation#id, TraceDigest#span_id, Utils.next_id). No caller — and no spec — passes a String.

It's also the cross-tracer convention: the span id is modeled numerically, with strings appearing only at the log-output boundary.

Tracer span_id trace_id Surface
Java String String CorrelationIdentifier.getSpanId()/getTraceId()
Python str (from int) str get_log_correlation_context() -> dict[str, str]
JS string string spanContext.toSpanId()/toTraceId()
.NET ulong ulong CorrelationIdentifier.SpanId/TraceId
PHP string (hex) string (hex) current_context(): array{span_id: string, …}
Go uint64 string / uint64 SpanContext.SpanID()/TraceID()/TraceIDLower()
Ruby Integer (rendered String for logs) Integer (rendered String for logs) Correlation::Identifier

So rather than widen the type, I removed the storage-time .to_s so the value has one type end to end: @span_id is now Integer (matching @trace_id), and the String is produced only when rendering for logs — the #span_id/#trace_id readers, #to_h, and #to_log_format. The public #span_id reader still returns a String, so there's no observable change and the specs pass unchanged. Fixed in c3d4cd8.

The span id is an Integer everywhere it is held (Utils.next_id, Span#id,
SpanOperation#id, TraceDigest#span_id) and every construction site of
Correlation::Identifier passes an Integer (or nil); #initialize is
@!visibility private, so no caller supplies a String. Storing it via
(span_id || 0).to_s gave the one value two types (Integer at the param,
String in the ivar/reader).

Store @span_id as Integer (mirroring @trace_id) and render the String only
at the log-output boundary: the #span_id reader now formats with #to_s, the
same way #trace_id formats via format_trace_id. The public #span_id reader
still returns a String, so there is no observable change; the existing specs
pass unchanged.
@dd-octo-sts dd-octo-sts Bot added logging Log Management product tracing labels Jul 29, 2026
@pr-commenter

pr-commenter Bot commented Jul 30, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-30 00:08:09

Comparing candidate commit c3d4cd8 in PR branch type-trace-span-id with baseline commit 90f3c3c in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 1 performance regressions! Performance is the same for 47 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:profiling - Allocations ()

  • 🟥 throughput [-376686.879op/s; -329599.936op/s] or [-11.599%; -10.149%]

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:tracing - trace.to_digest - Continue

  • unstable throughput [-1713.082op/s; +1288.816op/s] or [-5.791%; +4.357%]

@Strech Strech left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's a good step forward, but the method extraction altered the behavior in case of a nil input

@env = env || Datadog.configuration.env
@service = service || Datadog.configuration.service
@span_id = (span_id || 0).to_s
@span_id = span_id || 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With that method, span_id will always return string, even with nil it will be "", so that leads to 0 fallback never happening and this is altering existing behavior.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

span_id here is the parameter, not the instance attribute, which I think is what you are referring to.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, it's confusing because now we have a method that will be shadowed by this argument

def span_id
  @span_id.to_s
end

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What is confusing about it? attr_reader does the exact same thing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Confusing part is that reader is not going to perform to_s on every read, now it will be

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos logging Log Management product tracing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants