Skip to content

Commit cd9834d

Browse files
authored
Merge branch 'main' into release-please--branches--main--components--go-feature-flag-provider
2 parents e5dcec0 + 17fd726 commit cd9834d

23 files changed

+232
-68
lines changed

.release-please-manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
"libs/providers/launchdarkly-client": "0.3.3",
99
"libs/providers/go-feature-flag-web": "0.2.6",
1010
"libs/shared/flagd-core": "1.1.0",
11-
"libs/shared/ofrep-core": "1.2.0",
11+
"libs/shared/ofrep-core": "2.0.0",
1212
"libs/providers/ofrep": "0.2.1",
13-
"libs/providers/ofrep-web": "0.3.4",
13+
"libs/providers/ofrep-web": "0.3.5",
1414
"libs/providers/flipt": "0.1.3",
1515
"libs/providers/flagsmith-client": "0.1.3",
1616
"libs/providers/flipt-web": "0.1.5",

libs/providers/flagd/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"current-version": "echo $npm_package_version"
88
},
99
"peerDependencies": {
10-
"@grpc/grpc-js": "~1.8.0 || ~1.9.0 || ~1.10.0 || ~1.11.0 || ~1.12.0 || ~1.13.0",
10+
"@grpc/grpc-js": "~1.8.0 || ~1.9.0 || ~1.10.0 || ~1.11.0 || ~1.12.0 || ~1.13.0 || ~1.14.0",
1111
"@openfeature/server-sdk": "^1.17.0"
1212
},
1313
"dependencies": {
Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
11
import { getGherkinTestPath } from '@openfeature/flagd-core';
22

3-
export const FLAGD_NAME = 'flagd';
43
export const UNSTABLE_CLIENT_NAME = 'unstable';
54
export const UNAVAILABLE_CLIENT_NAME = 'unavailable';
65

76
export const GHERKIN_FLAGD = getGherkinTestPath('*.feature');
8-
export const CONNECTION_FEATURE = getGherkinTestPath('connection.feature');
9-
export const CONTEXT_ENRICHMENT_FEATURE = getGherkinTestPath('contextEnrichment.feature');
10-
export const EVALUATION_FEATURE = getGherkinTestPath('evaluation.feature');
11-
export const EVENTS_FEATURE = getGherkinTestPath('events.feature');
12-
export const METADATA_FEATURE = getGherkinTestPath('metadata.feature');
13-
export const RPC_CACHING_FEATURE = getGherkinTestPath('rpc-caching.feature');
14-
export const SELECTOR_FEATURE = getGherkinTestPath('selector.feature');
15-
export const TARGETING_FEATURE = getGherkinTestPath('targeting.feature');
16-
export const GHERKIN_EVALUATION_FEATURE = getGherkinTestPath(
17-
'evaluation.feature',
18-
'spec/specification/assets/gherkin/',
19-
);

libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ProviderStatus } from '@openfeature/server-sdk';
12
import { OpenFeature } from '@openfeature/server-sdk';
23
import { FlagdContainer } from '../tests/flagdContainer';
34
import type { State, Steps } from './state';
@@ -6,7 +7,7 @@ import type { FlagdProviderOptions } from '../../lib/configuration';
67

78
export const providerSteps: Steps =
89
(state: State) =>
9-
({ given, when, then }) => {
10+
({ given, when, then, and }) => {
1011
const container: FlagdContainer = FlagdContainer.build();
1112
beforeAll(async () => {
1213
console.log('Setting flagd provider...');
@@ -33,26 +34,29 @@ export const providerSteps: Steps =
3334
const flagdOptions: FlagdProviderOptions = {
3435
resolverType: state.resolverType,
3536
deadlineMs: 2000,
37+
...state.config,
38+
...state.options,
3639
};
40+
3741
let type = 'default';
3842
switch (providerType) {
39-
default:
40-
flagdOptions['port'] = container.getPort(state.resolverType);
41-
if (state?.options?.['selector']) {
42-
flagdOptions['selector'] = state?.options?.['selector'] as string;
43-
}
44-
break;
4543
case 'unavailable':
4644
flagdOptions['port'] = 9999;
4745
break;
4846
case 'ssl':
4947
// TODO: modify this to support ssl
5048
flagdOptions['port'] = container.getPort(state.resolverType);
51-
if (state?.config?.selector) {
52-
flagdOptions['selector'] = state.config.selector;
53-
}
5449
type = 'ssl';
5550
break;
51+
case 'stable':
52+
flagdOptions['port'] = container.getPort(state.resolverType);
53+
break;
54+
case 'syncpayload':
55+
flagdOptions['port'] = container.getPort(state.resolverType);
56+
type = 'sync-payload';
57+
break;
58+
default:
59+
throw new Error('unknown provider type: ' + providerType);
5660
}
5761

5862
await fetch('http://' + container.getLaunchpadUrl() + '/start?config=' + type);
@@ -67,6 +71,21 @@ export const providerSteps: Steps =
6771
state.providerType = providerType;
6872
});
6973

74+
function mapProviderState(state: string): ProviderStatus {
75+
const mappedState = state.toUpperCase().replace('-', '_');
76+
const status = Object.values(ProviderStatus).find((s) => s === mappedState);
77+
78+
if (!status) {
79+
throw new Error(`Unknown provider status: ${state}`);
80+
}
81+
82+
return status;
83+
}
84+
85+
then(/^the client should be in (.*) state/, (providerState: string) => {
86+
expect(state.client?.providerStatus).toBe(mapProviderState(providerState));
87+
});
88+
7089
when(/^the connection is lost for (\d+)s$/, async (time) => {
7190
console.log('stopping flagd');
7291
await fetch('http://' + container.getLaunchpadUrl() + '/restart?seconds=' + time);

libs/providers/flagd/src/e2e/step-definitions/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export function mapValueToType(value: string, type: string): any {
1919
return value.toLowerCase() as ResolverType;
2020
case 'CacheType':
2121
return value as CacheOption;
22+
case 'StringList':
23+
return value.split(',').map((item) => item.trim());
2224
case 'Object':
2325
if (value == 'null') {
2426
return undefined;

libs/providers/flagd/src/e2e/tests/in-process.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('in-process', () => {
2222
// remove filters as we add support for features
2323
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
2424
tagFilter:
25-
'@in-process and not @targetURI and not @customCert and not @events and not @sync and not @grace and not @metadata and not @contextEnrichment',
25+
'@in-process and not @targetURI and not @forbidden and not @customCert and not @events and not @sync and not @grace and not @metadata and not @unixsocket',
2626
scenarioNameTemplate: (vars) => {
2727
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
2828
},

libs/providers/flagd/src/e2e/tests/rpc.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('rpc', () => {
2323
tagFilter:
2424
// remove filters as we add support for features
2525
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
26-
'@rpc and not @targetURI and not @customCert and not @events and not @stream and not @grace and not @metadata and not @contextEnrichment and not @caching',
26+
'@rpc and not @targetURI and not @customCert and not @forbidden and not @events and not @stream and not @grace and not @metadata and not @caching and not @unixsocket',
2727
scenarioNameTemplate: (vars) => {
2828
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
2929
},
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Hook, EvaluationContext, BeforeHookContext, HookHints } from '@openfeature/server-sdk';
2+
3+
export class SyncMetadataHook implements Hook {
4+
enrichedContext: () => EvaluationContext;
5+
6+
constructor(enrichedContext: () => EvaluationContext) {
7+
this.enrichedContext = enrichedContext;
8+
}
9+
10+
public before(hookContext: BeforeHookContext, hookHints?: HookHints): EvaluationContext {
11+
return this.enrichedContext();
12+
}
13+
}

libs/providers/flagd/src/lib/configuration.spec.ts

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Config, FlagdProviderOptions } from './configuration';
22
import { getConfig } from './configuration';
33
import { DEFAULT_MAX_CACHE_SIZE } from './constants';
4+
import type { EvaluationContext } from '@openfeature/server-sdk';
45

56
describe('Configuration', () => {
67
const OLD_ENV = process.env;
@@ -20,6 +21,7 @@ describe('Configuration', () => {
2021
resolverType: 'rpc',
2122
selector: '',
2223
deadlineMs: 500,
24+
contextEnricher: expect.any(Function),
2325
});
2426
});
2527

@@ -46,22 +48,53 @@ describe('Configuration', () => {
4648
process.env['FLAGD_OFFLINE_FLAG_SOURCE_PATH'] = offlineFlagSourcePath;
4749
process.env['FLAGD_DEFAULT_AUTHORITY'] = defaultAuthority;
4850

49-
expect(getConfig()).toStrictEqual({
50-
host,
51-
port,
52-
tls,
53-
socketPath,
54-
maxCacheSize,
55-
cache,
56-
resolverType,
57-
selector,
58-
offlineFlagSourcePath,
59-
defaultAuthority,
60-
deadlineMs: 500,
61-
});
51+
expect(getConfig()).toEqual(
52+
expect.objectContaining({
53+
host,
54+
port,
55+
tls,
56+
socketPath,
57+
maxCacheSize,
58+
cache,
59+
resolverType,
60+
selector,
61+
offlineFlagSourcePath,
62+
defaultAuthority,
63+
deadlineMs: 500,
64+
}),
65+
);
66+
});
67+
68+
it('should override context enricher', () => {
69+
const contextEnricher = (syncContext: EvaluationContext | null): EvaluationContext => {
70+
return { ...syncContext, extraKey: 'extraValue' };
71+
};
72+
73+
expect(getConfig({ contextEnricher }).contextEnricher({})).toEqual({ extraKey: 'extraValue' });
74+
});
75+
76+
it('should return identity function', () => {
77+
expect(getConfig().contextEnricher({})).toStrictEqual({});
78+
});
79+
80+
it('should use flagd sync port over flagd port environment option', () => {
81+
const port = 8080;
82+
const syncPort = 9090;
83+
84+
process.env['FLAGD_PORT'] = `${port}`;
85+
process.env['FLAGD_SYNC_PORT'] = `${syncPort}`;
86+
87+
expect(getConfig()).toStrictEqual(
88+
expect.objectContaining({
89+
port: syncPort,
90+
}),
91+
);
6292
});
6393

6494
it('should use incoming options over defaults and environment variable', () => {
95+
const contextEnricher = (syncContext: EvaluationContext | null): EvaluationContext => {
96+
return { ...syncContext, extraKey: 'extraValue' };
97+
};
6598
const options: FlagdProviderOptions = {
6699
host: 'test',
67100
port: 3000,
@@ -72,10 +105,12 @@ describe('Configuration', () => {
72105
selector: '',
73106
defaultAuthority: '',
74107
deadlineMs: 500,
108+
contextEnricher: contextEnricher,
75109
};
76110

77111
process.env['FLAGD_HOST'] = 'override';
78112
process.env['FLAGD_PORT'] = '8080';
113+
process.env['FLAGD_SYNC_PORT'] = '9090';
79114
process.env['FLAGD_TLS'] = 'false';
80115
process.env['FLAGD_DEFAULT_AUTHORITY'] = 'test-authority-override';
81116

@@ -87,6 +122,11 @@ describe('Configuration', () => {
87122
expect(getConfig()).toStrictEqual(expect.objectContaining({ port: 8013 }));
88123
});
89124

125+
it('should ignore an invalid sync port set as an environment variable', () => {
126+
process.env['FLAGD_SYNC_PORT'] = 'invalid number';
127+
expect(getConfig()).toStrictEqual(expect.objectContaining({ port: 8013 }));
128+
});
129+
90130
describe('port handling', () => {
91131
describe('for "in-process" evaluation', () => {
92132
const resolverType = 'in-process';

libs/providers/flagd/src/lib/configuration.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DEFAULT_MAX_CACHE_SIZE } from './constants';
2+
import type { EvaluationContext } from '@openfeature/server-sdk';
23

34
export type CacheOption = 'lru' | 'disabled';
45
export type ResolverType = 'rpc' | 'in-process';
@@ -83,24 +84,39 @@ export interface Config {
8384
defaultAuthority?: string;
8485
}
8586

86-
export type FlagdProviderOptions = Partial<Config>;
87+
interface FlagdConfig extends Config {
88+
/**
89+
* Function providing an EvaluationContext to mix into every evaluation.
90+
* The syncContext from the SyncFlagsResponse
91+
* (https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.SyncFlagsResponse),
92+
* represented as a {@link dev.openfeature.sdk.Structure}, is passed as an argument.
93+
*
94+
* This function runs every time the provider (re)connects, and its result is cached and used in every evaluation.
95+
* By default, the entire sync response (as a JSON Object) is used.
96+
*/
97+
contextEnricher: (syncContext: EvaluationContext | null) => EvaluationContext;
98+
}
99+
100+
export type FlagdProviderOptions = Partial<FlagdConfig>;
87101

88-
const DEFAULT_CONFIG: Omit<Config, 'port' | 'resolverType'> = {
102+
const DEFAULT_CONFIG: Omit<FlagdConfig, 'port' | 'resolverType'> = {
89103
deadlineMs: 500,
90104
host: 'localhost',
91105
tls: false,
92106
selector: '',
93107
cache: 'lru',
94108
maxCacheSize: DEFAULT_MAX_CACHE_SIZE,
109+
contextEnricher: (syncContext: EvaluationContext | null) => syncContext ?? {},
95110
};
96111

97-
const DEFAULT_RPC_CONFIG: Config = { ...DEFAULT_CONFIG, resolverType: 'rpc', port: 8013 };
112+
const DEFAULT_RPC_CONFIG: FlagdConfig = { ...DEFAULT_CONFIG, resolverType: 'rpc', port: 8013 };
98113

99-
const DEFAULT_IN_PROCESS_CONFIG: Config = { ...DEFAULT_CONFIG, resolverType: 'in-process', port: 8015 };
114+
const DEFAULT_IN_PROCESS_CONFIG: FlagdConfig = { ...DEFAULT_CONFIG, resolverType: 'in-process', port: 8015 };
100115

101116
enum ENV_VAR {
102117
FLAGD_HOST = 'FLAGD_HOST',
103118
FLAGD_PORT = 'FLAGD_PORT',
119+
FLAGD_SYNC_PORT = 'FLAGD_SYNC_PORT',
104120
FLAGD_DEADLINE_MS = 'FLAGD_DEADLINE_MS',
105121
FLAGD_TLS = 'FLAGD_TLS',
106122
FLAGD_SOCKET_PATH = 'FLAGD_SOCKET_PATH',
@@ -137,6 +153,9 @@ const getEnvVarConfig = (): Partial<Config> => {
137153
...(Number(process.env[ENV_VAR.FLAGD_PORT]) && {
138154
port: Number(process.env[ENV_VAR.FLAGD_PORT]),
139155
}),
156+
...(Number(process.env[ENV_VAR.FLAGD_SYNC_PORT]) && {
157+
port: Number(process.env[ENV_VAR.FLAGD_SYNC_PORT]),
158+
}),
140159
...(Number(process.env[ENV_VAR.FLAGD_DEADLINE_MS]) && {
141160
deadlineMs: Number(process.env[ENV_VAR.FLAGD_DEADLINE_MS]),
142161
}),
@@ -167,7 +186,7 @@ const getEnvVarConfig = (): Partial<Config> => {
167186
};
168187
};
169188

170-
export function getConfig(options: FlagdProviderOptions = {}) {
189+
export function getConfig(options: FlagdProviderOptions = {}): FlagdConfig {
171190
const envVarConfig = getEnvVarConfig();
172191
const defaultConfig =
173192
options.resolverType == 'in-process' || envVarConfig.resolverType == 'in-process'

0 commit comments

Comments
 (0)