Skip to content

Commit 0edf3f5

Browse files
feat(go-feature-flag): Support exporter metadata in web and server providers (#1183)
Signed-off-by: Thomas Poignant <[email protected]>
1 parent 6d5309b commit 0edf3f5

File tree

9 files changed

+117
-19
lines changed

9 files changed

+117
-19
lines changed

libs/providers/go-feature-flag-web/src/lib/controller/goff-api.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DataCollectorRequest, FeatureEvent, GoFeatureFlagWebProviderOptions } from '../model';
1+
import { DataCollectorRequest, ExporterMetadataValue, FeatureEvent, GoFeatureFlagWebProviderOptions } from '../model';
22
import { CollectorError } from '../errors/collector-error';
33

44
export class GoffApiController {
@@ -15,7 +15,7 @@ export class GoffApiController {
1515
this.options = options;
1616
}
1717

18-
async collectData(events: FeatureEvent<any>[], dataCollectorMetadata: Record<string, string>) {
18+
async collectData(events: FeatureEvent<any>[], dataCollectorMetadata: Record<string, ExporterMetadataValue>) {
1919
if (events?.length === 0) {
2020
return;
2121
}

libs/providers/go-feature-flag-web/src/lib/data-collector-hook.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EvaluationDetails, FlagValue, Hook, HookContext, Logger } from '@openfeature/web-sdk';
2-
import { FeatureEvent, GoFeatureFlagWebProviderOptions } from './model';
2+
import { ExporterMetadataValue, FeatureEvent, GoFeatureFlagWebProviderOptions } from './model';
33
import { copy } from 'copy-anything';
44
import { CollectorError } from './errors/collector-error';
55
import { GoffApiController } from './controller/goff-api';
@@ -15,9 +15,7 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
1515
// dataFlushInterval interval time (in millisecond) we use to call the relay proxy to collect data.
1616
private readonly dataFlushInterval: number;
1717
// dataCollectorMetadata are the metadata used when calling the data collector endpoint
18-
private readonly dataCollectorMetadata: Record<string, string> = {
19-
provider: 'open-feature-js-sdk',
20-
};
18+
private readonly dataCollectorMetadata: Record<string, ExporterMetadataValue>;
2119
private readonly goffApiController: GoffApiController;
2220
// logger is the Open Feature logger to use
2321
private logger?: Logger;
@@ -26,6 +24,11 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
2624
this.dataFlushInterval = options.dataFlushInterval || 1000 * 60;
2725
this.logger = logger;
2826
this.goffApiController = new GoffApiController(options);
27+
this.dataCollectorMetadata = {
28+
provider: 'web',
29+
openfeature: true,
30+
...options.exporterMetadata,
31+
};
2932
}
3033

3134
init() {

libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.spec.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '@openfeature/web-sdk';
1111
import WS from 'jest-websocket-mock';
1212
import TestLogger from './test-logger';
13-
import { GOFeatureFlagWebsocketResponse } from './model';
13+
import { DataCollectorRequest, GOFeatureFlagWebsocketResponse } from './model';
1414
import fetchMock from 'fetch-mock-jest';
1515

1616
describe('GoFeatureFlagWebProvider', () => {
@@ -625,6 +625,41 @@ describe('GoFeatureFlagWebProvider', () => {
625625
'timeout of 1000 ms reached when initializing the websocket',
626626
);
627627
});
628+
629+
it('should call the data collector with exporter metadata', async () => {
630+
const clientName = expect.getState().currentTestName ?? 'test-provider';
631+
await OpenFeature.setContext(defaultContext);
632+
const p = new GoFeatureFlagWebProvider(
633+
{
634+
endpoint: endpoint,
635+
apiTimeout: 1000,
636+
maxRetries: 1,
637+
dataFlushInterval: 10000,
638+
apiKey: 'toto',
639+
exporterMetadata: {
640+
browser: 'chrome',
641+
version: '1.0.0',
642+
score: 123,
643+
},
644+
},
645+
logger,
646+
);
647+
648+
await OpenFeature.setProviderAndWait(clientName, p);
649+
const client = OpenFeature.getClient(clientName);
650+
await websocketMockServer.connected;
651+
await new Promise((resolve) => setTimeout(resolve, 5));
652+
653+
client.getBooleanDetails('bool_flag', false);
654+
client.getBooleanDetails('bool_flag', false);
655+
656+
await OpenFeature.close();
657+
658+
expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(1);
659+
const jsonBody = fetchMock.lastOptions(dataCollectorEndpoint)?.body;
660+
const body = JSON.parse(jsonBody as never) as DataCollectorRequest<never>;
661+
expect(body.meta).toEqual({ browser: 'chrome', version: '1.0.0', score: 123, openfeature: true, provider: 'web' });
662+
});
628663
});
629664

630665
class MockWebSocketConnectingState extends WebSocket {

libs/providers/go-feature-flag-web/src/lib/model.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export interface GoFeatureFlagWebProviderOptions {
4343
// Default: 100 ms
4444
retryInitialDelay?: number;
4545

46-
// multiplier of retryInitialDelay after each failure
46+
// retryDelayMultiplier (optional) multiplier of retryInitialDelay after each failure
4747
// (example: 1st connection retry will be after 100ms, second after 200ms, third after 400ms ...)
4848
// Default: 2
4949
retryDelayMultiplier?: number;
@@ -58,10 +58,20 @@ export interface GoFeatureFlagWebProviderOptions {
5858
// default: 1 minute
5959
dataFlushInterval?: number;
6060

61-
// disableDataCollection set to true if you don't want to collect the usage of flags retrieved in the cache.
61+
// disableDataCollection (optional) set to true if you don't want to collect the usage of flags retrieved in the cache.
6262
disableDataCollection?: boolean;
63+
64+
// exporterMetadata (optional) exporter metadata is a set of key-value that will be added to the metadata when calling the
65+
// exporter API. All those information will be added to the event produce by the exporter.
66+
//
67+
// ‼️Important: If you are using a GO Feature Flag relay proxy before version v1.41.0, the information
68+
// of this field will not be added to your feature events.
69+
exporterMetadata?: Record<string, ExporterMetadataValue>;
6370
}
6471

72+
// ExporterMetadataValue is the type of the value that can be used in the exporterMetadata
73+
export type ExporterMetadataValue = string | number | boolean;
74+
6575
/**
6676
* FlagState is the object used to get the value return by GO Feature Flag.
6777
*/
@@ -97,7 +107,7 @@ export interface GOFeatureFlagWebsocketResponse {
97107

98108
export interface DataCollectorRequest<T> {
99109
events: FeatureEvent<T>[];
100-
meta: Record<string, string>;
110+
meta: Record<string, ExporterMetadataValue>;
101111
}
102112

103113
export interface FeatureEvent<T> {

libs/providers/go-feature-flag/src/lib/controller/goff-api.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
ConfigurationChange,
33
DataCollectorRequest,
44
DataCollectorResponse,
5+
ExporterMetadataValue,
56
FeatureEvent,
67
GoFeatureFlagProviderOptions,
78
GoFeatureFlagProxyRequest,
@@ -146,7 +147,7 @@ export class GoffApiController {
146147
};
147148
}
148149

149-
async collectData(events: FeatureEvent<any>[], dataCollectorMetadata: Record<string, string>) {
150+
async collectData(events: FeatureEvent<any>[], dataCollectorMetadata: Record<string, ExporterMetadataValue>) {
150151
if (events?.length === 0) {
151152
return;
152153
}

libs/providers/go-feature-flag/src/lib/data-collector-hook.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
Logger,
77
StandardResolutionReasons,
88
} from '@openfeature/server-sdk';
9-
import { DataCollectorHookOptions, FeatureEvent } from './model';
9+
import { DataCollectorHookOptions, ExporterMetadataValue, FeatureEvent } from './model';
1010
import { copy } from 'copy-anything';
1111
import { CollectorError } from './errors/collector-error';
1212
import { GoffApiController } from './controller/goff-api';
@@ -24,9 +24,7 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
2424
// dataFlushInterval interval time (in millisecond) we use to call the relay proxy to collect data.
2525
private readonly dataFlushInterval: number;
2626
// dataCollectorMetadata are the metadata used when calling the data collector endpoint
27-
private readonly dataCollectorMetadata: Record<string, string> = {
28-
provider: 'open-feature-js-sdk',
29-
};
27+
private readonly dataCollectorMetadata: Record<string, ExporterMetadataValue>;
3028
private readonly goffApiController: GoffApiController;
3129
// logger is the Open Feature logger to use
3230
private logger?: Logger;
@@ -36,6 +34,11 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
3634
this.logger = logger;
3735
this.goffApiController = goffApiController;
3836
this.collectUnCachedEvaluation = options.collectUnCachedEvaluation;
37+
this.dataCollectorMetadata = {
38+
provider: 'js',
39+
openfeature: true,
40+
...options.exporterMetadata,
41+
};
3942
}
4043

4144
init() {

libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.spec.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,11 @@ describe('GoFeatureFlagProvider', () => {
872872
flagCacheTTL: 3000,
873873
flagCacheSize: 100,
874874
dataFlushInterval: 1000, // in milliseconds
875+
exporterMetadata: {
876+
nodeJSVersion: '14.17.0',
877+
appVersion: '1.0.0',
878+
identifier: 123,
879+
},
875880
});
876881
const providerName = expect.getState().currentTestName || 'test';
877882
await OpenFeature.setProviderAndWait(providerName, goff);
@@ -896,9 +901,9 @@ describe('GoFeatureFlagProvider', () => {
896901
userKey: 'user-key',
897902
},
898903
],
899-
meta: { provider: 'open-feature-js-sdk' },
904+
meta: { provider: 'js', openfeature: true, nodeJSVersion: '14.17.0', appVersion: '1.0.0', identifier: 123 },
900905
};
901-
expect(want).toEqual(got);
906+
expect(got).toEqual(want);
902907
});
903908

904909
it('should call the data collector when waiting more than the dataFlushInterval', async () => {
@@ -912,6 +917,11 @@ describe('GoFeatureFlagProvider', () => {
912917
flagCacheTTL: 3000,
913918
flagCacheSize: 100,
914919
dataFlushInterval: 100, // in milliseconds
920+
exporterMetadata: {
921+
nodeJSVersion: '14.17.0',
922+
appVersion: '1.0.0',
923+
identifier: 123,
924+
},
915925
});
916926
const providerName = expect.getState().currentTestName || 'test';
917927
await OpenFeature.setProviderAndWait(providerName, goff);
@@ -934,6 +944,11 @@ describe('GoFeatureFlagProvider', () => {
934944
flagCacheTTL: 3000,
935945
flagCacheSize: 100,
936946
dataFlushInterval: 100, // in milliseconds
947+
exporterMetadata: {
948+
nodeJSVersion: '14.17.0',
949+
appVersion: '1.0.0',
950+
identifier: 123,
951+
},
937952
});
938953
const providerName = expect.getState().currentTestName || 'test';
939954
await OpenFeature.setProviderAndWait(providerName, goff);
@@ -962,6 +977,11 @@ describe('GoFeatureFlagProvider', () => {
962977
flagCacheTTL: 3000,
963978
flagCacheSize: 100,
964979
dataFlushInterval: 200, // in milliseconds
980+
exporterMetadata: {
981+
nodeJSVersion: '14.17.0',
982+
appVersion: '1.0.0',
983+
identifier: 123,
984+
},
965985
});
966986
const providerName = expect.getState().currentTestName || 'test';
967987
await OpenFeature.setProviderAndWait(providerName, goff);
@@ -988,6 +1008,11 @@ describe('GoFeatureFlagProvider', () => {
9881008
flagCacheTTL: 3000,
9891009
flagCacheSize: 100,
9901010
dataFlushInterval: 2000, // in milliseconds
1011+
exporterMetadata: {
1012+
nodeJSVersion: '14.17.0',
1013+
appVersion: '1.0.0',
1014+
identifier: 123,
1015+
},
9911016
},
9921017
testLogger,
9931018
);

libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export class GoFeatureFlagProvider implements Provider {
3838
constructor(options: GoFeatureFlagProviderOptions, logger?: Logger) {
3939
this._goffApiController = new GoffApiController(options);
4040
this._dataCollectorHook = new GoFeatureFlagDataCollectorHook(
41-
{ dataFlushInterval: options.dataFlushInterval },
41+
{
42+
dataFlushInterval: options.dataFlushInterval,
43+
collectUnCachedEvaluation: false,
44+
exporterMetadata: options.exporterMetadata,
45+
},
4246
this._goffApiController,
4347
logger,
4448
);

libs/providers/go-feature-flag/src/lib/model.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,24 @@ export interface GoFeatureFlagProviderOptions {
7272
// If a negative number is provided, the provider will not poll.
7373
// Default: 30000
7474
pollInterval?: number; // in milliseconds
75+
76+
// exporterMetadata (optional) exporter metadata is a set of key-value that will be added to the metadata when calling the
77+
// exporter API. All those information will be added to the event produce by the exporter.
78+
//
79+
// ‼️Important: If you are using a GO Feature Flag relay proxy before version v1.41.0, the information
80+
// of this field will not be added to your feature events.
81+
exporterMetadata?: Record<string, ExporterMetadataValue>;
7582
}
7683

84+
// ExporterMetadataValue is the type of the value that can be used in the exporterMetadata
85+
export type ExporterMetadataValue = string | number | boolean;
86+
7787
// GOFeatureFlagResolutionReasons allows to extends resolution reasons
7888
export declare enum GOFeatureFlagResolutionReasons {}
7989

8090
export interface DataCollectorRequest<T> {
8191
events: FeatureEvent<T>[];
82-
meta: Record<string, string>;
92+
meta: Record<string, ExporterMetadataValue>;
8393
}
8494

8595
export interface FeatureEvent<T> {
@@ -107,6 +117,13 @@ export interface DataCollectorHookOptions {
107117

108118
// collectUnCachedEvent (optional) set to true if you want to send all events not only the cached evaluations.
109119
collectUnCachedEvaluation?: boolean;
120+
121+
// exporterMetadata (optional) exporter metadata is a set of key-value that will be added to the metadata when calling the
122+
// exporter API. All those information will be added to the event produce by the exporter.
123+
//
124+
// ‼️Important: If you are using a GO Feature Flag relay proxy before version v1.41.0, the information
125+
// of this field will not be added to your feature events.
126+
exporterMetadata?: Record<string, ExporterMetadataValue>;
110127
}
111128

112129
export enum ConfigurationChange {

0 commit comments

Comments
 (0)