Skip to content

Commit b7a55ce

Browse files
Merge branch 'main' into renovate/esbuild-0.x-lockfile
2 parents 8f4bb6e + 8686dbf commit b7a55ce

32 files changed

+4020
-5
lines changed

.eslintrc.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@
1212
},
1313
"plugins": ["@typescript-eslint", "check-file", "jsdoc"],
1414
"rules": {
15+
"@typescript-eslint/no-unused-vars": [
16+
"error",
17+
{
18+
"argsIgnorePattern": "^_",
19+
"varsIgnorePattern": "^_",
20+
"caughtErrorsIgnorePattern": "^_",
21+
"ignoreRestSiblings": true
22+
}
23+
],
1524
"@typescript-eslint/consistent-type-imports": [
1625
"error",
1726
{

packages/server/README.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,83 @@ OpenFeature.setProvider(new MyProvider());
140140
Once the provider has been registered, the status can be tracked using [events](#eventing).
141141

142142
In some situations, it may be beneficial to register multiple providers in the same application.
143-
This is possible using [domains](#domains), which is covered in more details below.
143+
This is possible using [domains](#domains), which is covered in more detail below.
144+
145+
#### Multi-Provider
146+
147+
The Multi-Provider allows you to use multiple underlying providers as sources of flag data for the OpenFeature server SDK. When a flag is being evaluated, the Multi-Provider will consult each underlying provider it is managing in order to determine the final result. Different evaluation strategies can be defined to control which providers get evaluated and which result is used.
148+
149+
The Multi-Provider is a powerful tool for performing migrations between flag providers, or combining multiple providers into a single feature flagging interface. For example:
150+
151+
- **Migration**: Gradually migrate from one provider to another by serving some flags from your old provider and some from your new provider
152+
- **Backup**: Use one provider as a backup for another in case of failures
153+
- **Comparison**: Compare results from multiple providers to validate consistency
154+
- **Hybrid**: Combine multiple providers to leverage different strengths (e.g., one for simple flags, another for complex targeting)
155+
156+
```ts
157+
import { OpenFeature, MultiProvider, FirstMatchStrategy } from '@openfeature/server-sdk';
158+
159+
// Create providers
160+
const primaryProvider = new YourPrimaryProvider();
161+
const backupProvider = new YourBackupProvider();
162+
163+
// Create multi-provider with a strategy
164+
const multiProvider = new MultiProvider(
165+
[primaryProvider, backupProvider],
166+
new FirstMatchStrategy()
167+
);
168+
169+
// Register the multi-provider
170+
await OpenFeature.setProviderAndWait(multiProvider);
171+
172+
// Use as normal
173+
const client = OpenFeature.getClient();
174+
const value = await client.getBooleanValue('my-flag', false);
175+
```
176+
177+
**Available Strategies:**
178+
179+
- `FirstMatchStrategy`: Returns the first successful result from the list of providers
180+
- `ComparisonStrategy`: Compares results from multiple providers and can handle discrepancies
181+
182+
**Migration Example:**
183+
184+
```ts
185+
import { OpenFeature, MultiProvider, FirstMatchStrategy } from '@openfeature/server-sdk';
186+
187+
// During migration, serve some flags from the new provider and fallback to the old one
188+
const newProvider = new NewFlagProvider();
189+
const oldProvider = new OldFlagProvider();
190+
191+
const multiProvider = new MultiProvider(
192+
[newProvider, oldProvider], // New provider is consulted first
193+
new FirstMatchStrategy()
194+
);
195+
196+
await OpenFeature.setProviderAndWait(multiProvider);
197+
```
198+
199+
**Comparison Example:**
200+
201+
```ts
202+
import { OpenFeature, MultiProvider, ComparisonStrategy } from '@openfeature/server-sdk';
203+
204+
// Compare results from two providers for validation
205+
const providerA = new ProviderA();
206+
const providerB = new ProviderB();
207+
208+
const multiProvider = new MultiProvider(
209+
[
210+
{ provider: providerA },
211+
{ provider: providerB }
212+
],
213+
new ComparisonStrategy(providerA, (resolutions) => {
214+
console.warn('Mismatch detected', resolutions);
215+
})
216+
);
217+
218+
await OpenFeature.setProviderAndWait(multiProvider);
219+
```
144220

145221
### Targeting
146222

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './provider';
22
export * from './no-op-provider';
33
export * from './in-memory-provider';
4+
export * from './multi-provider';
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# OpenFeature Multi-Provider
2+
3+
The Multi-Provider allows you to use multiple underlying providers as sources of flag data for the OpenFeature server SDK.
4+
When a flag is being evaluated, the Multi-Provider will consult each underlying provider it is managing in order to determine
5+
the final result. Different evaluation strategies can be defined to control which providers get evaluated and which result is used.
6+
7+
The Multi-Provider is a powerful tool for performing migrations between flag providers, or combining multiple providers into a single
8+
feature flagging interface. For example:
9+
10+
- *Migration*: When migrating between two providers, you can run both in parallel under a unified flagging interface. As flags are added to the
11+
new provider, the Multi-Provider will automatically find and return them, falling back to the old provider if the new provider does not have
12+
- *Multiple Data Sources*: The Multi-Provider allows you to seamlessly combine many sources of flagging data, such as environment variables,
13+
local files, database values and SaaS hosted feature management systems.
14+
15+
## Usage
16+
17+
The Multi-Provider is initialized with an array of providers it should evaluate:
18+
19+
```typescript
20+
import { MultiProvider } from '@openfeature/server-sdk'
21+
import { OpenFeature } from '@openfeature/server-sdk'
22+
23+
const multiProvider = new MultiProvider([
24+
{ provider: new ProviderA() },
25+
{ provider: new ProviderB() }
26+
])
27+
28+
await OpenFeature.setProviderAndWait(multiProvider)
29+
30+
const client = OpenFeature.getClient()
31+
32+
console.log("Evaluating flag")
33+
console.log(await client.getBooleanDetails("my-flag", false));
34+
```
35+
36+
By default, the Multi-Provider will evaluate all underlying providers in order and return the first successful result. If a provider indicates
37+
it does not have a flag (FLAG_NOT_FOUND error code), then it will be skipped and the next provider will be evaluated. If any provider throws
38+
or returns an error result, the operation will fail and the error will be thrown. If no provider returns a successful result, the operation
39+
will fail with a FLAG_NOT_FOUND error code.
40+
41+
To change this behaviour, a different "strategy" can be provided:
42+
43+
```typescript
44+
import { MultiProvider, FirstSuccessfulStrategy } from '@openfeature/server-sdk'
45+
46+
const multiProvider = new MultiProvider(
47+
[
48+
{ provider: new ProviderA() },
49+
{ provider: new ProviderB() }
50+
],
51+
new FirstSuccessfulStrategy()
52+
)
53+
```
54+
55+
## Strategies
56+
57+
The Multi-Provider comes with three strategies out of the box:
58+
59+
- `FirstMatchStrategy` (default): Evaluates all providers in order and returns the first successful result. Providers that indicate FLAG_NOT_FOUND error will be skipped and the next provider will be evaluated. Any other error will cause the operation to fail and the set of errors to be thrown.
60+
- `FirstSuccessfulStrategy`: Evaluates all providers in order and returns the first successful result. Any error will cause that provider to be skipped.
61+
If no successful result is returned, the set of errors will be thrown.
62+
- `ComparisonStrategy`: Evaluates all providers in parallel. If every provider returns a successful result with the same value, then that result is returned.
63+
Otherwise, the result returned by the configured "fallback provider" will be used. When values do not agree, an optional callback will be executed to notify
64+
you of the mismatch. This can be useful when migrating between providers that are expected to contain identical configuration. You can easily spot mismatches
65+
in configuration without affecting flag behaviour.
66+
67+
This strategy accepts several arguments during initialization:
68+
69+
```typescript
70+
import { MultiProvider, ComparisonStrategy } from '@openfeature/server-sdk'
71+
72+
const providerA = new ProviderA()
73+
const multiProvider = new MultiProvider(
74+
[
75+
{ provider: providerA },
76+
{ provider: new ProviderB() }
77+
],
78+
new ComparisonStrategy(providerA, (details) => {
79+
console.log("Mismatch detected", details)
80+
})
81+
)
82+
```
83+
84+
The first argument is the "fallback provider" whose value to use in the event that providers do not agree. It should be the same object reference as one of the providers in the list. The second argument is a callback function that will be executed when a mismatch is detected. The callback will be passed an object containing the details of each provider's resolution, including the flag key, the value returned, and any errors that were thrown.
85+
86+
## Tracking Support
87+
88+
The Multi-Provider supports tracking events across multiple providers. When you call the `track` method, it will by default send the tracking event to all underlying providers that implement the `track` method.
89+
90+
```typescript
91+
import { OpenFeature } from '@openfeature/server-sdk'
92+
import { MultiProvider } from '@openfeature/server-sdk'
93+
94+
const multiProvider = new MultiProvider([
95+
{ provider: new ProviderA() },
96+
{ provider: new ProviderB() }
97+
])
98+
99+
await OpenFeature.setProviderAndWait(multiProvider)
100+
const client = OpenFeature.getClient()
101+
102+
// Tracked events will be sent to all providers by default
103+
client.track('purchase', { targetingKey: 'user123' }, { value: 99.99, currency: 'USD' })
104+
```
105+
106+
### Tracking Behavior
107+
108+
- **Default**: All providers receive tracking calls by default
109+
- **Error Handling**: If one provider fails to track, others continue normally and errors are logged
110+
- **Provider Status**: Providers in `NOT_READY` or `FATAL` status are automatically skipped
111+
- **Optional Method**: Providers without a `track` method are gracefully skipped
112+
113+
### Customizing Tracking with Strategies
114+
115+
You can customize which providers receive tracking calls by overriding the `shouldTrackWithThisProvider` method in your custom strategy:
116+
117+
```typescript
118+
import { BaseEvaluationStrategy, StrategyPerProviderContext } from '@openfeature/server-sdk'
119+
120+
class CustomTrackingStrategy extends BaseEvaluationStrategy {
121+
shouldTrackWithThisProvider(
122+
strategyContext: StrategyPerProviderContext,
123+
context: EvaluationContext,
124+
trackingEventName: string,
125+
trackingEventDetails: TrackingEventDetails,
126+
): boolean {
127+
// Only track with the primary provider
128+
if (strategyContext.providerName === 'primary-provider') {
129+
return true;
130+
}
131+
132+
// Skip tracking for analytics events on backup providers
133+
if (trackingEventName.startsWith('analytics.')) {
134+
return false;
135+
}
136+
137+
return super.shouldTrackWithThisProvider(strategyContext, context, trackingEventName, trackingEventDetails);
138+
}
139+
}
140+
```
141+
142+
## Custom Strategies
143+
144+
It is also possible to implement your own strategy if the above options do not fit your use case. To do so, create a class which implements the "BaseEvaluationStrategy":
145+
146+
```typescript
147+
export abstract class BaseEvaluationStrategy {
148+
public runMode: 'parallel' | 'sequential' = 'sequential';
149+
150+
abstract shouldEvaluateThisProvider(strategyContext: StrategyPerProviderContext, evalContext: EvaluationContext): boolean;
151+
152+
abstract shouldEvaluateNextProvider<T extends FlagValue>(
153+
strategyContext: StrategyPerProviderContext,
154+
context: EvaluationContext,
155+
result: ProviderResolutionResult<T>,
156+
): boolean;
157+
158+
abstract shouldTrackWithThisProvider(
159+
strategyContext: StrategyPerProviderContext,
160+
context: EvaluationContext,
161+
trackingEventName: string,
162+
trackingEventDetails: TrackingEventDetails,
163+
): boolean;
164+
165+
abstract determineFinalResult<T extends FlagValue>(
166+
strategyContext: StrategyEvaluationContext,
167+
context: EvaluationContext,
168+
resolutions: ProviderResolutionResult<T>[],
169+
): FinalResult<T>;
170+
}
171+
```
172+
173+
The `runMode` property determines whether the list of providers will be evaluated sequentially or in parallel.
174+
175+
The `shouldEvaluateThisProvider` method is called just before a provider is evaluated by the Multi-Provider. If the function returns `false`, then
176+
the provider will be skipped instead of being evaluated. The function is called with details about the evaluation including the flag key and type.
177+
Check the type definitions for the full list.
178+
179+
The `shouldEvaluateNextProvider` function is called after a provider is evaluated. If it returns `true`, the next provider in the sequence will be called,
180+
otherwise no more providers will be evaluated. It is called with the same data as `shouldEvaluateThisProvider` as well as the details about the evaluation result. This function is not called when the `runMode` is `parallel`.
181+
182+
The `shouldTrackWithThisProvider` method is called before sending a tracking event to each provider. Return `false` to skip tracking with that provider. By default, it only tracks with providers that are in a ready state (not `NOT_READY` or `FATAL`). Override this method to implement custom tracking logic based on the tracking event name, details, or provider characteristics.
183+
184+
The `determineFinalResult` function is called after all providers have been called, or the `shouldEvaluateNextProvider` function returned false. It is called
185+
with a list of results from all the individual providers' evaluations. It returns the final decision for evaluation result, or throws an error if needed.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { ErrorCode } from '@openfeature/core';
2+
import { GeneralError, OpenFeatureError } from '@openfeature/core';
3+
import type { RegisteredProvider } from './types';
4+
5+
export class ErrorWithCode extends OpenFeatureError {
6+
constructor(
7+
public code: ErrorCode,
8+
message: string,
9+
) {
10+
super(message);
11+
}
12+
}
13+
14+
export class AggregateError extends GeneralError {
15+
constructor(
16+
message: string,
17+
public originalErrors: { source: string; error: unknown }[],
18+
) {
19+
super(message);
20+
}
21+
}
22+
23+
export const constructAggregateError = (providerErrors: { error: unknown; providerName: string }[]) => {
24+
const errorsWithSource = providerErrors
25+
.map(({ providerName, error }) => {
26+
return { source: providerName, error };
27+
})
28+
.flat();
29+
30+
// log first error in the message for convenience, but include all errors in the error object for completeness
31+
return new AggregateError(
32+
`Provider errors occurred: ${errorsWithSource[0].source}: ${errorsWithSource[0].error}`,
33+
errorsWithSource,
34+
);
35+
};
36+
37+
export const throwAggregateErrorFromPromiseResults = (
38+
result: PromiseSettledResult<unknown>[],
39+
providerEntries: RegisteredProvider[],
40+
) => {
41+
const errors = result
42+
.map((r, i) => {
43+
if (r.status === 'rejected') {
44+
return { error: r.reason, providerName: providerEntries[i].name };
45+
}
46+
return null;
47+
})
48+
.filter((val): val is { error: unknown; providerName: string } => Boolean(val));
49+
50+
if (errors.length) {
51+
throw constructAggregateError(errors);
52+
}
53+
};

0 commit comments

Comments
 (0)