@@ -9,20 +9,22 @@ import {
9
9
TypeMismatchError ,
10
10
} from '@openfeature/js-sdk' ;
11
11
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' ;
17
17
import {
18
+ DataCollectorRequest ,
19
+ DataCollectorResponse ,
20
+ FeatureEvent ,
18
21
GoFeatureFlagProviderOptions ,
19
22
GoFeatureFlagProxyRequest ,
20
23
GoFeatureFlagProxyResponse ,
21
24
GoFeatureFlagUser ,
22
25
} from './model' ;
23
26
import Receptacle from 'receptacle' ;
24
27
25
-
26
28
// GoFeatureFlagProvider is the official Open-feature provider for GO Feature Flag.
27
29
export class GoFeatureFlagProvider implements Provider {
28
30
metadata = {
@@ -33,29 +35,90 @@ export class GoFeatureFlagProvider implements Provider {
33
35
private readonly endpoint : string ;
34
36
// timeout in millisecond before we consider the request as a failure
35
37
private readonly timeout : number ;
38
+
39
+
36
40
// 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
+ } ;
38
53
39
54
// cacheTTL is the time we keep the evaluation in the cache before we consider it as obsolete.
40
55
// 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 ;
42
60
43
61
constructor ( options : GoFeatureFlagProviderOptions ) {
44
62
this . timeout = options . timeout || 0 ; // default is 0 = no timeout
45
63
this . endpoint = options . endpoint ;
46
64
this . cacheTTL = options . flagCacheTTL !== undefined && options . flagCacheTTL !== 0 ? options . flagCacheTTL : 1000 * 60 ;
65
+ this . dataFlushInterval = options . dataFlushInterval || 1000 * 60 ;
47
66
48
67
// Add API key to the headers
49
68
if ( options . apiKey ) {
50
69
axios . defaults . headers . common [ 'Authorization' ] = `Bearer ${ options . apiKey } ` ;
51
70
}
52
71
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 ;
55
74
this . cache = new Receptacle < ResolutionDetails < any > > ( { max : cacheSize } )
56
75
}
57
76
}
58
77
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
+
59
122
/**
60
123
* resolveBooleanEvaluation is calling the GO Feature Flag relay-proxy API and return a boolean value.
61
124
* @param flagKey - name of your feature flag key.
@@ -136,7 +199,7 @@ export class GoFeatureFlagProvider implements Provider {
136
199
* @param flagKey - name of your feature flag key.
137
200
* @param defaultValue - default value is used if we are not able to evaluate the flag for this user.
138
201
* @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.
140
203
* @throws {ProxyNotReady } When we are not able to communicate with the relay-proxy
141
204
* @throws {ProxyTimeout } When the HTTP call is timing out
142
205
* @throws {UnknownError } When an unknown error occurs
@@ -182,12 +245,25 @@ export class GoFeatureFlagProvider implements Provider {
182
245
if ( this . cache !== undefined ) {
183
246
const cacheValue = this . cache . get ( cacheKey ) ;
184
247
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
+
186
262
return cacheValue ;
187
263
}
188
264
}
189
265
190
- const request : GoFeatureFlagProxyRequest < T > = { user, defaultValue } ;
266
+ const request : GoFeatureFlagProxyRequest < T > = { user, defaultValue} ;
191
267
// build URL to access to the endpoint
192
268
const endpointURL = new URL ( this . endpoint ) ;
193
269
endpointURL . pathname = `v1/feature/${ flagKey } /eval` ;
@@ -249,12 +325,11 @@ export class GoFeatureFlagProvider implements Provider {
249
325
if ( apiResponseData . reason === StandardResolutionReasons . DISABLED ) {
250
326
// we don't set a variant since we are using the default value, and we are not able to know
251
327
// which variant it is.
252
- return { value : defaultValue , reason : apiResponseData . reason } ;
328
+ return { value : defaultValue , reason : apiResponseData . reason } ;
253
329
}
254
330
255
331
const sdkResponse : ResolutionDetails < T > = {
256
332
value : apiResponseData . value ,
257
- reason : apiResponseData . reason ?. toString ( ) || 'UNKNOWN'
258
333
variant : apiResponseData . variationType ,
259
334
reason : apiResponseData . reason ?. toString ( ) || 'UNKNOWN' ,
260
335
flagMetadata : apiResponseData . metadata || undefined ,
@@ -265,9 +340,8 @@ export class GoFeatureFlagProvider implements Provider {
265
340
sdkResponse . errorCode = ErrorCode . GENERAL ;
266
341
}
267
342
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 ) {
271
345
this . cache . set ( cacheKey , sdkResponse )
272
346
} else {
273
347
this . cache . set ( cacheKey , sdkResponse , { ttl : this . cacheTTL , refresh : false } )
0 commit comments