From 0edf3f55161878684ffe809fbde8ad6fa379c4b0 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 16 Jan 2025 18:37:26 +0100 Subject: [PATCH] feat(go-feature-flag): Support exporter metadata in web and server providers (#1183) Signed-off-by: Thomas Poignant --- .../src/lib/controller/goff-api.ts | 4 +- .../src/lib/data-collector-hook.ts | 11 ++++-- .../lib/go-feature-flag-web-provider.spec.ts | 37 ++++++++++++++++++- .../go-feature-flag-web/src/lib/model.ts | 16 ++++++-- .../src/lib/controller/goff-api.ts | 3 +- .../src/lib/data-collector-hook.ts | 11 ++++-- .../src/lib/go-feature-flag-provider.spec.ts | 29 ++++++++++++++- .../src/lib/go-feature-flag-provider.ts | 6 ++- .../go-feature-flag/src/lib/model.ts | 19 +++++++++- 9 files changed, 117 insertions(+), 19 deletions(-) diff --git a/libs/providers/go-feature-flag-web/src/lib/controller/goff-api.ts b/libs/providers/go-feature-flag-web/src/lib/controller/goff-api.ts index 8602a60fd..05a38dd7f 100644 --- a/libs/providers/go-feature-flag-web/src/lib/controller/goff-api.ts +++ b/libs/providers/go-feature-flag-web/src/lib/controller/goff-api.ts @@ -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 { @@ -15,7 +15,7 @@ export class GoffApiController { this.options = options; } - async collectData(events: FeatureEvent[], dataCollectorMetadata: Record) { + async collectData(events: FeatureEvent[], dataCollectorMetadata: Record) { if (events?.length === 0) { return; } diff --git a/libs/providers/go-feature-flag-web/src/lib/data-collector-hook.ts b/libs/providers/go-feature-flag-web/src/lib/data-collector-hook.ts index b0878be39..dccb7dfb5 100644 --- a/libs/providers/go-feature-flag-web/src/lib/data-collector-hook.ts +++ b/libs/providers/go-feature-flag-web/src/lib/data-collector-hook.ts @@ -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'; @@ -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 = { - provider: 'open-feature-js-sdk', - }; + private readonly dataCollectorMetadata: Record; private readonly goffApiController: GoffApiController; // logger is the Open Feature logger to use private logger?: Logger; @@ -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() { diff --git a/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.spec.ts b/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.spec.ts index df0e04f33..2ed757c18 100644 --- a/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.spec.ts +++ b/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.spec.ts @@ -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', () => { @@ -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; + expect(body.meta).toEqual({ browser: 'chrome', version: '1.0.0', score: 123, openfeature: true, provider: 'web' }); + }); }); class MockWebSocketConnectingState extends WebSocket { diff --git a/libs/providers/go-feature-flag-web/src/lib/model.ts b/libs/providers/go-feature-flag-web/src/lib/model.ts index bd278d727..8982c2375 100644 --- a/libs/providers/go-feature-flag-web/src/lib/model.ts +++ b/libs/providers/go-feature-flag-web/src/lib/model.ts @@ -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; @@ -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; } +// 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. */ @@ -97,7 +107,7 @@ export interface GOFeatureFlagWebsocketResponse { export interface DataCollectorRequest { events: FeatureEvent[]; - meta: Record; + meta: Record; } export interface FeatureEvent { diff --git a/libs/providers/go-feature-flag/src/lib/controller/goff-api.ts b/libs/providers/go-feature-flag/src/lib/controller/goff-api.ts index ddc5f58c5..6e0c6f479 100644 --- a/libs/providers/go-feature-flag/src/lib/controller/goff-api.ts +++ b/libs/providers/go-feature-flag/src/lib/controller/goff-api.ts @@ -2,6 +2,7 @@ import { ConfigurationChange, DataCollectorRequest, DataCollectorResponse, + ExporterMetadataValue, FeatureEvent, GoFeatureFlagProviderOptions, GoFeatureFlagProxyRequest, @@ -146,7 +147,7 @@ export class GoffApiController { }; } - async collectData(events: FeatureEvent[], dataCollectorMetadata: Record) { + async collectData(events: FeatureEvent[], dataCollectorMetadata: Record) { if (events?.length === 0) { return; } diff --git a/libs/providers/go-feature-flag/src/lib/data-collector-hook.ts b/libs/providers/go-feature-flag/src/lib/data-collector-hook.ts index f4f8ab534..7d297821a 100644 --- a/libs/providers/go-feature-flag/src/lib/data-collector-hook.ts +++ b/libs/providers/go-feature-flag/src/lib/data-collector-hook.ts @@ -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'; @@ -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 = { - provider: 'open-feature-js-sdk', - }; + private readonly dataCollectorMetadata: Record; private readonly goffApiController: GoffApiController; // logger is the Open Feature logger to use private logger?: Logger; @@ -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() { diff --git a/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.spec.ts b/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.spec.ts index b872a714b..8e19e00fa 100644 --- a/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.spec.ts +++ b/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.spec.ts @@ -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); @@ -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 () => { @@ -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); @@ -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); @@ -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); @@ -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, ); diff --git a/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts b/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts index 7aa12de4f..514b5ce59 100644 --- a/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts +++ b/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts @@ -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, ); diff --git a/libs/providers/go-feature-flag/src/lib/model.ts b/libs/providers/go-feature-flag/src/lib/model.ts index b2873e010..dc33ecd91 100644 --- a/libs/providers/go-feature-flag/src/lib/model.ts +++ b/libs/providers/go-feature-flag/src/lib/model.ts @@ -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; } +// 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 { events: FeatureEvent[]; - meta: Record; + meta: Record; } export interface FeatureEvent { @@ -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; } export enum ConfigurationChange {