Skip to content

feat(core): pass bound domain to provider initialize and enforce domain-scoped binding#1433

Open
jonathannorris wants to merge 8 commits into
mainfrom
feat/ofrep-domain-cache-key
Open

feat(core): pass bound domain to provider initialize and enforce domain-scoped binding#1433
jonathannorris wants to merge 8 commits into
mainfrom
feat/ofrep-domain-cache-key

Conversation

@jonathannorris

@jonathannorris jonathannorris commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

  • Add optional domain parameter to provider initialize and pass the bound domain from the provider mutator (spec 1.1.2.2, 2.4.1)
  • Add domainScoped provider declaration and reject binding a domain-scoped instance to more than one domain (spec 2.4.3, 1.1.8.1)
  • Forward domain through web and server MultiProvider initialize
  • Add conformance tests in web and server SDK test suites, including legacy single-argument initialize backward compatibility

Motivation

Unblocks OFREP static-context providers that need the bound domain at init time to scope persisted cache keys per open-feature/spec#393 and protocol ADR 0009.

Notes

  • Non-breaking: domain is an optional second parameter to initialize
  • Non-domain-scoped providers retain existing behavior (single instance can back multiple domains; initialize runs once)
  • Legacy providers that only implement initialize(context) remain compatible; the extra domain argument is ignored if unused

Related Issues

Closes #1434

Relates to: open-feature/spec#393

Test plan

  • npx jest --selectProjects=shared --selectProjects=web --selectProjects=server

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jonathannorris, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 477996a5-9d8d-4b6d-acce-a5de2e39e093

📥 Commits

Reviewing files that changed from the base of the PR and between da94f44 and b964fe0.

📒 Files selected for processing (2)
  • packages/server/test/open-feature.spec.ts
  • packages/web/test/open-feature.spec.ts
📝 Walkthrough

Walkthrough

Provider initialization now carries an optional domain through shared contracts, OpenFeature binding, and server/web MultiProvider paths. Tests add coverage for legacy single-argument providers and domain-scoped binding rules.

Changes

Provider domain handling

Layer / File(s) Summary
Contract and legacy helper
packages/shared/src/provider/provider.ts, packages/shared/test/legacy-initialize-provider.ts
CommonProvider adds domainScoped and a second initialize argument, and the shared legacy test helper tracks initialization calls and resolver stubs.
Domain-aware initialization forwarding
packages/shared/src/open-feature.ts, packages/server/test/open-feature.spec.ts, packages/web/test/open-feature.spec.ts, packages/web/test/evaluation-context.spec.ts
setAwaitableProvider passes domain into provider initialization, and tests cover default/domain initialization plus legacy single-argument providers.
MultiProvider domain forwarding
packages/server/src/provider/multi-provider/multi-provider.ts, packages/web/src/provider/multi-provider/multi-provider-web.ts, packages/server/test/multi-provider.spec.ts, packages/web/test/multi-provider-web.spec.ts
Both MultiProvider implementations forward (context, domain) to child providers, and the matching tests check legacy and regular child providers.
Domain-scoped binding guard
packages/shared/src/open-feature.ts, packages/server/test/open-feature.spec.ts, packages/web/test/open-feature.spec.ts
setAwaitableProvider now rejects reusing a domainScoped provider across domains, and tests cover the thrown error and shared-provider reuse cases.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the domain initialization and domain-scoped binding changes.
Description check ✅ Passed The description matches the implemented changes and testing focus.
Linked Issues check ✅ Passed The PR addresses the bound-domain initialization, optional signature change, domainScoped declaration, and one-domain enforcement.
Out of Scope Changes check ✅ Passed The changes stay within the domain initialization and binding scope without obvious unrelated additions.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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 updates the SDK provider lifecycle to support domain-aware initialization by adding an optional domain argument to Provider.initialize, forwarding it through MultiProvider, and enforcing that domainScoped provider instances cannot be bound to multiple domains. It also adds conformance tests (web + server) and introduces a shared legacy-provider test helper to validate backwards compatibility with single-argument initialize implementations.

Changes:

  • Extend Provider.initialize to accept (context?, domain?) and pass the bound domain during provider registration/initialization.
  • Add domainScoped provider declaration and reject binding the same domain-scoped instance to more than one domain.
  • Forward domain through web/server MultiProvider.initialize and add conformance + legacy compatibility tests.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/shared/src/provider/provider.ts Adds domainScoped to provider interface and updates initialize signature/docs.
packages/shared/src/open-feature.ts Enforces domain-scoped single-domain binding and forwards (context, domain) into provider initialization.
packages/web/src/provider/multi-provider/multi-provider-web.ts Forwards domain through MultiProvider.initialize to child providers.
packages/server/src/provider/multi-provider/multi-provider.ts Forwards domain through MultiProvider.initialize to child providers.
packages/shared/test/legacy-initialize-provider.ts Adds reusable legacy single-arg initialize test provider helper for web/server suites.
packages/web/test/open-feature.spec.ts Adds conformance tests for passing domain to initialize, enforcing domainScoped, and legacy init compatibility.
packages/web/test/multi-provider-web.spec.ts Adds test asserting MultiProvider forwards domain without breaking legacy child providers.
packages/web/test/evaluation-context.spec.ts Updates expectations to include (context, domain) in initialize calls.
packages/server/test/open-feature.spec.ts Mirrors web conformance tests for server SDK behavior + legacy init compatibility.
packages/server/test/multi-provider.spec.ts Mirrors web MultiProvider legacy-domain-forwarding test for server SDK.

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

Comment thread packages/shared/src/open-feature.ts
Comment thread packages/shared/src/provider/provider.ts

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

🧹 Nitpick comments (1)
packages/shared/test/legacy-initialize-provider.ts (1)

47-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add JSDoc @param/@returns to satisfy format-lint.

The format-lint check warns that the JSDoc blocks on legacyInitializeProvider (Line 47) and legacyInitTestProvider (Line 78) are missing @param and @returns declarations.

📝 Proposed JSDoc additions
 /**
  * Provider with a single-argument initialize that ignores any extra arguments passed by the SDK.
+ * `@param` options provider configuration (paradigm, name, resolver mode)
+ * `@returns` a legacy single-argument initialize provider
  */
 export function legacyInitializeProvider(options: LegacyInitializeProviderOptions): LegacyInitializeProvider {
 /**
  * Legacy initialize provider with the stubs MultiProvider expects on child providers.
+ * `@param` options provider configuration (paradigm, name, resolver mode)
+ * `@param` extras MultiProvider child stubs (events, hooks, track)
+ * `@returns` a legacy provider augmented with MultiProvider child stubs
  */
 export function legacyInitTestProvider(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/test/legacy-initialize-provider.ts` around lines 47 - 50, Add
the missing JSDoc tags on legacyInitializeProvider and legacyInitTestProvider so
format-lint passes: update each function’s docblock to include an `@param` entry
for the options argument and an `@returns` entry describing the provider returned.
Keep the existing function names and doc comments intact, and apply the same
JSDoc pattern to both legacyInitializeProvider and legacyInitTestProvider.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/shared/test/legacy-initialize-provider.ts`:
- Around line 47-50: Add the missing JSDoc tags on legacyInitializeProvider and
legacyInitTestProvider so format-lint passes: update each function’s docblock to
include an `@param` entry for the options argument and an `@returns` entry
describing the provider returned. Keep the existing function names and doc
comments intact, and apply the same JSDoc pattern to both
legacyInitializeProvider and legacyInitTestProvider.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b35fb910-9cfc-4133-b796-ea1f346e78c5

📥 Commits

Reviewing files that changed from the base of the PR and between 3c01041 and 62e2e99.

📒 Files selected for processing (10)
  • packages/server/src/provider/multi-provider/multi-provider.ts
  • packages/server/test/multi-provider.spec.ts
  • packages/server/test/open-feature.spec.ts
  • packages/shared/src/open-feature.ts
  • packages/shared/src/provider/provider.ts
  • packages/shared/test/legacy-initialize-provider.ts
  • packages/web/src/provider/multi-provider/multi-provider-web.ts
  • packages/web/test/evaluation-context.spec.ts
  • packages/web/test/multi-provider-web.spec.ts
  • packages/web/test/open-feature.spec.ts

@jonathannorris jonathannorris marked this pull request as ready for review June 26, 2026 15:01
@jonathannorris jonathannorris requested review from a team as code owners June 26, 2026 15:01

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

How does the MultiProvider handle domain-scoped providers? Or is wrapping a provider in MultiProvider essentially a way to bypass that restriction?

Comment thread packages/server/test/multi-provider.spec.ts
Comment thread packages/web/test/multi-provider-web.spec.ts
@jonathannorris

Copy link
Copy Markdown
Member Author

@MattIPv4 MultiProvider forwards (context, domain) to children; it doesn't enforce domainScoped. The SDK guard only runs on direct setProvider(domain, provider) binding, so wrapping in MultiProvider doesn't bypass that.

Reusing the same domain-scoped child across multiple MultiProviders isn't caught at registration (children aren't in allProviders); the child should reject a second domain in initialize. Happy to add a conformance test if useful.

…in-scoped binding

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
@jonathannorris jonathannorris force-pushed the feat/ofrep-domain-cache-key branch from 199f03b to 50bdb4d Compare July 2, 2026 20:43
* @param domain the bound domain, if any
*/
initialize?(context?: EvaluationContext): Promise<void>;
initialize?(context?: EvaluationContext, domain?: string): Promise<void>;

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 wonder if we should make this an initialization config object so we can extend it in the future without breaking anything.

@toddbaert toddbaert Jul 3, 2026

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 don't think it matters as much here due to overloading, but it's possible and maybe a good idea. We could have an InitializationContext (we already have a HookContext that is comparable). "Context" is already a bit overloaded, but this is mostly a provider author interface so I'm not that worried about that.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/test/open-feature.spec.ts`:
- Around line 120-129: The test expectation in OpenFeature provider resolution
is inverted for the named domain check. In the `it('MUST NOT bind a
domain-scoped default provider to a named domain'...)` case, update the
`OpenFeature.getProvider('domain-a')` assertion to verify it resolves to the
default provider instance rather than asserting it is different; use the
`OpenFeature.getProvider` and `provider.metadata.name` checks to confirm object
identity or matching metadata as appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c951b358-c438-4d73-a985-ca5b37391151

📥 Commits

Reviewing files that changed from the base of the PR and between 50bdb4d and da94f44.

📒 Files selected for processing (6)
  • packages/server/test/multi-provider.spec.ts
  • packages/server/test/open-feature.spec.ts
  • packages/shared/src/open-feature.ts
  • packages/shared/test/legacy-initialize-provider.ts
  • packages/web/test/multi-provider-web.spec.ts
  • packages/web/test/open-feature.spec.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/server/test/multi-provider.spec.ts
  • packages/server/test/open-feature.spec.ts
  • packages/web/test/multi-provider-web.spec.ts
  • packages/shared/src/open-feature.ts

Comment on lines +120 to +129
it('MUST NOT bind a domain-scoped default provider to a named domain', () => {
const provider = { ...mockProvider(), domainScoped: true } as Provider;

OpenFeature.setProvider(provider);
expect(() => OpenFeature.setProvider('domain-a', provider)).toThrow(
"Cannot bind domain-scoped provider 'mock-events-success' to more than one domain.",
);
expect(OpenFeature.getProvider()).toBe(provider);
expect(OpenFeature.getProvider('domain-a').metadata.name).not.toBe(provider.metadata.name);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

ast-grep outline packages/shared/src/open-feature.ts --view expanded

Repository: open-feature/js-sdk

Length of output: 671


🏁 Script executed:

sed -n '1,260p' packages/shared/src/open-feature.ts
rg -n "getProvider\\(|setAwaitableProvider|setProvider\\(" packages/shared/src/open-feature.ts packages/web -g '!**/dist/**'

Repository: open-feature/js-sdk

Length of output: 21775


🏁 Script executed:

sed -n '1,280p' packages/web/src/open-feature.ts
sed -n '100,190p' packages/web/test/open-feature.spec.ts

Repository: open-feature/js-sdk

Length of output: 16714


🏁 Script executed:

rg -n "getProviderForClient|_defaultProvider|_domainScopedProviders" packages/shared/src/open-feature.ts
sed -n '260,420p' packages/shared/src/open-feature.ts

Repository: open-feature/js-sdk

Length of output: 7606


Flip the getProvider('domain-a') expectation
getProvider('domain-a') still resolves to the default provider here, so the metadata name matches the default provider’s name. Change this to toBe(...) (or assert object identity) instead of not.toBe(...).

Proposed fix
         expect(OpenFeature.getProvider()).toBe(provider);
-        expect(OpenFeature.getProvider('domain-a').metadata.name).not.toBe(provider.metadata.name);
+        expect(OpenFeature.getProvider('domain-a').metadata.name).toBe(provider.metadata.name);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('MUST NOT bind a domain-scoped default provider to a named domain', () => {
const provider = { ...mockProvider(), domainScoped: true } as Provider;
OpenFeature.setProvider(provider);
expect(() => OpenFeature.setProvider('domain-a', provider)).toThrow(
"Cannot bind domain-scoped provider 'mock-events-success' to more than one domain.",
);
expect(OpenFeature.getProvider()).toBe(provider);
expect(OpenFeature.getProvider('domain-a').metadata.name).not.toBe(provider.metadata.name);
});
it('MUST NOT bind a domain-scoped default provider to a named domain', () => {
const provider = { ...mockProvider(), domainScoped: true } as Provider;
OpenFeature.setProvider(provider);
expect(() => OpenFeature.setProvider('domain-a', provider)).toThrow(
"Cannot bind domain-scoped provider 'mock-events-success' to more than one domain.",
);
expect(OpenFeature.getProvider()).toBe(provider);
expect(OpenFeature.getProvider('domain-a').metadata.name).toBe(provider.metadata.name);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/test/open-feature.spec.ts` around lines 120 - 129, The test
expectation in OpenFeature provider resolution is inverted for the named domain
check. In the `it('MUST NOT bind a domain-scoped default provider to a named
domain'...)` case, update the `OpenFeature.getProvider('domain-a')` assertion to
verify it resolves to the default provider instance rather than asserting it is
different; use the `OpenFeature.getProvider` and `provider.metadata.name` checks
to confirm object identity or matching metadata as appropriate.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Comment on lines +44 to +47
/**
* Provider with a single-argument initialize that ignores any extra arguments passed by the SDK.
* Pass optional extras when MultiProvider needs events, hooks, or track stubs on the child.
*/

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.

Suggested change
/**
* Provider with a single-argument initialize that ignores any extra arguments passed by the SDK.
* Pass optional extras when MultiProvider needs events, hooks, or track stubs on the child.
*/
/**
* Provider with a single-argument initialize that ignores any extra arguments passed by the SDK.
* Pass optional extras when MultiProvider needs events, hooks, or track stubs on the child.
* @param options provider configuration (paradigm, name, resolver mode)
* @param extras optional MultiProvider child stubs (events, hooks, track)
* @returns a single-argument-initialize provider
*/

Fixes a lint.

Comment on lines 234 to +253
@@ -241,6 +247,10 @@ export abstract class OpenFeatureCommonAPI<
throw new GeneralError(`Provider '${provider.metadata.name}' is intended for use on the ${provider.runsOn}.`);
}

if (provider.domainScoped && this.allProviders.includes(provider)) {
throw new GeneralError(`Cannot bind domain-scoped provider '${provider.metadata.name}' to more than one domain.`);
}

@toddbaert toddbaert Jul 3, 2026

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.

Nit: I think the "domainScoped" check could be consolidated into a single upfront check that inspects both _defaultProvider and _domainScopedProviders directly for this instance and rejects if any binding differs from the current one, letting the no-op branch stay focused on being a no-op. Just for consideration; happy either way... I think it would be more lines of code, but it would lock it to a single place... maybe extracted to a method.

Totally optional, only do it if you agree.

@toddbaert toddbaert 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'm approving because I see no fundamental problems, but consider mine and other's nits, and since this is a fundamentally new thing, let's leave it open for a few days at least.

I'm somewhat interested in doing what @beeme1mr said here, but I don't feel 100% sure. Interested in the opinions of others.

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.

Supply the bound domain to provider initialization

5 participants