Repository: open-feature/js-sdk
Pull request #919 (2024-04-25 20:09:47)

feat: set context during provider init on web
## This PR

- overloads the set provider methods to support defining context in the web SDK
- updates the web sdk readme

## Related Issues

Fixes https://github.com/open-feature/js-sdk/issues/748

## Notes

I decided to only support setting context in the web SDK because it is less valuable on the server and the expected behavior was less clear due to `domains`.

The behavior may need to be spec'd out. An issue in the spec repo has been created.
https://github.com/open-feature/spec/issues/219


beeme1mr: I've addressed your feedback. @toddbaert, any other concerns?
beeme1mr: @toddbaert @moredip any other concerns?

Patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..8791b70b
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+  "cSpell.words": [
+    "domainless"
+  ]
+}
\ No newline at end of file
diff --git a/jest.config.ts b/jest.config.ts
index c4b08a5c..b048809e 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -175,13 +175,13 @@ export default {
       displayName: 'react',
       testEnvironment: 'jsdom',
       preset: 'ts-jest',
-      testMatch: ['<rootDir>/packages/react/test/**/*.spec.ts*'],
+      testMatch: ['<rootDir>/packages/react/test/**/*.spec.{ts,tsx}'],
       moduleNameMapper: {
         '@openfeature/core': '<rootDir>/packages/shared/src',
         '@openfeature/web-sdk': '<rootDir>/packages/client/src',
       },
       transform: {
-        '^.+\\.tsx$': [
+        '^.+\\.(ts|tsx)$': [
           'ts-jest',
           {
             tsconfig: '<rootDir>/packages/react/test/tsconfig.json',
diff --git a/packages/client/README.md b/packages/client/README.md
index 75c196e3..03fb659b 100644
--- a/packages/client/README.md
+++ b/packages/client/README.md
@@ -121,7 +121,7 @@ To register a provider and ensure it is ready before further actions are taken,
 
 ```ts
 await OpenFeature.setProviderAndWait(new MyProvider());
-```  
+```
 
 #### Synchronous
 
@@ -158,9 +158,16 @@ Sometimes, the value of a flag must consider some dynamic criteria about the app
 In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting).
 If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context).
 
+```ts
+// Sets global context during provider registration
+await OpenFeature.setProvider(new MyProvider(), { origin: document.location.host });
+```
+
+Change context after the provider has been registered using `setContext`.
+
 ```ts
 // Set a value to the global context
-await OpenFeature.setContext({ origin: document.location.host });
+await OpenFeature.setContext({ targetingKey: localStorage.getItem("targetingKey") });
 ```
 
 Context is global and setting it is `async`.
@@ -233,6 +240,24 @@ const domainScopedClient = OpenFeature.getClient("my-domain");
 Domains can be defined on a provider during registration.
 For more details, please refer to the [providers](#providers) section.
 
+#### Manage evaluation context for domains
+
+By default, domain-scoped clients use the global context.
+This can be overridden by explicitly setting context when registering the provider or by references the domain when updating context:
+
+```ts
+OpenFeature.setProvider("my-domain", new NewCachedProvider(), { targetingKey: localStorage.getItem("targetingKey") });
+```
+
+To change context after the provider has been registered, use `setContext` with a name:
+
+```ts
+await OpenFeature.setContext("my-domain", { targetingKey: localStorage.getItem("targetingKey") })
+```
+
+Once context has been defined for a named client, it will override the global context for all clients using the associated provider.
+Context can be cleared using for a named provider using `OpenFeature.clearContext("my-domain")` or call `OpenFeature.clearContexts()` to reset all context.
+
 ### Eventing
 
 Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
diff --git a/packages/client/src/client/open-feature-client.ts b/packages/client/src/client/open-feature-client.ts
index 500d3d61..9fb23383 100644
--- a/packages/client/src/client/open-feature-client.ts
+++ b/packages/client/src/client/open-feature-client.ts
@@ -15,7 +15,8 @@ import {
   ResolutionDetails,
   SafeLogger,
   StandardResolutionReasons,
-  statusMatchesEvent
+  instantiateErrorByErrorCode,
+  statusMatchesEvent,
 } from '@openfeature/core';
 import { FlagEvaluationOptions } from '../evaluation';
 import { ProviderEvents } from '../events';
@@ -208,7 +209,7 @@ export class OpenFeatureClient implements Client {
 
     try {
       this.beforeHooks(allHooks, hookContext, options);
-      
+
       // short circuit evaluation entirely if provider is in a bad state
       if (this.providerStatus === ProviderStatus.NOT_READY) {
         throw new ProviderNotReadyError('provider has not yet initialized');
@@ -225,6 +226,10 @@ export class OpenFeatureClient implements Client {
         flagKey,
       };
 
+      if (evaluationDetails.errorCode) {
+        throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
+      }
+
       this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
 
       return evaluationDetails;
diff --git a/packages/client/src/open-feature.ts b/packages/client/src/open-feature.ts
index c0bcde6c..cae2d2fa 100644
--- a/packages/client/src/open-feature.ts
+++ b/packages/client/src/open-feature.ts
@@ -69,6 +69,140 @@ export class OpenFeatureAPI
     return this._domainScopedProviders.get(domain)?.status ?? this._defaultProvider.status;
   }
 
+  /**
+   * Sets the default provider for flag evaluations and returns a promise that resolves when the provider is ready.
+   * This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
+   * Setting a provider supersedes the current provider used in new and existing unbound clients.
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @returns {Promise<void>}
+   * @throws Uncaught exceptions thrown by the provider during initialization.
+   */
+  setProviderAndWait(provider: Provider): Promise<void>;
+  /**
+   * Sets the default provider for flag evaluations and returns a promise that resolves when the provider is ready.
+   * This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
+   * Setting a provider supersedes the current provider used in new and existing unbound clients.
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @param {EvaluationContext} context The evaluation context to use for flag evaluations.
+   * @returns {Promise<void>}
+   * @throws Uncaught exceptions thrown by the provider during initialization.
+   */
+  setProviderAndWait(provider: Provider, context: EvaluationContext): Promise<void>;
+  /**
+   * Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
+   * A promise is returned that resolves when the provider is ready.
+   * Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
+   * @param {string} domain The name to identify the client
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @returns {Promise<void>}
+   * @throws Uncaught exceptions thrown by the provider during initialization.
+   */
+  setProviderAndWait(domain: string, provider: Provider): Promise<void>;
+  /**
+   * Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
+   * A promise is returned that resolves when the provider is ready.
+   * Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
+   * @param {string} domain The name to identify the client
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @param {EvaluationContext} context The evaluation context to use for flag evaluations.
+   * @returns {Promise<void>}
+   * @throws Uncaught exceptions thrown by the provider during initialization.
+   */
+  setProviderAndWait(domain: string, provider: Provider, context: EvaluationContext): Promise<void>;
+  async setProviderAndWait(
+    clientOrProvider?: string | Provider,
+    providerContextOrUndefined?: Provider | EvaluationContext,
+    contextOrUndefined?: EvaluationContext,
+  ): Promise<void> {
+    const domain = stringOrUndefined(clientOrProvider);
+    const provider = domain
+      ? objectOrUndefined<Provider>(providerContextOrUndefined)
+      : objectOrUndefined<Provider>(clientOrProvider);
+    const context = domain
+      ? objectOrUndefined<EvaluationContext>(contextOrUndefined)
+      : objectOrUndefined<EvaluationContext>(providerContextOrUndefined);
+
+    if (context) {
+      // synonymously setting context prior to provider initialization.
+      // No context change event will be emitted.
+      if (domain) {
+        this._domainScopedContext.set(domain, context);
+      } else {
+        this._context = context;
+      }
+    }
+
+    await this.setAwaitableProvider(domain, provider);
+  }
+
+  /**
+   * Sets the default provider for flag evaluations.
+   * This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
+   * Setting a provider supersedes the current provider used in new and existing unbound clients.
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @returns {this} OpenFeature API
+   */
+  setProvider(provider: Provider): this;
+  /**
+   * Sets the default provider and evaluation context for flag evaluations.
+   * This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
+   * Setting a provider supersedes the current provider used in new and existing unbound clients.
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @param context {EvaluationContext} The evaluation context to use for flag evaluations.
+   * @returns {this} OpenFeature API
+   */
+  setProvider(provider: Provider, context: EvaluationContext): this;
+  /**
+   * Sets the provider for flag evaluations of providers with the given name.
+   * Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
+   * @param {string} domain The name to identify the client
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @returns {this} OpenFeature API
+   */
+  setProvider(domain: string, provider: Provider): this;
+  /**
+   * Sets the provider and evaluation context flag evaluations of providers with the given name.
+   * Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
+   * @param {string} domain The name to identify the client
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @param context {EvaluationContext} The evaluation context to use for flag evaluations.
+   * @returns {this} OpenFeature API
+   */
+  setProvider(domain: string, provider: Provider, context: EvaluationContext): this;
+  setProvider(
+    domainOrProvider?: string | Provider,
+    providerContextOrUndefined?: Provider | EvaluationContext,
+    contextOrUndefined?: EvaluationContext,
+  ): this {
+    const domain = stringOrUndefined(domainOrProvider);
+    const provider = domain
+      ? objectOrUndefined<Provider>(providerContextOrUndefined)
+      : objectOrUndefined<Provider>(domainOrProvider);
+    const context = domain
+      ? objectOrUndefined<EvaluationContext>(contextOrUndefined)
+      : objectOrUndefined<EvaluationContext>(providerContextOrUndefined);
+
+    if (context) {
+      // synonymously setting context prior to provider initialization.
+      // No context change event will be emitted.
+      if (domain) {
+        this._domainScopedContext.set(domain, context);
+      } else {
+        this._context = context;
+      }
+    }
+
+    const maybePromise = this.setAwaitableProvider(domain, provider);
+
+    // The setProvider method doesn't return a promise so we need to catch and
+    // log any errors that occur during provider initialization to avoid having
+    // an unhandled promise rejection.
+    Promise.resolve(maybePromise).catch((err) => {
+      this._logger.error('Error during provider initialization:', err);
+    });
+    return this;
+  }
+
   /**
    * Sets the evaluation context globally.
    * This will be used by all providers that have not bound to a domain.
@@ -135,7 +269,7 @@ export class OpenFeatureAPI
    * @param {string} domain An identifier which logically binds clients with providers
    * @returns {EvaluationContext} Evaluation context
    */
-  getContext(domain?: string): EvaluationContext;
+  getContext(domain?: string | undefined): EvaluationContext;
   getContext(domainOrUndefined?: string): EvaluationContext {
     const domain = stringOrUndefined(domainOrUndefined);
     if (domain) {
diff --git a/packages/server/README.md b/packages/server/README.md
index 6a8eee66..691311ff 100644
--- a/packages/server/README.md
+++ b/packages/server/README.md
@@ -161,6 +161,9 @@ const requestContext = {
 const boolValue = await client.getBooleanValue('some-flag', false, requestContext);
 ```
 
+Context is merged by the SDK before a flag evaluation occurs.
+The merge order is defined [here](https://openfeature.dev/specification/sections/evaluation-context#requirement-323) in the OpenFeature specification.
+
 ### Hooks
 
 [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.
diff --git a/packages/server/src/client/open-feature-client.ts b/packages/server/src/client/open-feature-client.ts
index d697c167..fb37b73f 100644
--- a/packages/server/src/client/open-feature-client.ts
+++ b/packages/server/src/client/open-feature-client.ts
@@ -16,6 +16,7 @@ import {
   ResolutionDetails,
   SafeLogger,
   StandardResolutionReasons,
+  instantiateErrorByErrorCode,
   statusMatchesEvent,
 } from '@openfeature/core';
 import { FlagEvaluationOptions } from '../evaluation';
@@ -278,6 +279,10 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
         flagKey,
       };
 
+      if (evaluationDetails.errorCode) {
+        throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
+      }
+
       await this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
 
       return evaluationDetails;
diff --git a/packages/server/src/open-feature.ts b/packages/server/src/open-feature.ts
index 09b00250..f0313fd8 100644
--- a/packages/server/src/open-feature.ts
+++ b/packages/server/src/open-feature.ts
@@ -73,6 +73,68 @@ export class OpenFeatureAPI
     return this._domainScopedProviders.get(domain)?.status ?? this._defaultProvider.status;
   }
 
+  /**
+   * Sets the default provider for flag evaluations and returns a promise that resolves when the provider is ready.
+   * This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
+   * Setting a provider supersedes the current provider used in new and existing unbound clients.
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @returns {Promise<void>}
+   * @throws Uncaught exceptions thrown by the provider during initialization.
+   */
+  setProviderAndWait(provider: Provider): Promise<void>;
+  /**
+   * Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
+   * A promise is returned that resolves when the provider is ready.
+   * Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
+   * @param {string} domain The name to identify the client
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @returns {Promise<void>}
+   * @throws Uncaught exceptions thrown by the provider during initialization.
+   */
+  setProviderAndWait(domain: string, provider: Provider): Promise<void>;
+  async setProviderAndWait(domainOrProvider?: string | Provider, providerOrUndefined?: Provider): Promise<void> {
+    const domain = stringOrUndefined(domainOrProvider);
+    const provider = domain
+      ? objectOrUndefined<Provider>(providerOrUndefined)
+      : objectOrUndefined<Provider>(domainOrProvider);
+
+    await this.setAwaitableProvider(domain, provider);
+  }
+
+  /**
+   * Sets the default provider for flag evaluations.
+   * This provider will be used by domainless clients and clients associated with domains to which no provider is bound.
+   * Setting a provider supersedes the current provider used in new and existing unbound clients.
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @returns {this} OpenFeature API
+   */
+  setProvider(provider: Provider): this;
+  /**
+   * Sets the provider for flag evaluations of providers with the given name.
+   * Setting a provider supersedes the current provider used in new and existing clients bound to the same domain.
+   * @param {string} domain The name to identify the client
+   * @param {Provider} provider The provider responsible for flag evaluations.
+   * @returns {this} OpenFeature API
+   */
+  setProvider(domain: string, provider: Provider): this;
+  setProvider(clientOrProvider?: string | Provider, providerOrUndefined?: Provider): this {
+    const domain = stringOrUndefined(clientOrProvider);
+    const provider = domain
+      ? objectOrUndefined<Provider>(providerOrUndefined)
+      : objectOrUndefined<Provider>(clientOrProvider);
+
+    const maybePromise = this.setAwaitableProvider(domain, provider);
+
+    // The setProvider method doesn't return a promise so we need to catch and
+    // log any errors that occur during provider initialization to avoid having
+    // an unhandled promise rejection.
+    Promise.resolve(maybePromise).catch((err) => {
+      this._logger.error('Error during provider initialization:', err);
+    });
+
+    return this;
+  }
+
   setContext(context: EvaluationContext): this {
     this._context = context;
     return this;
diff --git a/packages/shared/src/errors/index.ts b/packages/shared/src/errors/index.ts
index 6cde15e2..bd87e965 100644
--- a/packages/shared/src/errors/index.ts
+++ b/packages/shared/src/errors/index.ts
@@ -1,9 +1,45 @@
-export * from './general-error';
-export * from './flag-not-found-error';
-export * from './parse-error';
-export * from './type-mismatch-error';
-export * from './targeting-key-missing-error';
-export * from './invalid-context-error';
-export * from './open-feature-error-abstract';
-export * from './provider-not-ready-error';
-export * from './provider-fatal-error';
+import { ErrorCode } from '../evaluation';
+
+import { FlagNotFoundError } from './flag-not-found-error';
+import { GeneralError } from './general-error';
+import { InvalidContextError } from './invalid-context-error';
+import { OpenFeatureError } from './open-feature-error-abstract';
+import { ParseError } from './parse-error';
+import { ProviderFatalError } from './provider-fatal-error';
+import { ProviderNotReadyError } from './provider-not-ready-error';
+import { TargetingKeyMissingError } from './targeting-key-missing-error';
+import { TypeMismatchError } from './type-mismatch-error';
+
+const instantiateErrorByErrorCode = (errorCode: ErrorCode, message?: string): OpenFeatureError => {
+  switch (errorCode) {
+    case ErrorCode.FLAG_NOT_FOUND:
+      return new FlagNotFoundError(message);
+    case ErrorCode.PARSE_ERROR:
+      return new ParseError(message);
+    case ErrorCode.TYPE_MISMATCH:
+      return new TypeMismatchError(message);
+    case ErrorCode.TARGETING_KEY_MISSING:
+      return new TargetingKeyMissingError(message);
+    case ErrorCode.INVALID_CONTEXT:
+      return new InvalidContextError(message);
+    case ErrorCode.PROVIDER_NOT_READY:
+      return new ProviderNotReadyError(message);
+    case ErrorCode.PROVIDER_FATAL:
+      return new ProviderFatalError(message);
+    default:
+      return new GeneralError(message);
+  }
+};
+
+export {
+  FlagNotFoundError,
+  GeneralError,
+  InvalidContextError,
+  ParseError,
+  ProviderFatalError,
+  ProviderNotReadyError,
+  TargetingKeyMissingError,
+  TypeMismatchError,
+  OpenFeatureError,
+  instantiateErrorByErrorCode,
+};
diff --git a/packages/shared/src/open-feature.ts b/packages/shared/src/open-feature.ts
index 4ba7e376..2e7f32a1 100644
--- a/packages/shared/src/open-feature.ts
+++ b/packages/shared/src/open-feature.ts
@@ -7,7 +7,7 @@ import {
   EventHandler,
   Eventing,
   GenericEventEmitter,
-  statusMatchesEvent
+  statusMatchesEvent,
 } from './events';
 import { isDefined } from './filter';
 import { BaseHook, EvaluationLifeCycle } from './hooks';
@@ -25,7 +25,11 @@ type AnyProviderStatus = ClientProviderStatus | ServerProviderStatus;
 export class ProviderWrapper<P extends CommonProvider<AnyProviderStatus>, S extends AnyProviderStatus> {
   private _pendingContextChanges = 0;
 
-  constructor(private _provider: P, private _status: S, _statusEnumType: typeof ClientProviderStatus | typeof ServerProviderStatus) {
+  constructor(
+    private _provider: P,
+    private _status: S,
+    _statusEnumType: typeof ClientProviderStatus | typeof ServerProviderStatus,
+  ) {
     // update the providers status with events
     _provider.events?.addHandler(AllProviderEvents.Ready, () => {
       // These casts are due to the face we don't "know" what status enum we are dealing with here (client or server).
@@ -50,7 +54,7 @@ export class ProviderWrapper<P extends CommonProvider<AnyProviderStatus>, S exte
 
   set provider(provider: P) {
     this._provider = provider;
-  } 
+  }
 
   get status(): S {
     return this._status;
@@ -73,8 +77,15 @@ export class ProviderWrapper<P extends CommonProvider<AnyProviderStatus>, S exte
   }
 }
 
-export abstract class OpenFeatureCommonAPI<S extends AnyProviderStatus, P extends CommonProvider<S> = CommonProvider<S>, H extends BaseHook = BaseHook>
-  implements Eventing<AnyProviderEvent>, EvaluationLifeCycle<OpenFeatureCommonAPI<S, P>>, ManageLogger<OpenFeatureCommonAPI<S, P>>
+export abstract class OpenFeatureCommonAPI<
+    S extends AnyProviderStatus,
+    P extends CommonProvider<S> = CommonProvider<S>,
+    H extends BaseHook = BaseHook,
+  >
+  implements
+    Eventing<AnyProviderEvent>,
+    EvaluationLifeCycle<OpenFeatureCommonAPI<S, P>>,
+    ManageLogger<OpenFeatureCommonAPI<S, P>>
 {
   // accessor for the type of the ProviderStatus enum (client or server)
   protected abstract readonly _statusEnumType: typeof ClientProviderStatus | typeof ServerProviderStatus;
@@ -185,60 +196,19 @@ export abstract class OpenFeatureCommonAPI<S extends AnyProviderStatus, P extend
     return this._apiEmitter.getHandlers(eventType);
   }
 
-  /**
-   * Sets the default provider for flag evaluations and returns a promise that resolves when the provider is ready.
-   * The default provider will be used by domainless clients and clients associated with domains to which no provider is bound.
-   * Setting a provider supersedes the current provider used in new and existing unbound clients.
-   * @template P
-   * @param {P} provider The provider responsible for flag evaluations.
-   * @returns {Promise<void>}
-   * @throws Uncaught exceptions thrown by the provider during initialization.
-   */
-  async setProviderAndWait(provider: P): Promise<void>;
-  /**
-   * Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
-   * A promise is returned that resolves when the provider is ready.
-   * Setting a provider supersedes the current provider used in new and existing clients in the same domain.
-   * @template P
-   * @param {string} domain An identifier which logically binds clients with providers
-   * @param {P} provider The provider responsible for flag evaluations.
-   * @returns {Promise<void>}
-   * @throws Uncaught exceptions thrown by the provider during initialization.
-   */
-  async setProviderAndWait(domain: string, provider: P): Promise<void>;
-  async setProviderAndWait(domainOrProvider?: string | P, providerOrUndefined?: P): Promise<void> {
-    await this.setAwaitableProvider(domainOrProvider, providerOrUndefined);
-  }
+  abstract setProviderAndWait(
+    clientOrProvider?: string | P,
+    providerContextOrUndefined?: P | EvaluationContext,
+    contextOrUndefined?: EvaluationContext,
+  ): Promise<void>;
 
-  /**
-   * Sets the default provider for flag evaluations.
-   * The default provider will be used by domainless clients and clients associated with domains to which no provider is bound.
-   * Setting a provider supersedes the current provider used in new and existing unbound clients.
-   * @template P
-   * @param {P} provider The provider responsible for flag evaluations.
-   * @returns {this} OpenFeature API
-   */
-  setProvider(provider: P): this;
-  /**
-   * Sets the provider that OpenFeature will use for flag evaluations on clients bound to the same domain.
-   * Setting a provider supersedes the current provider used in new and existing clients in the same domain.
-   * @template P
-   * @param {string} domain An identifier which logically binds clients with providers
-   * @param {P} provider The provider responsible for flag evaluations.
-   * @returns {this} OpenFeature API
-   */
-  setProvider(domain: string, provider: P): this;
-  setProvider(domainOrProvider?: string | P, providerOrUndefined?: P): this {
-    const maybePromise = this.setAwaitableProvider(domainOrProvider, providerOrUndefined);
-    if (maybePromise) {
-      maybePromise.catch(() => {
-        /* ignore, errors are emitted via the event emitter */
-      });
-    }
-    return this;
-  }
+  abstract setProvider(
+    clientOrProvider?: string | P,
+    providerContextOrUndefined?: P | EvaluationContext,
+    contextOrUndefined?: EvaluationContext,
+  ): this;
 
-  private setAwaitableProvider(domainOrProvider?: string | P, providerOrUndefined?: P): Promise<void> | void {
+  protected setAwaitableProvider(domainOrProvider?: string | P, providerOrUndefined?: P): Promise<void> | void {
     const domain = stringOrUndefined(domainOrProvider);
     const provider = objectOrUndefined<P>(domainOrProvider) ?? objectOrUndefined<P>(providerOrUndefined);
 
@@ -265,7 +235,11 @@ export abstract class OpenFeatureCommonAPI<S extends AnyProviderStatus, P extend
     const emitters = this.getAssociatedEventEmitters(domain);
 
     let initializationPromise: Promise<void> | void = undefined;
-    const wrappedProvider = new ProviderWrapper<P, AnyProviderStatus>(provider, this._statusEnumType.NOT_READY, this._statusEnumType);
+    const wrappedProvider = new ProviderWrapper<P, AnyProviderStatus>(
+      provider,
+      this._statusEnumType.NOT_READY,
+      this._statusEnumType,
+    );
 
     // initialize the provider if it implements "initialize" and it's not already registered
     if (typeof provider.initialize === 'function' && !this.allProviders.includes(provider)) {
@@ -438,7 +412,11 @@ export abstract class OpenFeatureCommonAPI<S extends AnyProviderStatus, P extend
       this._logger.error('Unable to cleanly close providers. Resetting to the default configuration.');
     } finally {
       this._domainScopedProviders.clear();
-      this._defaultProvider = new ProviderWrapper<P, AnyProviderStatus>(defaultProvider, this._statusEnumType.NOT_READY, this._statusEnumType);
+      this._defaultProvider = new ProviderWrapper<P, AnyProviderStatus>(
+        defaultProvider,
+        this._statusEnumType.NOT_READY,
+        this._statusEnumType,
+      );
     }
   }
 


Test patch:
diff --git a/packages/client/test/evaluation-context.spec.ts b/packages/client/test/evaluation-context.spec.ts
index 1d9c66e4..4b6b24ed 100644
--- a/packages/client/test/evaluation-context.spec.ts
+++ b/packages/client/test/evaluation-context.spec.ts
@@ -1,5 +1,7 @@
 import { EvaluationContext, JsonValue, OpenFeature, Provider, ProviderMetadata, ResolutionDetails } from '../src';
 
+const initializeMock = jest.fn();
+
 class MockProvider implements Provider {
   readonly metadata: ProviderMetadata;
 
@@ -7,6 +9,8 @@ class MockProvider implements Provider {
     this.metadata = { name: options?.name ?? 'mock-provider' };
   }
 
+  initialize = initializeMock;
+
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   onContextChange(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> {
     return Promise.resolve();
@@ -15,7 +19,7 @@ class MockProvider implements Provider {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   resolveBooleanEvaluation = jest.fn((flagKey: string, defaultValue: boolean, context: EvaluationContext) => {
     return {
-      value: true
+      value: true,
     };
   });
 
@@ -35,6 +39,7 @@ class MockProvider implements Provider {
 describe('Evaluation Context', () => {
   afterEach(async () => {
     await OpenFeature.clearContexts();
+    jest.clearAllMocks();
   });
 
   describe('Requirement 3.2.2', () => {
@@ -59,6 +64,42 @@ describe('Evaluation Context', () => {
       expect(OpenFeature.getContext('invalid')).toEqual(defaultContext);
     });
 
+    describe('Set context during provider registration', () => {
+      it('should set the context for the default provider', () => {
+        const context: EvaluationContext = { property1: false };
+        const provider = new MockProvider();
+        OpenFeature.setProvider(provider, context);
+        expect(OpenFeature.getContext()).toEqual(context);
+      });
+
+      it('should set the context for a domain', async () => {
+        const context: EvaluationContext = { property1: false };
+        const domain = 'test';
+        const provider = new MockProvider({ name: domain });
+        OpenFeature.setProvider(domain, provider, context);
+        expect(OpenFeature.getContext()).toEqual({});
+        expect(OpenFeature.getContext(domain)).toEqual(context);
+      });
+
+      it('should set the context for the default provider prior to initialization', async () => {
+        const context: EvaluationContext = { property1: false };
+        const provider = new MockProvider();
+        await OpenFeature.setProviderAndWait(provider, context);
+        expect(initializeMock).toHaveBeenCalledWith(context);
+        expect(OpenFeature.getContext()).toEqual(context);
+      });
+
+      it('should set the context for a domain prior to initialization', async () => {
+        const context: EvaluationContext = { property1: false };
+        const domain = 'test';
+        const provider = new MockProvider({ name: domain });
+        await OpenFeature.setProviderAndWait(domain, provider, context);
+        expect(OpenFeature.getContext()).toEqual({});
+        expect(OpenFeature.getContext(domain)).toEqual(context);
+        expect(initializeMock).toHaveBeenCalledWith(context);
+      });
+    });
+
     describe('Context Management', () => {
       it('should reset global context', async () => {
         const globalContext: EvaluationContext = { scope: 'global' };
diff --git a/packages/client/test/hooks.spec.ts b/packages/client/test/hooks.spec.ts
index 902eba1c..d52884a7 100644
--- a/packages/client/test/hooks.spec.ts
+++ b/packages/client/test/hooks.spec.ts
@@ -7,6 +7,8 @@ import {
   GeneralError,
   OpenFeature,
   Hook,
+  StandardResolutionReasons,
+  ErrorCode,
 } from '../src';
 
 const BOOLEAN_VALUE = true;
@@ -206,6 +208,27 @@ describe('Hooks', () => {
           ],
         });
       });
+
+      it('"error" must run if resolution details contains an error code', () => {
+        (MOCK_ERROR_PROVIDER.resolveBooleanEvaluation as jest.Mock).mockReturnValue({
+          value: BOOLEAN_VALUE,
+          errorCode: ErrorCode.FLAG_NOT_FOUND,
+        });
+
+        const mockErrorHook = jest.fn();
+
+        const details = client.getBooleanDetails(FLAG_KEY, false, {
+          hooks: [{ error: mockErrorHook }],
+        });
+
+        expect(mockErrorHook).toHaveBeenCalled();
+        expect(details).toEqual(
+          expect.objectContaining({
+            errorCode: ErrorCode.FLAG_NOT_FOUND,
+            reason: StandardResolutionReasons.ERROR,
+          }),
+        );
+      });
     });
   });
 
diff --git a/packages/react/test/options.spec.ts b/packages/react/test/options.spec.ts
index e5f120a6..c8c04bf8 100644
--- a/packages/react/test/options.spec.ts
+++ b/packages/react/test/options.spec.ts
@@ -27,7 +27,7 @@ describe('normalizeOptions', () => {
     });
   });
 
-  // we fallback the more specific suspense props (`ssuspendUntilReady` and `suspendWhileReconciling`) to `suspend`
+  // we fallback the more specific suspense props (`suspendUntilReady` and `suspendWhileReconciling`) to `suspend`
   describe('suspend fallback', () => {
     it('should fallback to true suspend value', () => {
       const normalized = normalizeOptions({
diff --git a/packages/server/test/evaluation-context.spec.ts b/packages/server/test/evaluation-context.spec.ts
new file mode 100644
index 00000000..1ea2d50a
--- /dev/null
+++ b/packages/server/test/evaluation-context.spec.ts
@@ -0,0 +1,26 @@
+import { EvaluationContext, OpenFeature } from '../src';
+
+describe('Evaluation Context', () => {
+  afterEach(async () => {
+    OpenFeature.setContext({});
+    jest.clearAllMocks();
+  });
+
+  describe('Requirement 3.2.2', () => {
+    it('the API MUST have a method for setting the global evaluation context', () => {
+      const context: EvaluationContext = { property1: false };
+      OpenFeature.setContext(context);
+      expect(OpenFeature.getContext()).toEqual(context);
+    });
+
+    it('the API MUST have a method for setting evaluation context for a named client', () => {
+      const context: EvaluationContext = { property1: false };
+      OpenFeature.setContext(context);
+      expect(OpenFeature.getContext()).toEqual(context);
+    });
+  });
+
+  describe.skip('Requirement 3.2.4', () => {
+    // Only applies to the static-context paradigm
+  });
+});
diff --git a/packages/server/test/hooks.spec.ts b/packages/server/test/hooks.spec.ts
index cda0ec87..627a6835 100644
--- a/packages/server/test/hooks.spec.ts
+++ b/packages/server/test/hooks.spec.ts
@@ -1,11 +1,19 @@
-import { OpenFeature, Provider, ResolutionDetails, Client, FlagValueType, EvaluationContext, Hook } from '../src';
+import {
+  OpenFeature,
+  Provider,
+  ResolutionDetails,
+  Client,
+  FlagValueType,
+  EvaluationContext,
+  Hook,
+  StandardResolutionReasons,
+  ErrorCode,
+} from '../src';
 
 const BOOLEAN_VALUE = true;
 
 const BOOLEAN_VARIANT = `${BOOLEAN_VALUE}`;
 const REASON = 'mocked-value';
-const ERROR_REASON = 'error';
-const ERROR_CODE = 'MOCKED_ERROR';
 
 // a mock provider with some jest spies
 const MOCK_PROVIDER: Provider = {
@@ -28,8 +36,8 @@ const MOCK_ERROR_PROVIDER: Provider = {
   },
   resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
     return Promise.reject({
-      reason: ERROR_REASON,
-      errorCode: ERROR_CODE,
+      reason: StandardResolutionReasons.ERROR,
+      errorCode: ErrorCode.GENERAL,
     });
   }),
 } as unknown as Provider;
@@ -357,6 +365,27 @@ describe('Hooks', () => {
           ],
         });
       });
+
+      it('"error" must run if resolution details contains an error code', async () => {
+        (MOCK_ERROR_PROVIDER.resolveBooleanEvaluation as jest.Mock).mockResolvedValueOnce({
+          value: BOOLEAN_VALUE,
+          errorCode: ErrorCode.FLAG_NOT_FOUND,
+        });
+
+        const mockErrorHook = jest.fn();
+
+        const details = await client.getBooleanDetails(FLAG_KEY, false, undefined, {
+          hooks: [{ error: mockErrorHook }],
+        });
+
+        expect(mockErrorHook).toHaveBeenCalled();
+        expect(details).toEqual(
+          expect.objectContaining({
+            errorCode: ErrorCode.FLAG_NOT_FOUND,
+            reason: StandardResolutionReasons.ERROR,
+          }),
+        );
+      });
     });
   });
 
@@ -636,8 +665,8 @@ describe('Hooks', () => {
         ],
         resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
           return Promise.reject({
-            reason: ERROR_REASON,
-            errorCode: ERROR_CODE,
+            reason: StandardResolutionReasons.ERROR,
+            errorCode: ErrorCode.INVALID_CONTEXT,
           });
         }),
       } as unknown as Provider;
@@ -717,8 +746,8 @@ describe('Hooks', () => {
         ],
         resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
           return Promise.reject({
-            reason: ERROR_REASON,
-            errorCode: ERROR_CODE,
+            reason: StandardResolutionReasons.ERROR,
+            errorCode: ErrorCode.PROVIDER_NOT_READY,
           });
         }),
       } as unknown as Provider;