Skip to content

feat(node): support immediate per-subject experimentation opt-out - #420

Merged
leoromanovsky merged 16 commits into
mainfrom
leoromanovsky/node-experimentation-opt-out-immediate
Oct 30, 2026
Merged

leoromanovsky merged 16 commits into
mainfrom
leoromanovsky/node-experimentation-opt-out-immediate

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Node applications need to exclude selected subjects from experiments while retaining ordinary feature delivery. The Node provider needs an explicit policy option; tracer configuration and environment precedence belong in dd-trace-js.

Changes

With the shared evaluator and timestamp parser merged in #423, this PR adds only the Node provider option, wiring, declarations, documentation, and tests.

import { DatadogNodeServerProvider, ExperimentationOptOutMode } from '@datadog/openfeature-node-server'

const provider = new DatadogNodeServerProvider({
  exposureChannel,
  experimentationOptOutMode: ExperimentationOptOutMode.Immediate,
})

Disabled is the default. Immediate skips experiment allocations when the evaluation context contains a valid dd_experimentation_opt_out_at, including a future timestamp. Ordinary allocations remain eligible. Invalid enabled input returns INVALID_CONTEXT; unsupported provider modes throw at construction. Exposure deduplication and evaluation telemetry are unchanged.

dd-trace-js #10826 owns DD_FEATURE_FLAGS_EXPERIMENTATION_OPT_OUT_MODE and passes the resolved mode to this provider. The provider does not read environment variables. Future-only mode remains outside this Node API change. The tracer integration requires a core/Node provider release and a vendored dependency update.

Test instructions

After merging main at 58bc213b51eb539d58ee4a5c7719ccf500f09b66, all three package builds, unit test suites, and type checks passed, along with Node test type checking, lint, formatting, package-version validation, and all 124 build-script tests. Node tests cover explicit/default modes, independence from the environment, all four evaluation methods, shared timestamp validation, and exposure deduplication.

These are local SDK checks on Node 22.15.0. Package-install smoke tests, tracer system tests, and staging were not rerun.

Checklist

  • Updated Documentation
  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.

FFE reviewer selection

Reviewers: @sameerank, @greghuels

Why: Global fallback — This PR adds an experimentationOptOutMode option (Disabled/Immediate) to the Node provider. Global reviewers were selected because no project or SME pod matched.

🤖 Automated selection.

Selection details

Initiative: global / global

Why this match: This PR adds an experimentationOptOutMode option (Disabled/Immediate) to the Node provider. The provider passes it into evaluate() for all four evaluation methods, so opted-out subjects skip experiment allocations. Exposure handling and telemetry are unchanged. No registered initiative matches this per-subject experiment exclusion during evaluation. ufc-compliance covers suppressing evaluation/exposure event egress, which this PR does not change. experimentation-decisioning covers layers and mutual exclusion, and holdouts and bandits do not apply. The only fallback, browser-ts, covers packages/browser, core and react, not packages/node-server. So global ownership applies.

  • @sameerank — final owner (final-review-owners): Global pool filling a pod coverage gap. Existing review coverage preserved.
  • @greghuels — final owner (final-review-owners): Global pool filling a pod coverage gap. Existing review coverage preserved.

The PR author was excluded. Existing reviewers and teams were preserved.

Debug or fix a problem

Workflow run · Bits Code session

  • Wrong project or pod? Clarify this PR’s purpose, or correct the reviewer registry and publish the change. Sheet edits take effect after publication.
  • Need another reviewer now? Request them in GitHub. This workflow only adds requests and keeps existing reviewers.
  • Stuck beyond five minutes or failed? Inspect the run’s failed step and available Bits session. After fixing the cause, rerun the workflow with this PR URL and dry_run=false. Unchanged completed runs are skipped. If the receipt says needs_reconciliation or a write outcome is uncertain, ask the workflow owner to reconcile GitHub and the receipt before retrying; do not delete the lock blindly.
  • Report a problem: Comment on the reviewer-routing skill PR with this PR URL, the workflow run link, and the expected initiative or reviewer correction.
Evidence and run details

✅ Complete — reviewer coverage verified. The workflow checked pending requests or submitted reviews on the evaluated head.

Generated asynchronously by the FFE reviewer-routing workflow.

Changed paths: packages/node-server/src/experimentation-opt-out.ts, packages/node-server/src/provider.ts, packages/node-server/src/index.ts, packages/node-server/index.d.ts

Evaluated head: dc9086f6c1004f512a436ce84be71a11ce488744

Registry revision: revision:1946aa2a-530e-487a-84a7-83aff44a1089

Pinned skill source: d214cfc65ae154b97d39af5cf32caf48aaec9bf0

Instructions: workflow-injected snapshot (no plugin loaded)

Instruction snapshot SHA-256: f23b2a88c13c0da90e2dd58246a14f257af91e2195c555039bd555b52e6f3516

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

OpenFeature Browser Provider Bundle Sizes

Measured from the Vite production output after installing packed @datadog/flagging-core and @datadog/openfeature-browser tarballs.

Both scenarios initialize an OpenFeature provider, evaluate a boolean flag, change context, and evaluate again. Telemetry is disabled for DatadogProvider; no tracking hooks are registered for DatadogCoreProvider. DatadogProvider fetches precomputed assignments for each context; DatadogCoreProvider receives rules from fetchRulesConfiguration once and evaluates locally.

Sizes include OpenFeature and the same small scenario harness. Configuration responses are supplied by Playwright and are not bundled. These are complete scenario JS sizes, not configuration payload sizes or isolated provider/Protobuf costs; the difference between rows is not a decoder-only delta.

Scenario HTML JS Assets Raw JS Gzip JS Protobuf Markers
DatadogProvider (precomputed fetching) provider.html 15 104.4 KiB 37.7 KiB no
DatadogCoreProvider + fetchRulesConfiguration core-provider.html 15 196.1 KiB 63.4 KiB yes

Dependency checks passed: no Protobuf markers in default/precomputed scenarios; markers present in rules-based scenarios.

OpenFeature Browser Tracking Hook Bundle Sizes

Synthetic entrypoints import and call tracking hook factories from the packed @datadog/openfeature-browser/rules-based ESM package. Deltas are measured against the no-hook baseline from the same Vite production build.

Scenario HTML JS Assets Raw JS Raw Δ Gzip JS Gzip Δ
tracking hook baseline tracking-baseline.html 5 2.7 KiB 0 B 1.5 KiB 0 B
exposure logging hook tracking-exposure.html 10 71.1 KiB +68.4 KiB 26.4 KiB +24.9 KiB
evaluation logging hook tracking-evaluation.html 11 66.7 KiB +64.0 KiB 24.9 KiB +23.4 KiB
RUM tracking hook tracking-rum.html 8 6.8 KiB +4.1 KiB 3.5 KiB +2.0 KiB
all tracking hooks tracking-all.html 13 75.7 KiB +73.0 KiB 28.5 KiB +27.0 KiB

OpenFeature Node Server Bundle Sizes

Independent Webpack production builds of the installed core and Node SDK tarballs, targeting Node 18 with ESM (module export condition). Node built-ins remain external. OpenFeature, dd-trace, and configuration fixtures are not bundled.

Scenario Raw JS Gzip JS Protobuf Modules
Node provider only 42.1 KiB 13.4 KiB no
SSR helpers only (fetch client rules + serialize) 81.0 KiB 23.5 KiB yes
Node provider + SSR helpers 122.1 KiB 35.7 KiB yes

Adding SSR helpers to the provider adds 80.0 KiB raw / 22.3 KiB gzip in this build. This includes fetching, parsing, and serialization, not just the decoder. SSR helpers fetch rules for the browser, not for configuring that provider (which uses its existing JSON API).

These are bundler sizes, not npm install sizes, unbundled Node require/import costs, payload sizes, or latency. Gzip compares compressed artifacts. The browser scenarios include OpenFeature and a different harness, so their sizes are not directly comparable.

Dependency checks passed: no Protobuf runtime or generated schema modules in the provider-only bundle; present in both SSR bundles (checked using Webpack module statistics).

This report shows current PR artifact sizes only; it does not compare against the base branch.

@leoromanovsky
leoromanovsky marked this pull request as ready for review October 20, 2026 22:29
@leoromanovsky
leoromanovsky requested a review from a team as a code owner October 20, 2026 22:29
Validate activation timestamps when JSON configurations are received and protobuf
responses are decoded. Reuse parsed instants during evaluation while preserving
nanosecond precision, per-flag errors in every mode, and raw core API compatibility.

Environment: Datadog workspace
Make doLog an explicit boolean in the UFC allocation type, matching the backend
schema. Update test allocations and regenerate the Node package declarations.

Environment: Datadog workspace
Use one mode-and-cutoff cache key scheme and require an explicit acknowledgement policy.
Receive the edited browser rules fixture again so activation preparation uses its final timestamp.

Environment: Datadog workspace
Explain immediate exclusion and the literal activation cutoff used by future-only mode.

Environment: Datadog workspace
Stack Node immediate opt-out on the shared browser implementation, reuse its timestamp validation, and leave environment configuration to dd-trace-js.

Environment: Datadog workspace
@leoromanovsky
leoromanovsky changed the base branch from main to leoromanovsky/browser-experimentation-opt-out-immediate October 22, 2026 03:46
@leoromanovsky
leoromanovsky added this pull request to stack #427 October 22, 2026 03:50
Apply the repository Prettier formatting required by CI.

Environment: Datadog workspace
Base automatically changed from leoromanovsky/browser-experimentation-opt-out-immediate to main October 22, 2026 12:47
Keep the merged core and browser implementation, preserve the Node opt-out option, and regenerate the public declarations.

Environment: Datadog workspace

@aarsilv aarsilv 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.

🤖 Comment from Claude working with Aaron Silverman:

Heads up: main pins the fixture submodule to 1c831fed, which DataDog/ffe-system-test-data#52 replaced when it was squash-merged as 0781ce2. Could you re-pin, and update the two case counts in packages/core/test/evaluation/flags-v1.spec.ts?

1c831fed is not on the fixture repository's main; GitHub reports it as diverged (2 ahead, 5 behind). This PR does not change the pin, but it is the open PR in this area.

The merged fixtures add two immediate and two future-mode cases. So a pin bump alone fails flags-v1.spec.ts: toBe(26) receives 28, and after that fix toHaveLength(42) receives 44. With both counts updated, every core test passes, including the capability cases.

Evidence — core tests with the pin moved
Fixture commit Counts core node-server
1c831fed (current pin) 26 / 42 1031/1031 546/546
0781ce2 (merged) 26 / 42 1 failed, 1048 passed 560/560
0781ce2 (merged) 28 / 44 1049/1049 560/560
f20ab80 (fixture main when I ran it) 28 / 44 1053/1053 564/564
1c831fed (control) 28 / 44 1 failed, 1030 passed —
Suggested fix — re-pin and update the counts
diff --git a/packages/core/test/evaluation/flags-v1.spec.ts b/packages/core/test/evaluation/flags-v1.spec.ts
index 6787537..a0e75df 100644
--- a/packages/core/test/evaluation/flags-v1.spec.ts
+++ b/packages/core/test/evaluation/flags-v1.spec.ts
@@ -141,9 +141,9 @@ describe('Universal Flag Configuration V1', () => {
   it('loads the immediate opt-out capability cases', () => {
     const cases = getTestCaseFileNames().flatMap(getTestCases)
     const immediateCases = cases.filter((testCase) => testCase.capabilities?.experimentationOptOutImmediate)
-    expect(immediateCases.length).toBe(26)
+    expect(immediateCases.length).toBe(28)
     expect(cases.filter((testCase) => testCase.capabilities?.experimentationOptOutFutureExperimentsOnly)).toHaveLength(
-      42
+      44
     )
     expect(new Set(immediateCases.map((testCase) => testCase.variationType))).toEqual(
       new Set(['BOOLEAN', 'STRING', 'INTEGER', 'NUMERIC', 'JSON'])
diff --git a/packages/core/test/ffe-system-test-data b/packages/core/test/ffe-system-test-data
index 1c831fe..0781ce2 160000
--- a/packages/core/test/ffe-system-test-data
+++ b/packages/core/test/ffe-system-test-data
@@ -1 +1 @@
-Subproject commit 1c831fed02a428a1b0a4ac412da9b773e22df257
+Subproject commit 0781ce2c55f9da7f9290035c768bbc46570556c0

Comment thread packages/node-server/src/provider.ts Resolved
Comment thread packages/node-server/README.md Resolved
@aarsilv

aarsilv commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

🤖 Comment from Claude working with Aaron Silverman:

Friendly ping on the review from Oct 24: the four items below are still open, with no reply yet. The Android and iOS companion PRs have addressed their review rounds, so this is the remaining SDK in the set.

Each comment includes a tested patch. If you'd rather handle any of them in a follow-up PR, a short reply saying so is enough.

@aarsilv aarsilv 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 with some low-priority findings you can take or leave as you see fit!

@leoromanovsky

leoromanovsky commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

FFE reviewer selection

Reviewers: @typotter, @sameerank

Why: openfeature architecture — Adds a public experimentationOptOutMode option (Disabled/Immediate) to the Node OpenFeature provider. Reviewers come from the matched pod.

🤖 Automated selection.

Selection details

Initiative: fallback / openfeature-architecture

Why this match: Adds a public experimentationOptOutMode option (Disabled/Immediate) to the Node OpenFeature provider. The provider validates the option at construction and passes it to every typed evaluation. In core, evaluateForSubject now skips experiment allocations, and prepares activations only when the policy is active. This changes experiment allocation eligibility through the provider's evaluation contract. It does not change how evaluation or exposure events are sent, so ufc-compliance's exclusions rule it out. No other registered project covers experiment opt-out. That leaves the provider/evaluation SME. browser-ts targets the Browser subject area, not the Node provider.

  • @typotter — primary (openfeature-architecture): Matched pod member. Existing review coverage preserved.
  • @sameerank — core (openfeature-architecture): Matched pod member. Existing review coverage preserved.

The PR author was excluded. Existing reviewers and teams were preserved.

Debug or fix a problem

Workflow run · Bits Code session

  • Wrong project or pod? Clarify this PR’s purpose, or correct the reviewer registry and publish the change. Sheet edits take effect after publication.
  • Need another reviewer now? Request them in GitHub. This workflow only adds requests and keeps existing reviewers.
  • Stuck beyond five minutes or failed? Inspect the run’s failed step and available Bits session. After fixing the cause, rerun the workflow with this PR URL and dry_run=false. Unchanged completed runs are skipped. If the receipt says needs_reconciliation or a write outcome is uncertain, ask the workflow owner to reconcile GitHub and the receipt before retrying; do not delete the lock blindly.
  • Report a problem: Comment on the reviewer-routing skill PR with this PR URL, the workflow run link, and the expected initiative or reviewer correction.
Evidence and run details

✅ Complete — reviewer coverage verified. The workflow checked pending requests or submitted reviews on the evaluated head.

Generated asynchronously by the FFE reviewer-routing workflow.

Changed paths: packages/node-server/src/provider.ts, packages/node-server/src/experimentation-opt-out.ts, packages/node-server/src/index.ts, packages/node-server/index.d.ts, packages/core/src/evaluation/evaluateForSubject.ts

Evaluated head: 7abb29707b0f689d740692d594bf1634bb2795fe

Registry revision: revision:c041715e-7c35-40a5-bc60-5180aab7a600

Pinned skill source: 7e3283c602f6b18a7e3e623eecb52d3a9c091a2e

Instructions: workflow-injected snapshot (no plugin loaded)

Instruction snapshot SHA-256: 52ed8cb68675dde1a47addaefe4a9500891c24014abf7dade6dfe38cf836c1d2

@leoromanovsky
leoromanovsky requested review from btthomas and typotter and removed request for btthomas October 29, 2026 01:16
Exercise the shared immediate corpus, skip inactive exclusion work, document timestamp constraints, and pin merged fixtures.

Environment: Datadog workspace
@leoromanovsky

Copy link
Copy Markdown
Collaborator Author

🤖 Comment from Codex working with @leoromanovsky:

For the fixture pin suggestion and follow-up, the change pins the merged opt-out fixtures at 0781ce2c55f9da7f9290035c768bbc46570556c0. Core expects 28 Immediate and 44 Future cases, and the Node provider runs all 28 Immediate cases.

The four suggested changes are published in 7b7227c. Local suites pass 2,328 tests, with six existing skips.

Evaluate opt-out transitions through the OpenFeature client so exposure finalization hooks run after the latest main changes.

Environment: Datadog workspace
@leoromanovsky
leoromanovsky merged commit b9235f2 into main Oct 30, 2026
5 checks passed
@leoromanovsky
leoromanovsky deleted the leoromanovsky/node-experimentation-opt-out-immediate branch October 30, 2026 17:31
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.

2 participants