Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(go-feature-flag): Support exporter metadata in web and server providers #1183

Merged
merged 2 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading