Skip to content

Commit 62efbe5

Browse files
Add call to data collector
Signed-off-by: Thomas Poignant <[email protected]>
1 parent c5b07c5 commit 62efbe5

File tree

4 files changed

+142
-23
lines changed

4 files changed

+142
-23
lines changed

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

+92-18
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,22 @@ import {
99
TypeMismatchError,
1010
} from '@openfeature/js-sdk';
1111
import axios from 'axios';
12-
import { transformContext } from './context-transformer';
13-
import { ProxyNotReady } from './errors/proxyNotReady';
14-
import { ProxyTimeout } from './errors/proxyTimeout';
15-
import { UnknownError } from './errors/unknownError';
16-
import { Unauthorized } from './errors/unauthorized';
12+
import {transformContext} from './context-transformer';
13+
import {ProxyNotReady} from './errors/proxyNotReady';
14+
import {ProxyTimeout} from './errors/proxyTimeout';
15+
import {UnknownError} from './errors/unknownError';
16+
import {Unauthorized} from './errors/unauthorized';
1717
import {
18+
DataCollectorRequest,
19+
DataCollectorResponse,
20+
FeatureEvent,
1821
GoFeatureFlagProviderOptions,
1922
GoFeatureFlagProxyRequest,
2023
GoFeatureFlagProxyResponse,
2124
GoFeatureFlagUser,
2225
} from './model';
2326
import Receptacle from 'receptacle';
2427

25-
2628
// GoFeatureFlagProvider is the official Open-feature provider for GO Feature Flag.
2729
export class GoFeatureFlagProvider implements Provider {
2830
metadata = {
@@ -33,29 +35,90 @@ export class GoFeatureFlagProvider implements Provider {
3335
private readonly endpoint: string;
3436
// timeout in millisecond before we consider the request as a failure
3537
private readonly timeout: number;
38+
39+
3640
// cache contains the local cache used in the provider to avoid calling the relay-proxy for every evaluation
37-
private cache?: Receptacle<ResolutionDetails<any>>;
41+
private readonly cache?: Receptacle<ResolutionDetails<any>>;
42+
// bgSchedulerId contains the id of the setInterval that is running.
43+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
44+
// @ts-ignore
45+
private bgScheduler?: NodeJS.Timer;
46+
47+
// dataCollectorBuffer contains all the FeatureEvents that we need to send to the relay-proxy for data collection.
48+
private dataCollectorBuffer?: FeatureEvent<any>[];
49+
// dataCollectorMetadata are the metadata used when calling the data collector endpoint
50+
private readonly dataCollectorMetadata: Record<string, string> = {
51+
provider: 'open-feature-js-sdk',
52+
};
3853

3954
// cacheTTL is the time we keep the evaluation in the cache before we consider it as obsolete.
4055
// If you want to keep the value forever you can set the FlagCacheTTL field to -1
41-
private readonly cacheTTL?: number
56+
private readonly cacheTTL?: number;
57+
58+
// dataFlushInterval interval time (in millisecond) we use to call the relay proxy to collect data.
59+
private readonly dataFlushInterval: number;
4260

4361
constructor(options: GoFeatureFlagProviderOptions) {
4462
this.timeout = options.timeout || 0; // default is 0 = no timeout
4563
this.endpoint = options.endpoint;
4664
this.cacheTTL = options.flagCacheTTL !== undefined && options.flagCacheTTL !== 0 ? options.flagCacheTTL : 1000 * 60;
65+
this.dataFlushInterval = options.dataFlushInterval || 1000 * 60;
4766

4867
// Add API key to the headers
4968
if (options.apiKey) {
5069
axios.defaults.headers.common['Authorization'] = `Bearer ${options.apiKey}`;
5170
}
5271

53-
if(!options.disableCache) {
54-
const cacheSize = options.flagCacheSize!== undefined && options.flagCacheSize !== 0 ? options.flagCacheSize : 10000;
72+
if (!options.disableCache) {
73+
const cacheSize = options.flagCacheSize !== undefined && options.flagCacheSize !== 0 ? options.flagCacheSize : 10000;
5574
this.cache = new Receptacle<ResolutionDetails<any>>({max: cacheSize})
5675
}
5776
}
5877

78+
79+
async onClose() {
80+
if (this.cache !== undefined && this.bgScheduler !== undefined) {
81+
// we stop the background task to call the data collector endpoint
82+
clearInterval(this.bgScheduler);
83+
// We call the data collector with what is still in the buffer.
84+
await this.callGoffDataCollection()
85+
}
86+
}
87+
88+
async initialize() {
89+
if (this.cache !== undefined) {
90+
this.bgScheduler = setInterval(async () => await this.callGoffDataCollection(), this.dataFlushInterval)
91+
this.dataCollectorBuffer = []
92+
}
93+
}
94+
95+
async callGoffDataCollection() {
96+
if (this.dataCollectorBuffer?.length === 0) {
97+
return
98+
}
99+
100+
const dataToSend = structuredClone(this.dataCollectorBuffer);
101+
this.dataCollectorBuffer = []
102+
103+
const request: DataCollectorRequest<boolean> = {events: dataToSend, meta: this.dataCollectorMetadata,}
104+
const endpointURL = new URL(this.endpoint);
105+
endpointURL.pathname = 'v1/data/collector';
106+
107+
try {
108+
await axios.post<DataCollectorResponse>(endpointURL.toString(), request, {
109+
headers: {
110+
'Content-Type': 'application/json',
111+
Accept: 'application/json',
112+
},
113+
timeout: this.timeout,
114+
});
115+
} catch (e) {
116+
// TODO : add a log here
117+
// if we have an issue calling the collector we put the data back in the buffer
118+
this.dataCollectorBuffer = [...this.dataCollectorBuffer, ...dataToSend]
119+
}
120+
}
121+
59122
/**
60123
* resolveBooleanEvaluation is calling the GO Feature Flag relay-proxy API and return a boolean value.
61124
* @param flagKey - name of your feature flag key.
@@ -136,7 +199,7 @@ export class GoFeatureFlagProvider implements Provider {
136199
* @param flagKey - name of your feature flag key.
137200
* @param defaultValue - default value is used if we are not able to evaluate the flag for this user.
138201
* @param context - the context used for flag evaluation.
139-
* @return {Promise<ResolutionDetails<U>>} An object containing the result of the flag evaluation by GO Feature Flag.
202+
* @return {Promise<ResolutionDetails<U extends JsonValue>>} An object containing the result of the flag evaluation by GO Feature Flag.
140203
* @throws {ProxyNotReady} When we are not able to communicate with the relay-proxy
141204
* @throws {ProxyTimeout} When the HTTP call is timing out
142205
* @throws {UnknownError} When an unknown error occurs
@@ -182,12 +245,25 @@ export class GoFeatureFlagProvider implements Provider {
182245
if (this.cache !== undefined) {
183246
const cacheValue = this.cache.get(cacheKey);
184247
if (cacheValue !== null) {
185-
// console.log(this.cache.get(cacheKey).)
248+
// Building and inserting an event to the data collector buffer,
249+
// so we will be able to bulk send these events to GO Feature Flag.
250+
const dataCollectorEvent: FeatureEvent<T> = {
251+
contextKind: user.anonymous ? 'anonymousUser' : 'user',
252+
kind: 'feature',
253+
creationDate: Math.round(Date.now() / 1000),
254+
default: false,
255+
key: flagKey,
256+
value: cacheValue.value,
257+
variation: cacheValue.variant || 'SdkDefault',
258+
userKey: user.key,
259+
}
260+
this.dataCollectorBuffer?.push(dataCollectorEvent)
261+
186262
return cacheValue;
187263
}
188264
}
189265

190-
const request: GoFeatureFlagProxyRequest<T> = { user, defaultValue };
266+
const request: GoFeatureFlagProxyRequest<T> = {user, defaultValue};
191267
// build URL to access to the endpoint
192268
const endpointURL = new URL(this.endpoint);
193269
endpointURL.pathname = `v1/feature/${flagKey}/eval`;
@@ -249,12 +325,11 @@ export class GoFeatureFlagProvider implements Provider {
249325
if (apiResponseData.reason === StandardResolutionReasons.DISABLED) {
250326
// we don't set a variant since we are using the default value, and we are not able to know
251327
// which variant it is.
252-
return { value: defaultValue, reason: apiResponseData.reason };
328+
return {value: defaultValue, reason: apiResponseData.reason};
253329
}
254330

255331
const sdkResponse: ResolutionDetails<T> = {
256332
value: apiResponseData.value,
257-
reason: apiResponseData.reason?.toString() || 'UNKNOWN'
258333
variant: apiResponseData.variationType,
259334
reason: apiResponseData.reason?.toString() || 'UNKNOWN',
260335
flagMetadata: apiResponseData.metadata || undefined,
@@ -265,9 +340,8 @@ export class GoFeatureFlagProvider implements Provider {
265340
sdkResponse.errorCode = ErrorCode.GENERAL;
266341
}
267342

268-
if(this.cache!==undefined && apiResponseData.cacheable){
269-
console.log('add cache', cacheKey)
270-
if (this.cacheTTL === -1){
343+
if (this.cache !== undefined && apiResponseData.cacheable) {
344+
if (this.cacheTTL === -1) {
271345
this.cache.set(cacheKey, sdkResponse)
272346
} else {
273347
this.cache.set(cacheKey, sdkResponse, {ttl: this.cacheTTL, refresh: false})

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

+28
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,38 @@ export interface GoFeatureFlagProviderOptions {
6767
// If you want to keep the value forever you can set the FlagCacheTTL field to -1
6868
// default: 1 minute
6969
flagCacheTTL?: number
70+
71+
// dataFlushInterval (optional) interval time (in millisecond) we use to call the relay proxy to collect data.
72+
// The parameter is used only if the cache is enabled, otherwise the collection of the data is done directly
73+
// when calling the evaluation API.
74+
// default: 1 minute
75+
dataFlushInterval?: number
7076
}
7177

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

7581
// GOFeatureFlagErrorCode allows to extends error codes
7682
export declare enum GOFeatureFlagErrorCode {}
83+
84+
85+
export interface DataCollectorRequest<T> {
86+
events: FeatureEvent<T>[];
87+
meta: Record<string, string>;
88+
}
89+
90+
export interface FeatureEvent<T> {
91+
contextKind: string;
92+
creationDate: number;
93+
default: boolean;
94+
key: string;
95+
kind: string;
96+
userKey: string;
97+
value: T;
98+
variation: string;
99+
version?: string;
100+
}
101+
102+
export interface DataCollectorResponse {
103+
ingestedContentCount: number;
104+
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import {OpenFeature} from '@openfeature/js-sdk';
2-
import { GoFeatureFlagProvider } from './go-feature-flag-provider';
2+
import {GoFeatureFlagProvider} from './go-feature-flag-provider';
3+
4+
jest.setTimeout(1000 * 60 * 60);
35
it('XXX', async () => {
4-
const goff = new GoFeatureFlagProvider({endpoint:'http://localhost:1031', flagCacheTTL: 3000, flagCacheSize:1})
6+
const goff = new GoFeatureFlagProvider({
7+
endpoint: 'http://localhost:1031',
8+
flagCacheTTL: 3000,
9+
flagCacheSize: 1,
10+
dataFlushInterval: 1000000
11+
})
512
OpenFeature.setProvider(goff)
613
const cli = OpenFeature.getClient()
714
console.log(await cli.getBooleanDetails('bool_targeting_match', false, {targetingKey: 'my-key'}))
8-
await new Promise((r) => setTimeout(r, 3100));
915
console.log(await cli.getBooleanDetails('bool_targeting_match', false, {targetingKey: 'my-key'}))
16+
await new Promise((r) => setTimeout(r, 1000));
17+
console.log(await cli.getBooleanDetails('bool_targeting_match', false, {targetingKey: 'my-key'}))
18+
console.log(await cli.getBooleanDetails('bool_targeting_match', false, {targetingKey: 'my-key'}))
19+
console.log(await cli.getBooleanDetails('bool_targeting_match', false, {targetingKey: 'my-key'}))
20+
console.log(await cli.getBooleanDetails('bool_targeting_match', false, {targetingKey: 'my-key'}))
21+
console.log(await cli.getBooleanDetails('bool_targeting_match', false, {targetingKey: 'my-key'}))
22+
await OpenFeature.close()
1023

24+
// await new Promise((r) => setTimeout(r, 1000 * 60));
1125
})

libs/providers/go-feature-flag/tsconfig.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
"noImplicitOverride": true,
88
"noPropertyAccessFromIndexSignature": true,
99
"noImplicitReturns": true,
10-
"noFallthroughCasesInSwitch": true
10+
"noFallthroughCasesInSwitch": true,
11+
"types": [
12+
"node"
13+
]
1114
},
1215
"files": [],
1316
"include": [],
@@ -18,5 +21,5 @@
1821
{
1922
"path": "./tsconfig.spec.json"
2023
}
21-
]
24+
],
2225
}

0 commit comments

Comments
 (0)