Skip to content

feat(open_feature): FFE APM feature-flag span enrichment (experimental, gated)#5910

Draft
leoromanovsky wants to merge 10 commits into
masterfrom
leo.romanovsky/ffe-apm-span-enrichment
Draft

feat(open_feature): FFE APM feature-flag span enrichment (experimental, gated)#5910
leoromanovsky wants to merge 10 commits into
masterfrom
leo.romanovsky/ffe-apm-span-enrichment

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

feat(open_feature): FFE APM feature-flag span enrichment

⚠️ Experimental, opt-in, gated behind DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED (off by default).

Summary

Adds Feature Flag Events (FFE) span enrichment to the OpenFeature integration. When feature
flags are evaluated, the evaluation metadata is attached to the root APM span so APM customers
can filter traces and errors by active flag variant, and the FFE/Experimentation platform can
correlate spans with experiments. The wire format matches the merged reference implementation
(dd-trace-js#8343) so backend/Trino decode is identical.

How it works

  1. A flag is evaluated through the OpenFeature client.
  2. The capture hook records the evaluation (serial ID, targeting key, runtime default).
  3. Evaluations are accumulated against the local root span.
  4. On local-root finish, the accumulated state is encoded and written as ffe_* tags.

Configuration

Opt-in, off by default:

DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED=true

This is distinct from DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED.

Span tags added

Tag Description Format
ffe_flags_enc All evaluated flag serial IDs base64 delta-varint
ffe_subjects_enc Subject → flags mapping (when __dd_do_log=true) JSON { sha256(key): encodedIds }
ffe_runtime_defaults Fallback values for flags not in UFC JSON { flagKey: value }

Limits: 200 serial IDs, 10 subjects, 20 experiments/subject, 5 runtime defaults, 64 chars/runtime-default value (UTF-8-safe truncation).

Changes

  • Gate + config: add DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED (open_feature/configuration.rb, supported_configurations), off by default; bind the split serial_id over FFI (ext/libdatadog_api/feature_flags.c).
  • Codec + hook: open_feature/hooks/span_enrichment_hook.rb — ULEB128 / delta-varint serial IDs, SHA256-hashed subject keys; accumulator + capture hook write ffe_* tags to the local root span; provider/component wiring.
  • Tests + sigs: L0 spec + child-before-root cleanup regression; RBS signatures and vendored OpenFeature RBS.

Decisions

  • Varint encoding fix: encode_varint/encode_delta_varint originally built their byte buffer as a UTF-8 string (+''), so String#<< with an integer ≥ 0x80 appended a 2-byte Unicode codepoint — corrupting every serial ID ≥ 128 (serial 2312 decoded to 296002). Fixed to binary buffers ((+'').b), with a regression spec.
  • No idle per-span overhead when the gate is off — the accumulator is absent and spans carry no ffe_* tags.
  • Lifecycle: state cleaned up only on local-root finish.
  • ffe_* are bare tag names on span meta (not _dd.-prefixed); subject keys are SHA256 hashes emitted only when logging is authorized.

Validation

FFE dogfooding app

Validated live against the ffe-dogfooding app via a trace-intake tee-proxy that captures the raw /v0.4/traces payload and decodes the ffe_* tags. Flag ffe-dogfooding-string-flag (serial 2312):

  • Gate ON — the root span carried ffe_flags_enc decoding to serial [2312] plus a SHA256-hashed ffe_subjects_enc[2312] (confirming the binary-buffer varint fix; previously this emitted the corrupted 296002).
  • Gate OFF — span flushed with zero ffe_* tags.

Local system-tests run

Ran the frozen system-tests parametric suite (tests/parametric/test_ffe/test_span_enrichment.py, unchanged) against this branch's gem (datadog-2.36.0.dev, libdatadog-33.0.0.1.0):

TEST_LIBRARY=ruby ./run.sh PARAMETRIC -k span_enrichment
============================= 18 passed in 30.48s ==============================

All 18 cases pass, covering the delta-varint codec (serial 2312 → bytes 88 12), multi-flag/multi-subject aggregation onto one root span, child-span → root propagation, the 200/10/20/5/64 limits, and SHA256 subject keys gated on __dd_do_log. No SDK source changes beyond the varint fix were required. The system-tests enablement (parametric server.rb + manifests/ruby.yml) is a separate draft PR against DataDog/system-tests.

Harness note: the Ruby parametric Dockerfile pins --platform=linux/amd64; on an arm64 host the in-container native-ext build segfaults under QEMU. The validation run used a local-only workaround (drop the pin so the ext compiles natively on arm64); the gem and enrichment behavior are identical on either platform and the workaround is not part of any PR.

Full dogfooding matrix + system-tests (2026-06-17)

Re-validated end-to-end through the real OpenFeature client (gem built from this branch) behind
the trace-intake tee-proxy, decoding ffe_* with scripts/decode_ffe_span_tags.py (root span
scenario, service ffe-dogfooding-ruby):

Scenario Result
Gate ON (serial 2312) ffe_flags_enc[2312]; ffe_subjects_enc = {sha256(targeting key): ids} only when do_log
Gate OFF zero ffe_* tags; no hook constructed
Aggregation multiple flags + 2 subjects on one root → ffe_flags_enc = [829,1442,2311,2312], nothing overwritten
Child→root eval inside a child span → ffe_* on the local root only; the child carries none
Unicode + object runtime defaults ffe_runtime_defaults raw UTF-8 (héllo-wörld-☃-日本語-Ω, こんにちは, 🎉), valid JSON, codepoint-safe 64-char truncation (binary varint buffers encode serial ids ≥128 correctly)
Codec parity ZAgUAg==[100,108,128,130]

Concurrency / lifecycle / reconfiguration are covered by the Mutex-guarded accumulator,
ObjectSpace::WeakMap per-trace keying, and provider-path dispatch in this PR.

System-tests: 18 passed (TEST_LIBRARY=ruby ./run.sh PARAMETRIC -k span_enrichment, library
ruby@2.36.0.dev). No SDK source change was required to pass; the only change needed was in the
system-tests Ruby parametric harness (re-activate the test's root trace operation per
/ffe/evaluate so evaluations aggregate onto the test root rather than forking a per-call trace).

… gate

- Bind ddog_ffe_assignment_get_serial_id in feature_flags.c, converting the
  ddog_Option_I32 tagged union (SOME -> Integer, NONE -> nil)
- Register serial_id on the ResolutionDetails C class; add :serial_id to the
  Ruby ResolutionDetails Struct (nil in build_error)
- Thread __dd_split_serial_id + __dd_do_log into build_flag_metadata, read off
  the Struct (flag_metadata native path disabled, FFL-1450)
- Add span_enrichment_enabled gate (DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED),
  distinct from the provider gate, default off; register the env var
- RBS for serial_id on both ResolutionDetails classes; C-binding spec asserts
  the NONE->nil path against real libdatadog
…lator, capture, write

- Codec golden vector ZAgUAg== + empty + dedupe/sort + round-trip + SHA256
- Accumulator limits 200/10/20/5/64, JSON object shapes, first-wins, truncation
- finally capture: serial_id, subject (do_log+targeting_key), runtime-default
  via missing variant, no-active-root, error isolation
- root-span write integration (ffe_* on finish + state cleanup) and shutdown
…re hook

- Add SpanEnrichmentHook with embedded Codec (ULEB128 delta-varint + base64,
  SHA256), Accumulator (limits 200/10/20/5/64, dedupe, JSON object shapes,
  first-wins, 64-char truncation), and per-root AccumulatorStore
- finally hook captures serial_id/subject/runtime-default (missing-variant
  detection) keyed to the active root span; error-isolated (never raises)
- Write ffe_flags_enc (bare base64), ffe_subjects_enc + ffe_runtime_defaults
  (JSON objects) on the local root via span_before_finish, then delete state
- Gate-gated construction in component (DG-005: nothing built when off);
  symmetric teardown via shutdown on component.shutdown!
- provider.hooks returns [flag_eval, span_enrichment].compact
- RBS for the hook + settings gate + component accessor; vendor OF SDK RBS
  gains value/evaluation_context; Steepfile loads base64; steep + standard clean
- Spec covers all 7 required L0 cases incl. gate-off control + codec golden
…anup (CR-01)

- Reproduces CR-01: a child span finishing before the local root span
  triggers the span_before_finish handler, which (pre-fix) unconditionally
  deletes the per-trace accumulator in its ensure block, so the root emits
  no ffe_* tags.
- New spec builds a trace with a nested child that finishes first, evaluates
  a flag (serial id + subject), then finishes the root and asserts the root
  carries ffe_flags_enc / ffe_subjects_enc and state is cleaned up once.
- Companion spec: a flag evaluated after a child has already finished still
  reaches the root write.
- Fails against current code (root ffe_flags_enc == nil).
…(CR-01)

- write_tags_on_root subscribes to span_before_finish, which fires for every
  span in the trace. The cleanup (@store.delete / @subscribed.delete) was in
  an unconditional ensure block, so it ran on every span finish, including
  child finishes that hit the early non-root return.
- In any nested trace the child finishes before the local root, so the first
  child finish wiped the per-trace accumulator before the root could write
  its tags, leaving the root with no ffe_* tags.
- Move the delete inside the root-only branch (guarded by the root-span
  check), mirroring the Node reference's spanStates.delete(span) and the
  Python sibling's _on_span_finish pop: cleanup happens exactly once, when
  the local root span finishes, and a child finish never touches state.
- Contract/codec/C-binding/limits/gate-off behavior unchanged; signatures
  unchanged (RBS/steep clean).
encode_varint / encode_delta_varint built their byte buffers as UTF-8 strings,
so String#<< with an Integer >= 0x80 appended a Unicode codepoint (a 2-byte
UTF-8 sequence) instead of a raw byte. This corrupted every serial id whose
varint contains a continuation byte: serial 2312 (bytes 88 12) was emitted as
C2 88 12, decoding to 296002. ffe_flags_enc / ffe_subjects_enc were wrong for
any serial id >= 128.

Use binary (ASCII-8BIT) buffers so << appends raw bytes. Add a regression spec
covering continuation-byte serial ids (the existing golden vector's deltas are
all < 128, so it never exercised this path).
@dd-octo-sts dd-octo-sts Bot added the core Involves Datadog core libraries label Jun 16, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

👋 Hey @DataDog/ruby-guild, please fill "Change log entry" section in the pull request description.

If changes need to be present in CHANGELOG.md you can state it this way

**Change log entry**

Yes. A brief summary to be placed into the CHANGELOG.md

(possible answers Yes/Yep/Yeah)

Or you can opt out like that

**Change log entry**

None.

(possible answers No/Nope/None)

Visited at: 2026-06-17 12:30:50 UTC

@dd-octo-sts

dd-octo-sts Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Typing analysis

Note: Ignored files are excluded from the next sections.

steep:ignore comments

This PR introduces 2 steep:ignore comments.

steep:ignore comments (+2-0)Introduced:
lib/datadog/open_feature/hooks/span_enrichment_hook.rb:205
lib/datadog/open_feature/hooks/span_enrichment_hook.rb:232

Untyped methods

This PR introduces 12 partially typed methods, and clears 3 partially typed methods. It decreases the percentage of typed methods from 65.28% to 65.25% (-0.03%).

Partially typed methods (+12-3)Introduced:
sig/datadog/core/feature_flags.rbs:50
└── def json?: (untyped) -> bool
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:48
└── def add_default: (::String flag_key, untyped value) -> void
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:67
└── def fetch: (untyped trace_op) -> Accumulator?
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:69
└── def fetch_or_create: (untyped trace_op) -> [ Accumulator, bool ]
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:71
└── def delete: (untyped trace_op) -> void
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:84
└── def capture: (
          flag_key: ::String,
          variant: ::String?,
          value: untyped,
          serial_id: ::Integer?,
          do_log: bool,
          targeting_key: ::String?
        ) -> void
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:93
└── def finally: (
          hook_context: ::OpenFeature::SDK::Hooks::HookContext,
          evaluation_details: ::OpenFeature::SDK::EvaluationDetails,
          **untyped _opts
        ) -> void
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:103
└── def state_for: (untyped trace_op) -> Accumulator
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:105
└── def subscribe_root_finish: (untyped trace_op, Accumulator accumulator) -> void
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:107
└── def write_tags_on_root: (untyped span_op, untyped trace_op, Accumulator accumulator) -> void
sig/datadog/open_feature/resolution_details.rbs:26
└── def self.new: (
        ?value: untyped,
        ?reason: ::String?,
        ?variant: ::String?,
        ?error_code: ::String?,
        ?error_message: ::String?,
        ?flag_metadata: ::Hash[::String, untyped]?,
        ?allocation_key: ::String?,
        ?serial_id: ::Integer?,
        ?extra_logging: ::Hash[::String, untyped]?,
        ?log?: bool?,
        ?error?: bool?
      ) -> ResolutionDetails
sig/datadog/open_feature/resolution_details.rbs:40
└── def self.build_error: (
        value: untyped,
        error_code: ::String,
        error_message: ::String,
        ?reason: ::String
      ) -> ResolutionDetails
Cleared:
sig/datadog/core/feature_flags.rbs:48
└── def json?: (untyped) -> bool
sig/datadog/open_feature/resolution_details.rbs:24
└── def self.new: (
        ?value: untyped,
        ?reason: ::String?,
        ?variant: ::String?,
        ?error_code: ::String?,
        ?error_message: ::String?,
        ?flag_metadata: ::Hash[::String, untyped]?,
        ?allocation_key: ::String?,
        ?extra_logging: ::Hash[::String, untyped]?,
        ?log?: bool?,
        ?error?: bool?
      ) -> ResolutionDetails
sig/datadog/open_feature/resolution_details.rbs:37
└── def self.build_error: (
        value: untyped,
        error_code: ::String,
        error_message: ::String,
        ?reason: ::String
      ) -> ResolutionDetails

Untyped other declarations

This PR introduces 1 untyped other declaration and 1 partially typed other declaration, and clears 1 partially typed other declaration. It increases the percentage of typed other declarations from 82.62% to 82.75% (+0.13%).

Untyped other declarations (+1-0)Introduced:
sig/datadog/open_feature/hooks/span_enrichment_hook.rbs:63
└── @states: untyped
Partially typed other declarations (+1-1)Introduced:
sig/datadog/open_feature/resolution_details.rbs:20
└── attr_accessor extra_logging: ::Hash[::String, untyped]?
Cleared:
sig/datadog/open_feature/resolution_details.rbs:18
└── attr_accessor extra_logging: ::Hash[::String, 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-5

datadog-prod-us1-5 Bot commented Jun 16, 2026

Copy link
Copy Markdown

Pipelines  Tests

Fix all issues with BitsAI

⚠️ Warnings

🚦 6 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-rb | ruby-app-deployment-mode.amd64.DOA9: [public.ecr.aws/lts/ubuntu:22.04, linux/amd64, 3.1.7]   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-rb | ruby-app-deployment-mode.amd64.DOC: [public.ecr.aws/lts/ubuntu:22.04, linux/amd64, 3.1.7]   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-rb | ruby-app-deployment-mode.arm64.DOA9: [public.ecr.aws/lts/ubuntu:22.04, linux/arm64, 3.1.7]   View in Datadog   GitLab

View all 6 failed jobs.

❄️ 8 New flaky tests detected

OpenFeature provider span enrichment (end-to-end) when the gate is OFF (no hook constructed) writes no ffe_* tags and stays fully inert from rspec   View in Datadog
#&lt;InstanceDouble(Datadog::OpenFeature::Component) (anonymous)&gt; received unexpected message :flag_eval_hook with (no args)

Failure/Error: component&amp;.flag_eval_hook,
  #&lt;InstanceDouble(Datadog::OpenFeature::Component) (anonymous)&gt; received unexpected message :flag_eval_hook with (no args)
./lib/datadog/open_feature/provider.rb:75:in &#39;Datadog::OpenFeature::Provider#hooks&#39;
/usr/local/bundle/gems/openfeature-sdk-0.6.5/lib/open_feature/sdk/client.rb:80:in &#39;OpenFeature::SDK::Client#fetch_details&#39;
/usr/local/bundle/gems/openfeature-sdk-0.6.5/lib/open_feature/sdk/client.rb:65:in &#39;OpenFeature::SDK::Client#fetch_string_value&#39;
./spec/datadog/open_feature/provider_span_enrichment_spec.rb:220:in &#39;block (4 levels) in &lt;top (required)&gt;&#39;
./lib/datadog/tracing/trace_operation.rb:271:in &#39;block in Datadog::Tracing::TraceOperation#measure&#39;
./lib/datadog/tracing/span_operation.rb:169:in &#39;Datadog::Tracing::SpanOperation#measure&#39;
...

New test introduced in this PR is flaky.

OpenFeature provider span enrichment (end-to-end) when the gate is ON aggregates evaluations from a child span onto the one local root from rspec   View in Datadog
#&lt;InstanceDouble(Datadog::OpenFeature::Component) (anonymous)&gt; received unexpected message :flag_eval_hook with (no args)

Failure/Error: component&amp;.flag_eval_hook,
  #&lt;InstanceDouble(Datadog::OpenFeature::Component) (anonymous)&gt; received unexpected message :flag_eval_hook with (no args)
./lib/datadog/open_feature/provider.rb:75:in &#39;Datadog::OpenFeature::Provider#hooks&#39;
/usr/local/bundle/gems/openfeature-sdk-0.6.5/lib/open_feature/sdk/client.rb:80:in &#39;OpenFeature::SDK::Client#fetch_details&#39;
/usr/local/bundle/gems/openfeature-sdk-0.6.5/lib/open_feature/sdk/client.rb:65:in &#39;OpenFeature::SDK::Client#fetch_string_value&#39;
./spec/datadog/open_feature/provider_span_enrichment_spec.rb:149:in &#39;block (4 levels) in &lt;top (required)&gt;&#39;
./lib/datadog/tracing/trace_operation.rb:271:in &#39;block in Datadog::Tracing::TraceOperation#measure&#39;
./lib/datadog/tracing/span_operation.rb:169:in &#39;Datadog::Tracing::SpanOperation#measure&#39;
...

New test introduced in this PR is flaky.

View in Flaky Test Management

ℹ️ Info

No other issues found (see more)

🧪 All tests passed

Useful? React with 👍 / 👎

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

@pr-commenter

pr-commenter Bot commented Jun 16, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-06-17 23:33:15

Comparing candidate commit 89b7543 in PR branch leo.romanovsky/ffe-apm-span-enrichment with baseline commit 25c5eb5 in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 48 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 ----------------------------------'

…ync + weak lifecycle

Address codex review blockers on the FFE APM span-enrichment hook.

The supported OpenFeature Ruby SDKs (>= 0.3.1) do not dispatch provider or
client hooks at all (Client#fetch_details calls the provider and never invokes
a hook), so the previous design — gated on OpenFeature::SDK::Hooks::Hook being
defined and relying on #finally — never constructed the hook and silently
emitted no ffe_* tags even with DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED=true.

Changes:
- Dispatch enrichment directly from Provider#evaluate via a new
  SpanEnrichmentHook#capture(flag_key:, variant:, value:, serial_id:, do_log:,
  targeting_key:) primitive, wrapped never-throw. Works on every supported SDK.
  SpanEnrichmentHook.available? now returns true unconditionally; #finally is
  kept as an idempotent delegate for any future SDK that does dispatch hooks.
- Guard all store/subscription/accumulator access with a Mutex; take the
  finish-time tag snapshot under the lock so concurrent evaluations cannot race
  fetch-or-create, duplicate subscriptions, or mutate-while-encoding.
- Key per-trace state in an ObjectSpace::WeakMap so an abandoned trace (root
  span never finishes) cannot pin its accumulator; the span_before_finish
  subscription closure captures the accumulator to keep it alive exactly for the
  trace's lifetime, and both are collected together when the trace is GC'd.
- Update RBS sigs for the new capture/tuple-returning store/weak backing.

Gate-off behavior is unchanged: the hook is still only constructed when
span_enrichment_enabled is true (no idle per-span overhead, DG-005). The frozen
wire contract (tag names, ULEB128 delta-varint binary buffer, limits, SHA256
subjects, JSON runtime defaults) is unchanged.
…eature client

Add a provider/client spec that drives the production path
OpenFeature::SDK.build_client -> Datadog::OpenFeature::Provider#fetch_* ->
enrich_span -> ffe_* tags on the local root span, stubbing only the native
EvaluationEngine and the active-trace seam (no native ext / libdatadog / Docker).

Covers the highest-risk behavior the unit specs cannot: that real Ruby
evaluations actually trigger enrichment (the SDK does not dispatch hooks),
asserting:
- gate ON: ffe_flags_enc after a real string evaluation,
- ffe_subjects_enc only when do_log is authorized and a targeting key is present,
- runtime-default capture via a missing variant,
- child-span evaluations aggregate onto the one local root,
- concurrent evaluations on the same root lose no serial ids (Mutex),
- enrichment still works after provider reconfiguration,
- gate OFF: no ffe_* tags, fully inert.

Passes on both the openfeature-min (0.3.1) and openfeature-latest (0.4.1)
appraisals.
… SDK >= 0.6

The end-to-end span-enrichment spec failed under openfeature-sdk 0.6.x (Ruby 3.4
CI lane) for two version-specific reasons:

1. 0.6+ dispatches provider hooks during Client#fetch_details, calling
   Provider#hooks -> Component#flag_eval_hook. The Component instance_double did
   not allow :flag_eval_hook, raising MockExpectationError. Stub it (nil ->
   compacted) on both doubles.

2. 0.6+ sets the provider asynchronously (spawns a background init thread), which
   the leaked-thread detector flagged. Use set_provider_and_wait when available
   (synchronous, no thread); older SDKs already set synchronously.

Also corrected comments that claimed supported SDKs never dispatch hooks (true for
< 0.6, false for >= 0.6; the direct dispatch + finally hook are idempotent via the
deduping accumulators). No production behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Involves Datadog core libraries

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant