Skip to content

Commit

Permalink
feat(go-feature-flag): Support exporter metadata in web and server pr…
Browse files Browse the repository at this point in the history
…oviders (#1183)

Signed-off-by: Thomas Poignant <[email protected]>
  • Loading branch information
thomaspoignant authored Jan 16, 2025
1 parent 6d5309b commit 0edf3f5
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DataCollectorRequest, FeatureEvent, GoFeatureFlagWebProviderOptions } from '../model';
import { DataCollectorRequest, ExporterMetadataValue, FeatureEvent, GoFeatureFlagWebProviderOptions } from '../model';
import { CollectorError } from '../errors/collector-error';

export class GoffApiController {
Expand All @@ -15,7 +15,7 @@ export class GoffApiController {
this.options = options;
}

async collectData(events: FeatureEvent<any>[], dataCollectorMetadata: Record<string, string>) {
async collectData(events: FeatureEvent<any>[], dataCollectorMetadata: Record<string, ExporterMetadataValue>) {
if (events?.length === 0) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EvaluationDetails, FlagValue, Hook, HookContext, Logger } from '@openfeature/web-sdk';
import { FeatureEvent, GoFeatureFlagWebProviderOptions } from './model';
import { ExporterMetadataValue, FeatureEvent, GoFeatureFlagWebProviderOptions } from './model';
import { copy } from 'copy-anything';
import { CollectorError } from './errors/collector-error';
import { GoffApiController } from './controller/goff-api';
Expand All @@ -15,9 +15,7 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
// dataFlushInterval interval time (in millisecond) we use to call the relay proxy to collect data.
private readonly dataFlushInterval: number;
// dataCollectorMetadata are the metadata used when calling the data collector endpoint
private readonly dataCollectorMetadata: Record<string, string> = {
provider: 'open-feature-js-sdk',
};
private readonly dataCollectorMetadata: Record<string, ExporterMetadataValue>;
private readonly goffApiController: GoffApiController;
// logger is the Open Feature logger to use
private logger?: Logger;
Expand All @@ -26,6 +24,11 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
this.dataFlushInterval = options.dataFlushInterval || 1000 * 60;
this.logger = logger;
this.goffApiController = new GoffApiController(options);
this.dataCollectorMetadata = {
provider: 'web',
openfeature: true,
...options.exporterMetadata,
};
}

init() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@openfeature/web-sdk';
import WS from 'jest-websocket-mock';
import TestLogger from './test-logger';
import { GOFeatureFlagWebsocketResponse } from './model';
import { DataCollectorRequest, GOFeatureFlagWebsocketResponse } from './model';
import fetchMock from 'fetch-mock-jest';

describe('GoFeatureFlagWebProvider', () => {
Expand Down Expand Up @@ -625,6 +625,41 @@ describe('GoFeatureFlagWebProvider', () => {
'timeout of 1000 ms reached when initializing the websocket',
);
});

it('should call the data collector with exporter metadata', async () => {
const clientName = expect.getState().currentTestName ?? 'test-provider';
await OpenFeature.setContext(defaultContext);
const p = new GoFeatureFlagWebProvider(
{
endpoint: endpoint,
apiTimeout: 1000,
maxRetries: 1,
dataFlushInterval: 10000,
apiKey: 'toto',
exporterMetadata: {
browser: 'chrome',
version: '1.0.0',
score: 123,
},
},
logger,
);

await OpenFeature.setProviderAndWait(clientName, p);
const client = OpenFeature.getClient(clientName);
await websocketMockServer.connected;
await new Promise((resolve) => setTimeout(resolve, 5));

client.getBooleanDetails('bool_flag', false);
client.getBooleanDetails('bool_flag', false);

await OpenFeature.close();

expect(fetchMock.calls(dataCollectorEndpoint).length).toBe(1);
const jsonBody = fetchMock.lastOptions(dataCollectorEndpoint)?.body;
const body = JSON.parse(jsonBody as never) as DataCollectorRequest<never>;
expect(body.meta).toEqual({ browser: 'chrome', version: '1.0.0', score: 123, openfeature: true, provider: 'web' });
});
});

class MockWebSocketConnectingState extends WebSocket {
Expand Down
16 changes: 13 additions & 3 deletions libs/providers/go-feature-flag-web/src/lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface GoFeatureFlagWebProviderOptions {
// Default: 100 ms
retryInitialDelay?: number;

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

// disableDataCollection set to true if you don't want to collect the usage of flags retrieved in the cache.
// disableDataCollection (optional) set to true if you don't want to collect the usage of flags retrieved in the cache.
disableDataCollection?: boolean;

// exporterMetadata (optional) exporter metadata is a set of key-value that will be added to the metadata when calling the
// exporter API. All those information will be added to the event produce by the exporter.
//
// ‼️Important: If you are using a GO Feature Flag relay proxy before version v1.41.0, the information
// of this field will not be added to your feature events.
exporterMetadata?: Record<string, ExporterMetadataValue>;
}

// ExporterMetadataValue is the type of the value that can be used in the exporterMetadata
export type ExporterMetadataValue = string | number | boolean;

/**
* FlagState is the object used to get the value return by GO Feature Flag.
*/
Expand Down Expand Up @@ -97,7 +107,7 @@ export interface GOFeatureFlagWebsocketResponse {

export interface DataCollectorRequest<T> {
events: FeatureEvent<T>[];
meta: Record<string, string>;
meta: Record<string, ExporterMetadataValue>;
}

export interface FeatureEvent<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ConfigurationChange,
DataCollectorRequest,
DataCollectorResponse,
ExporterMetadataValue,
FeatureEvent,
GoFeatureFlagProviderOptions,
GoFeatureFlagProxyRequest,
Expand Down Expand Up @@ -146,7 +147,7 @@ export class GoffApiController {
};
}

async collectData(events: FeatureEvent<any>[], dataCollectorMetadata: Record<string, string>) {
async collectData(events: FeatureEvent<any>[], dataCollectorMetadata: Record<string, ExporterMetadataValue>) {
if (events?.length === 0) {
return;
}
Expand Down
11 changes: 7 additions & 4 deletions libs/providers/go-feature-flag/src/lib/data-collector-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Logger,
StandardResolutionReasons,
} from '@openfeature/server-sdk';
import { DataCollectorHookOptions, FeatureEvent } from './model';
import { DataCollectorHookOptions, ExporterMetadataValue, FeatureEvent } from './model';
import { copy } from 'copy-anything';
import { CollectorError } from './errors/collector-error';
import { GoffApiController } from './controller/goff-api';
Expand All @@ -24,9 +24,7 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
// dataFlushInterval interval time (in millisecond) we use to call the relay proxy to collect data.
private readonly dataFlushInterval: number;
// dataCollectorMetadata are the metadata used when calling the data collector endpoint
private readonly dataCollectorMetadata: Record<string, string> = {
provider: 'open-feature-js-sdk',
};
private readonly dataCollectorMetadata: Record<string, ExporterMetadataValue>;
private readonly goffApiController: GoffApiController;
// logger is the Open Feature logger to use
private logger?: Logger;
Expand All @@ -36,6 +34,11 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
this.logger = logger;
this.goffApiController = goffApiController;
this.collectUnCachedEvaluation = options.collectUnCachedEvaluation;
this.dataCollectorMetadata = {
provider: 'js',
openfeature: true,
...options.exporterMetadata,
};
}

init() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,11 @@ describe('GoFeatureFlagProvider', () => {
flagCacheTTL: 3000,
flagCacheSize: 100,
dataFlushInterval: 1000, // in milliseconds
exporterMetadata: {
nodeJSVersion: '14.17.0',
appVersion: '1.0.0',
identifier: 123,
},
});
const providerName = expect.getState().currentTestName || 'test';
await OpenFeature.setProviderAndWait(providerName, goff);
Expand All @@ -896,9 +901,9 @@ describe('GoFeatureFlagProvider', () => {
userKey: 'user-key',
},
],
meta: { provider: 'open-feature-js-sdk' },
meta: { provider: 'js', openfeature: true, nodeJSVersion: '14.17.0', appVersion: '1.0.0', identifier: 123 },
};
expect(want).toEqual(got);
expect(got).toEqual(want);
});

it('should call the data collector when waiting more than the dataFlushInterval', async () => {
Expand All @@ -912,6 +917,11 @@ describe('GoFeatureFlagProvider', () => {
flagCacheTTL: 3000,
flagCacheSize: 100,
dataFlushInterval: 100, // in milliseconds
exporterMetadata: {
nodeJSVersion: '14.17.0',
appVersion: '1.0.0',
identifier: 123,
},
});
const providerName = expect.getState().currentTestName || 'test';
await OpenFeature.setProviderAndWait(providerName, goff);
Expand All @@ -934,6 +944,11 @@ describe('GoFeatureFlagProvider', () => {
flagCacheTTL: 3000,
flagCacheSize: 100,
dataFlushInterval: 100, // in milliseconds
exporterMetadata: {
nodeJSVersion: '14.17.0',
appVersion: '1.0.0',
identifier: 123,
},
});
const providerName = expect.getState().currentTestName || 'test';
await OpenFeature.setProviderAndWait(providerName, goff);
Expand Down Expand Up @@ -962,6 +977,11 @@ describe('GoFeatureFlagProvider', () => {
flagCacheTTL: 3000,
flagCacheSize: 100,
dataFlushInterval: 200, // in milliseconds
exporterMetadata: {
nodeJSVersion: '14.17.0',
appVersion: '1.0.0',
identifier: 123,
},
});
const providerName = expect.getState().currentTestName || 'test';
await OpenFeature.setProviderAndWait(providerName, goff);
Expand All @@ -988,6 +1008,11 @@ describe('GoFeatureFlagProvider', () => {
flagCacheTTL: 3000,
flagCacheSize: 100,
dataFlushInterval: 2000, // in milliseconds
exporterMetadata: {
nodeJSVersion: '14.17.0',
appVersion: '1.0.0',
identifier: 123,
},
},
testLogger,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ export class GoFeatureFlagProvider implements Provider {
constructor(options: GoFeatureFlagProviderOptions, logger?: Logger) {
this._goffApiController = new GoffApiController(options);
this._dataCollectorHook = new GoFeatureFlagDataCollectorHook(
{ dataFlushInterval: options.dataFlushInterval },
{
dataFlushInterval: options.dataFlushInterval,
collectUnCachedEvaluation: false,
exporterMetadata: options.exporterMetadata,
},
this._goffApiController,
logger,
);
Expand Down
19 changes: 18 additions & 1 deletion libs/providers/go-feature-flag/src/lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,24 @@ export interface GoFeatureFlagProviderOptions {
// If a negative number is provided, the provider will not poll.
// Default: 30000
pollInterval?: number; // in milliseconds

// exporterMetadata (optional) exporter metadata is a set of key-value that will be added to the metadata when calling the
// exporter API. All those information will be added to the event produce by the exporter.
//
// ‼️Important: If you are using a GO Feature Flag relay proxy before version v1.41.0, the information
// of this field will not be added to your feature events.
exporterMetadata?: Record<string, ExporterMetadataValue>;
}

// ExporterMetadataValue is the type of the value that can be used in the exporterMetadata
export type ExporterMetadataValue = string | number | boolean;

// GOFeatureFlagResolutionReasons allows to extends resolution reasons
export declare enum GOFeatureFlagResolutionReasons {}

export interface DataCollectorRequest<T> {
events: FeatureEvent<T>[];
meta: Record<string, string>;
meta: Record<string, ExporterMetadataValue>;
}

export interface FeatureEvent<T> {
Expand Down Expand Up @@ -107,6 +117,13 @@ export interface DataCollectorHookOptions {

// collectUnCachedEvent (optional) set to true if you want to send all events not only the cached evaluations.
collectUnCachedEvaluation?: boolean;

// exporterMetadata (optional) exporter metadata is a set of key-value that will be added to the metadata when calling the
// exporter API. All those information will be added to the event produce by the exporter.
//
// ‼️Important: If you are using a GO Feature Flag relay proxy before version v1.41.0, the information
// of this field will not be added to your feature events.
exporterMetadata?: Record<string, ExporterMetadataValue>;
}

export enum ConfigurationChange {
Expand Down

0 comments on commit 0edf3f5

Please sign in to comment.