Skip to content

feat(feature-flagging): FFE APM feature-flag span enrichment (experimental, gated)#11658

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 30 commits into
masterfrom
leo.romanovsky/ffe-apm-span-enrichment
Jul 14, 2026
Merged

feat(feature-flagging): FFE APM feature-flag span enrichment (experimental, gated)#11658
gh-worker-dd-mergequeue-cf854d[bot] merged 30 commits into
masterfrom
leo.romanovsky/ffe-apm-span-enrichment

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

feat(feature-flagging): 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 finally hook captures the evaluation (serial ID, targeting key, runtime default).
  3. Evaluations are accumulated against the local root span (resolved via SpanEnrichmentInterceptor.findLocalRootInFragment).
  4. On root-span finish, the accumulated state is encoded and written as ffe_* tags.

Configuration

Opt-in, off by default. The gate is resolved through the standard ConfigProvider precedence
(system property → stable config → environment variable), the same mechanism as the sibling
provider-enabled gate:

# environment variable
DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED=true
# or system property
-Ddd.experimental.flagging.provider.span.enrichment.enabled=true

Canonical key: experimental.flagging.provider.span.enrichment.enabled
(FeatureFlaggingConfig.SPAN_ENRICHMENT_ENABLED). This is distinct from
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED — enabling the provider does not enable span
enrichment. The provider logs span enrichment enabled / span enrichment disabled at INFO on
startup.

Span tags added

Tag Description Format
ffe_flags_enc All evaluated flag serial IDs base64 delta-varint
ffe_subjects_enc Subject → flags mapping (when doLog=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 the experimental.flagging.provider.span.enrichment.enabled gate
    (FeatureFlaggingConfig.SPAN_ENRICHMENT_ENABLEDDD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED),
    off by default, read via ConfigProvider so system property + stable config + env all apply;
    log enabled/disabled at INFO on startup; surface the split serialId in eval metadata
    (ufc/v1/Split.java).
  • Codec: ULeb128Encoder — delta-varint serial IDs, SHA256-hashed subject keys.
  • Accumulator + hook + interceptor: SpanEnrichmentAccumulator, SpanEnrichmentHook, SpanEnrichmentInterceptor, SpanEnrichmentStates — accumulate evaluations and write ffe_* tags onto the local root span. State is held in a WeakHashMap keyed by the local-root span object (no central id-keyed store, no cap/eviction): it is collected with its root, so a never-completing trace can't leak. This matches the Python/Ruby SDK design.
  • Tests: JUnit 5 L0 suite (7 required cases + golden round-trip) + lifecycle regression tests (partial-flush, leak, reconfiguration).

Decisions

  • No idle per-span overhead when the gate is off — the accumulator is absent and spans carry no ffe_* tags.
  • Lifecycle: per-trace state is weak-keyed by the local-root span, so it's reclaimed with the span (no cap, no eviction); it is also removed on local-root finish and cleared on provider close. Regression tests cover partial-flush, no-leak, and reconfiguration paths.
  • 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 trace payload and decodes the ffe_* tags. Flag ffe-dogfooding-string-flag (serial 2312):

  • Gate ON — the root span (trace.annotation) carried ffe_flags_enc = iBI= decoding to serial [2312] plus a SHA256-hashed ffe_subjects_enc[2312] (doLog=true), through the real dd-openfeature provider path.
  • 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 dd-java-agent 1.64.0-SNAPSHOT (+ custom dd-trace-api, dd-openfeature) built from this branch (HEAD 8491123c5d):

TEST_LIBRARY=java ./run.sh PARAMETRIC -k span_enrichment
======================= 18 xpassed in 207.57s (0:03:27) ========================

All 18 cases ran and asserted green, covering ffe_flags_enc / ffe_subjects_enc / ffe_runtime_defaults, delta-varint encoding, SHA256 subject keys with doLog gating, and the 200/10/20/5/64 limits. (Reported as xpassed only because the manifest declares the feature at release v1.64.0 while the local artifact is pre-release 1.64.0-SNAPSHOT; it resolves to a clean passed at release.) No SDK source changes were required to pass. The system-tests enablement (parametric handler + manifests/java.yml) is a separate draft PR against DataDog/system-tests.

Full dogfooding matrix + regression suite (2026-06-17)

Re-validated end-to-end through the real OpenFeature provider path (javaagent built from this
branch, java@1.64.0-SNAPSHOT+109eab80bc) behind the trace-intake tee-proxy, decoding ffe_*
with scripts/decode_ffe_span_tags.py (root span trace.annotation, service ffe-dogfooding-java):

Scenario Result
Gate ON (serial 2312) ffe_flags_enc[2312]; ffe_subjects_enc = {sha256(targeting key): ids} only when __dd_do_log
Gate OFF zero ffe_* tags
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 span carries none
Unicode + object runtime defaults Value structure unwrapped to JSON {"grëeting":"こんにちは","emoji":"🎉","n":3} (not Value(innerObject=…)), raw UTF-8 with no \uXXXX escaping, valid JSON, values truncated to 64
Codec parity ZAgUAg==[100,108,128,130]

Deterministic regression suite (SpanEnrichmentHookTest + SpanEnrichmentLifecycleRegressionTest, 28 cases, all pass) additionally covers: distinctTracesDoNotMerge (state keyed by local-root span identity), hookCapturedRootAndInterceptorResolvedRootAreSameObject (the capture and write sides resolve the same local-root instance), newProviderAfterShutdownStillEnriches + lateShutdownOfDisplacedProviderDoesNotClobberActiveProvider (reconfiguration), eachProviderOwnsADistinctStateStore, partialFlushNeverWritesTagsOnAChildSpan, neverCompletingTracesAreNotCapped + unreachableRootStateIsWeaklyCollected (weak-keyed lifecycle), gateOffConstructsNothingAndAccumulatesNoState, and the Value→JSON runtime-default cases.

System-tests re-confirmed against this branch's artifacts: 18 passed (TEST_LIBRARY=java ./run.sh PARAMETRIC -k span_enrichment, library java@1.64.0-SNAPSHOT+109eab80bc).

…nrichment gate

- Add nullable Integer serialId to UFC Split model + constructor (Moshi
  reflectively deserializes the serialId JSON field; no custom adapter needed)
- DDEvaluator surfaces __dd_split_serial_id (when present) and __dd_do_log in
  OpenFeature eval metadata for APM span enrichment (JAVA-01)
- Add FeatureFlaggingConfig.SPAN_ENRICHMENT_ENABLED dot-form constant mapping to
  DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED (distinct, off by default)
- Register the env var in metadata/supported-configurations.json (version A, boolean)
- Update DDEvaluatorTest Split call sites for the new constructor arg
…ceptor (gate-gated)

- ULeb128Encoder: ULEB128 delta-varint + base64 codec ported verbatim from the
  frozen Node reference (dd-trace-js#8343); golden vector {100,108,128,130} -> ZAgUAg==,
  SHA-256 hex targeting-key hashing; JDK stdlib only (java.util.Base64, MessageDigest)
- SpanEnrichmentAccumulator: per-trace state keyed by trace id in a shared
  ConcurrentHashMap; frozen limits (200/10/20/5/64), UTF-8-safe truncation, object
  default -> JSON (Pattern F tag shapes: ffe_flags_enc bare base64, ffe_subjects_enc /
  ffe_runtime_defaults JSON objects)
- SpanEnrichmentHook: OpenFeature finally hook (capture) resolving the active local
  root via AgentTracer and applying the Node branch (serialId -> addSerialId/addSubject;
  missing variant -> addDefault); error-isolated
- SpanEnrichmentInterceptor: TraceInterceptor (write) flushing ffe_* onto the local
  root onTraceComplete with unique priority 4, then clearing state; disable() for
  provider-close cleanup
- Provider: gate-gated construction via ConfigHelper.env (DG-005 — nothing built/
  registered when off), hook added to getProviderHooks, interceptor registered via
  GlobalTracer, shutdown() disables interceptor + drains state
- feature-flagging-api: add compileOnly/testImplementation :internal-api for the
  tracer/interceptor types (runtime-provided by the agent)
… golden round-trip)

- Codec golden-vector round-trip {100,108,128,130} -> ZAgUAg== (encode + decode +
  empty + structural dedupe)
- no-span, finished-root (accumulate+dedupe+flush), runtime-default missing-variant
  (ffe_runtime_defaults JSON object), per-subject cap (10 subj / 20 exp/subj) +
  doLog gating, max-200 serial ids, object-default JSON + 64-char truncation
- gate-off negative control: no hook/interceptor constructed, not in getProviderHooks,
  no accumulator state (DG-005)
- gate-on construction + provider-close cleanup (shutdown disables interceptor +
  drains state); error isolation; interceptor priority uniqueness
- JUnit 5 only (no Groovy); injectable RootSpanResolver + Provider gate override
  avoid static mocking (project mockito uses the subclass mock maker)
…econfig)

Gap-closure for the three BLOCKER lifecycle defects in 02-REVIEW-java.md. The
frozen contract (codec/limits/gate/tag shapes, golden vector ZAgUAg==) is
unchanged — this fixes only how per-trace state is scoped, bounded, and flushed.

CR-01 (partial-flush data loss + tag misattribution):
  CoreTracer.write -> interceptCompleteTrace runs onTraceComplete on EVERY flush,
  and PendingTrace.write(isPartial) excludes the still-open local root from a
  partial flush (getRootSpan() is null until the final write). The interceptor
  previously resolved the root by reference (reachable even when absent from the
  fragment) and unconditionally removed the accumulator on the first partial
  flush, dropping all pre-flush flags and tagging a not-yet-finished root.
  SpanEnrichmentInterceptor now flushes + removes ONLY when the local root is
  actually present in the flushed collection (the final write); a partial flush
  leaves the accumulator intact. Also adds the missing getTraceId() null guard
  (WR-01) and never falls back to tagging a non-root span (WR-02).

CR-02 (unbounded state leak):
  State lived in a static ConcurrentHashMap whose only removal paths were
  trace-complete and shutdown, so dropped / Noop / never-finishing traces leaked
  forever (#4844 leak class). State now lives in a per-provider
  SpanEnrichmentStates store, hard-bounded at MAX_TRACES with FIFO eviction of
  the oldest in-flight entry, so a never-completing trace cannot grow the map
  unboundedly.

CR-03 (reconfiguration corruption):
  Provider ignored addTraceInterceptor's return value, so a second gate-on
  provider (rejected on duplicate priority 4) still wired a hook into shared
  static state and its shutdown() cleared the first provider's live state.
  Provider now honors the registration result (wires the hook/interceptor only
  when registration succeeds) and each provider owns its own state store, so one
  provider's shutdown can never clear another's. Removes the global static map
  (review IN-03 root cause). Adds an injectable TraceInterceptorRegistrar test
  seam (mirrors the existing gate-override seam).

Runtime-default rendering (String() parity) and hasData() are intentionally
unchanged. L0 suite migrated to the instance-owned store; module 146 tests green.
Adds SpanEnrichmentLifecycleRegressionTest exercising the real tracer lifecycle
paths the original L0 suite bypassed (single root, no children, one provider).
Each test FAILS against the pre-fix code and PASSES after the fix:

CR-01 partialFlushExcludingRootPreservesPreFlushFlags / partialFlushNeverWrites-
  TagsOnAChildSpan: a partial flush carrying only child spans (root excluded, as
  dd-trace-core does) must not drain state or tag a child; the full pre+post-flush
  serial-id set {100,108,128,130} -> 'ZAgUAg==' must land on the root at final
  completion. Fail-before: tags written on the partial flush (NeverWantedButInvoked).

CR-02 neverCompletingTracesDoNotLeakUnbounded / boundedStoreEvictsOldestFirst:
  3x MAX_TRACES distinct never-flushed traces keep the store bounded; eviction is
  FIFO. Fail-before: store grew to 3x the cap (expected <=cap but was larger).

CR-03 secondProviderRejectedRegistrationDoesNotClearFirstProviderState /
  eachProviderOwnsADistinctStateStore / hookAndInterceptorOfOneProviderShareThe-
  SameStore: a second provider whose registration is rejected wires no
  hook/interceptor and its shutdown does not clear the first provider's in-flight
  state. Fail-before: second provider kept a non-null interceptor (expected null).

Fail-before verified by temporarily reintroducing each defect: 5/7 failed (the
remaining 2 are structural-invariant checks that hold by construction).
@datadog-datadog-prod-us1-2

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.99 s 13.92 s [-0.2%; +1.1%] (no difference)
startup:insecure-bank:tracing:Agent 12.92 s 12.98 s [-1.4%; +0.6%] (no difference)
startup:petclinic:appsec:Agent 16.32 s 16.68 s [-6.5%; +2.2%] (no difference)
startup:petclinic:iast:Agent 16.79 s 16.38 s [-3.1%; +8.1%] (unstable)
startup:petclinic:profiling:Agent 16.60 s 16.55 s [-0.6%; +1.2%] (no difference)
startup:petclinic:sca:Agent 16.90 s 16.76 s [-0.2%; +1.9%] (no difference)
startup:petclinic:tracing:Agent 15.95 s 16.10 s [-2.0%; +0.1%] (no difference)

Commit: afcc029a · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

leoromanovsky and others added 4 commits June 17, 2026 01:47
…hment comments

Remove leaked workflow labels (e.g. JAVA-01) from production comments so the
upstream PR carries only behavioral context.
- Reconfiguration safety: replace the per-provider trace interceptor with a
  single process-wide delegating interceptor (SpanEnrichmentInterceptor.INSTANCE)
  registered once and rebound per active provider. A closing provider unbinds
  only if still active, so provider close/reopen no longer permanently disables
  enrichment via a duplicate-priority rejection, and a displaced provider's late
  shutdown cannot clobber the active provider's in-flight state.
- Runtime defaults: recursively unwrap OpenFeature Value (structure/list/scalar)
  to its native form before JSON serialization so ffe_runtime_defaults matches
  the frozen Node contract instead of emitting Value.toString().
- 128-bit trace ids: key per-trace state by the full trace id hex string rather
  than DDTraceId.toLong(), so two traces sharing low-order 64 bits no longer
  merge enrichment state.
- Gate-off inertness: precompute the immutable provider-hook list once in the
  constructor so getProviderHooks() (called per evaluation) allocates nothing.
- Add regression tests: reconfiguration, displaced-provider late shutdown,
  128-bit low-bit collision, Value structure/list/scalar serialization, and
  gate-off zero-allocation.
@pavlokhrebto

Copy link
Copy Markdown
Contributor

Finalization pass (handoff takeover)

Picking this up to get it merge-ready. Changes since the last review, all scoped to the existing feature — the frozen wire contract (codec, limits, tag shapes, golden vector ZAgUAg==) is unchanged:

  • Config gate now uses the standard resolution path. The gate was read env-var-only (ConfigHelper.env). It now goes through ConfigProvider.getInstance().getBoolean(FeatureFlaggingConfig.SPAN_ENRICHMENT_ENABLED, false), so system property + stable config + env var all apply — matching the sibling FLAGGING_PROVIDER_ENABLED gate. This also makes the previously-unused FeatureFlaggingConfig.SPAN_ENRICHMENT_ENABLED constant the single source of truth for the key (canonical experimental.flagging.provider.span.enrichment.enabled). "1"/"true" semantics preserved; still off by default.
  • Startup INFO log added. The provider now logs span enrichment enabled / span enrichment disabled at construction (parity with dd-trace-js#8343 and the cross-language handoff requirement). Reflects effective wiring — "enabled" only when the hook was actually constructed.
  • Tech-debt cleanup. Removed the unused SpanEnrichmentInterceptor.isRegistered() method (dead surface being introduced by this PR; zero callers in prod or tests).

Verification (local):

  • :products:feature-flagging:feature-flagging-api:compileJava
  • :products:feature-flagging:feature-flagging-api:test ✅ (span-enrichment suite green)
  • :products:feature-flagging:feature-flagging-api:spotlessApply ✅ (no reformatting needed)

Files touched in this pass: Provider.java, SpanEnrichmentInterceptor.java (+ the FeatureFlaggingConfig / supported-configurations.json gate entry from earlier).

…manovsky/ffe-apm-span-enrichment

# Conflicts:
#	products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java
@pavlokhrebto
pavlokhrebto marked this pull request as ready for review July 7, 2026 14:57
@pavlokhrebto
pavlokhrebto requested review from a team as code owners July 7, 2026 14:57
@pavlokhrebto
pavlokhrebto requested review from dd-oleksii, dougqh and sameerank and removed request for a team July 7, 2026 14:57
@dd-octo-sts

dd-octo-sts Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@sarahchen6 sarahchen6 added the type: feature Enhancements and improvements label Jul 7, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99422b5c34

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return trace; // partial flush, or no resolvable in-fragment root: keep state untouched
}
// Key by the local-root span object to match the capture-side keying.
final SpanEnrichmentAccumulator state = states.remove((AgentSpan) localRoot);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve enrichment until long-running traces finish

When trace.experimental.long-running.enabled is true and a trace survives to a periodic long-running flush, PendingTrace emits unfinished spans including the still-running local root (writeRunningSpans adds running spans to the trace). This path then treats root presence as final and removes the accumulator, so flags evaluated before the first long-running snapshot are no longer present when the trace actually completes and later snapshots can overwrite the root tag with only newer IDs. Please keep the state until the root is finished rather than using presence in the emitted collection as the only finality check.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good catch — this is a real edge case with long-running traces. The fix needs a "is this local root actually finished?" signal on the interface the interceptor uses (AgentSpan), which DDSpan already tracks internally but doesn't expose. I'm handling that as a small separate core PR (add AgentSpan.isFinished()), then this interceptor will key off the finished root instead of fragment presence. Tracked as a follow-up; not gating this PR since long-running traces are off by default.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Opened the core prerequisite PR for this: #11884 (adds AgentSpan.isFinished()). Once it lands, the interceptor will key off the finished local root instead of fragment presence, fixing this long-running-trace case. Follow-up on this branch after #11884 merges.

static final int PRIORITY = 4;

/** The single, long-lived interceptor registered with the tracer (reconfiguration safety). */
static final SpanEnrichmentInterceptor INSTANCE = new SpanEnrichmentInterceptor();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid registering an app-classloader singleton globally

In servlet/app-server deployments where dd-openfeature is loaded by a webapp classloader, this singleton is only singleton per classloader, but the tracer's interceptor set is JVM-wide and has no removal API. The first webapp's interceptor remains registered and pins that classloader; after redeploy a new classloader creates a different INSTANCE, registration is rejected by the same priority, and the new provider's store is never read, so span enrichment is disabled until JVM restart. Register the long-lived interceptor from an agent/bootstrap classloader or otherwise avoid retaining application classloaders.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved by the capture/write split: the interceptor is now created and registered by the agent-side SpanEnrichmentWriter (agent classloader), not a per-webapp singleton in the application classloader. No application classloader is retained, so redeploys are unaffected.

…digest

  - Gate the __dd_split_serial_id / __dd_do_log evaluation metadata on the
    span-enrichment flag so an enabled provider with enrichment off attaches
    nothing extra per evaluation (DDEvaluator).
  - Extract the metadata keys to named constants (METADATA_SPLIT_SERIAL_ID /
    METADATA_DO_LOG) as the single source of truth; SpanEnrichmentHook
    references them.
  - Rename the config constant to EXPERIMENTAL_SPAN_ENRICHMENT_ENABLED to make
    its experimental status obvious at call sites.
  - Reuse a ThreadLocal<MessageDigest> for subject hashing instead of
    allocating a SHA-256 instance per capture (ULeb128Encoder).
pavlokhrebto added a commit that referenced this pull request Jul 8, 2026
Add a non-breaking default boolean isFinished() to the AgentSpan
interface (internal-api), surfacing the finished state DDSpan already
tracks (durationNano != 0) so other modules can query it without
reaching into dd-trace-core. DDSpan's existing method overrides the
default; the TrackingSpanDecorator test wrapper delegates; leaf/synthetic
spans (NoopSpan etc.) keep the false default. Adds a DDSpanTest case
covering false-before / true-after-finish via the interface.

Motivated by the FFE span-enrichment interceptor (PR #11658), which needs
to distinguish a truly-finished local root from a still-running one
(long-running traces) and resolve it in O(1); that consumer lands in a
separate follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
metadataBuilder.addString(METADATA_SPLIT_SERIAL_ID, split.serialId.toString());
}
metadataBuilder.addString(
METADATA_DO_LOG, String.valueOf(allocation.doLog != null && allocation.doLog));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Is it required that these are strings? I would have imagined __dd_do_log would be a boolean

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fixed

void jsonUsesPlatformWriterEscaping() {
// The platform JsonWriter escapes '/' as backslash-slash and non-ASCII as a backslash-u escape.
// That differs from the JS reference bytes but is round-trip-equivalent: all consumers
// JSON-parse these tags (backend Jackson, system-tests json.loads), so byte-parity is not

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Another way to verify this is https://github.com/DataDog/ffe-dogfooding

You may be able to run a local build of this code. Might need some extra setup to enable enrichment, but I was ultimately able to verify that this worked for the JS implementation when I saw this in the traces

Image

@sarahchen6 sarahchen6 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

left some small comments, and ran the branch by Codex!

Comment thread products/feature-flagging/feature-flagging-api/build.gradle.kts Outdated
PerfectSlayer and others added 3 commits July 9, 2026 09:48
…TraceInterceptor() false return: ensureInterceptorRegistered() now latches only on a true return; registration ordering: moved ensureInterceptorRegistered() below the root == null check

@PerfectSlayer PerfectSlayer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving to remove the request for changes flag.
I focused my review on the high level design, I’ll let my pear having a review of the changes in details 🙏

@sarahchen6 sarahchen6 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like CI still requires a spotBugs clean-up, but otherwise, let's give it a try!

@pavlokhrebto
pavlokhrebto added this pull request to the merge queue Jul 14, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Jul 14, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-14 09:21:26 UTC ℹ️ Start processing command /merge


2026-07-14 09:21:31 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 2h (p90).


2026-07-14 10:16:53 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 14, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 05671ce into master Jul 14, 2026
586 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the leo.romanovsky/ffe-apm-span-enrichment branch July 14, 2026 10:16
@github-actions github-actions Bot added this to the 1.65.0 milestone Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: openfeature OpenFeature type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants