@flagsmith/flagsmith 11.0.0-internal.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +38 -0
  2. package/evaluation-context.d.ts +27 -0
  3. package/evaluation-context.ts +29 -0
  4. package/flagsmith-core.d.ts +18 -0
  5. package/index.d.ts +8 -0
  6. package/index.js +2 -0
  7. package/index.js.map +1 -0
  8. package/index.mjs +2 -0
  9. package/index.mjs.map +1 -0
  10. package/isomorphic.d.ts +5 -0
  11. package/isomorphic.js +2 -0
  12. package/isomorphic.js.map +1 -0
  13. package/isomorphic.mjs +2 -0
  14. package/isomorphic.mjs.map +1 -0
  15. package/next-middleware.d.ts +5 -0
  16. package/next-middleware.js +2 -0
  17. package/next-middleware.js.map +1 -0
  18. package/next-middleware.mjs +2 -0
  19. package/next-middleware.mjs.map +1 -0
  20. package/package.json +57 -0
  21. package/react.d.ts +32 -0
  22. package/react.js +2 -0
  23. package/react.js.map +1 -0
  24. package/react.mjs +2 -0
  25. package/react.mjs.map +1 -0
  26. package/src/evaluation-context.d.ts +27 -0
  27. package/src/flagsmith-core.d.ts +18 -0
  28. package/src/flagsmith-core.ts +1092 -0
  29. package/src/index.d.ts +8 -0
  30. package/src/index.ts +22 -0
  31. package/src/isomorphic.d.ts +5 -0
  32. package/src/isomorphic.ts +27 -0
  33. package/src/next-middleware.d.ts +5 -0
  34. package/src/next-middleware.ts +8 -0
  35. package/src/react.d.ts +32 -0
  36. package/src/react.tsx +200 -0
  37. package/src/readme.md +1 -0
  38. package/src/types.d.ts +338 -0
  39. package/src/utils/angular-fetch.ts +36 -0
  40. package/src/utils/async-storage.ts +41 -0
  41. package/src/utils/emitter.ts +90 -0
  42. package/src/utils/ensureTrailingSlash.ts +3 -0
  43. package/src/utils/get-changes.ts +19 -0
  44. package/src/utils/set-dynatrace-value.ts +15 -0
  45. package/src/utils/types.ts +24 -0
  46. package/src/utils/version.ts +2 -0
  47. package/types.d.ts +338 -0
  48. package/utils/angular-fetch.d.ts +6 -0
  49. package/utils/async-storage.d.ts +7 -0
  50. package/utils/emitter.d.ts +11 -0
  51. package/utils/ensureTrailingSlash.d.ts +1 -0
  52. package/utils/get-changes.d.ts +2 -0
  53. package/utils/set-dynatrace-value.d.ts +2 -0
  54. package/utils/types.d.ts +7 -0
  55. package/utils/version.d.ts +1 -0
@@ -0,0 +1,1092 @@
1
+ import {
2
+ ClientEvaluationContext,
3
+ DynatraceObject,
4
+ GetValueOptions,
5
+ HasFeatureOptions,
6
+ IDatadogRum,
7
+ IFlags,
8
+ IFlagsmith,
9
+ IFlagsmithResponse,
10
+ IFlagsmithTrait,
11
+ IInitConfig,
12
+ IPipelineEvent,
13
+ IPipelineEventBatch,
14
+ ISentryClient,
15
+ IState,
16
+ ITraits,
17
+ LoadingState,
18
+ OnChange,
19
+ Traits,
20
+ } from './types';
21
+ // @ts-ignore
22
+ import deepEqual from 'fast-deep-equal';
23
+ import { AsyncStorageType } from './utils/async-storage';
24
+ import getChanges from './utils/get-changes';
25
+ import angularFetch from './utils/angular-fetch';
26
+ import setDynatraceValue from './utils/set-dynatrace-value';
27
+ import { EvaluationContext } from './evaluation-context';
28
+ import { isTraitEvaluationContext, toEvaluationContext, toTraitEvaluationContextObject } from './utils/types';
29
+ import { ensureTrailingSlash } from './utils/ensureTrailingSlash';
30
+ import { SDK_VERSION } from './utils/version';
31
+
32
+ export enum FlagSource {
33
+ "NONE" = "NONE",
34
+ "DEFAULT_FLAGS" = "DEFAULT_FLAGS",
35
+ "CACHE" = "CACHE",
36
+ "SERVER" = "SERVER",
37
+ }
38
+
39
+ export type LikeFetch = (input: Partial<RequestInfo>, init?: Partial<RequestInit>) => Promise<Partial<Response>>
40
+ let _fetch: LikeFetch;
41
+
42
+ type RequestOptions = {
43
+ method: "GET"|"PUT"|"DELETE"|"POST",
44
+ headers: Record<string, string>
45
+ body?: string
46
+ }
47
+
48
+ let AsyncStorage: AsyncStorageType = null;
49
+ const DEFAULT_FLAGSMITH_KEY = "FLAGSMITH_DB";
50
+ const DEFAULT_FLAGSMITH_EVENT = "FLAGSMITH_EVENT";
51
+ let FlagsmithEvent = DEFAULT_FLAGSMITH_EVENT;
52
+ const defaultAPI = 'https://edge.api.flagsmith.com/api/v1/';
53
+ let eventSource: typeof EventSource;
54
+ const initError = function(caller: string) {
55
+ return "Attempted to " + caller + " a user before calling flagsmith.init. Call flagsmith.init first, if you wish to prevent it sending a request for flags, call init with preventFetch:true."
56
+ }
57
+
58
+ type Config = {
59
+ browserlessStorage?: boolean,
60
+ fetch?: LikeFetch,
61
+ AsyncStorage?: AsyncStorageType,
62
+ eventSource?: any,
63
+ applicationMetadata?: IInitConfig['applicationMetadata'],
64
+ };
65
+
66
+ const FLAGSMITH_CONFIG_ANALYTICS_KEY = "flagsmith_value_";
67
+ const FLAGSMITH_FLAG_ANALYTICS_KEY = "flagsmith_enabled_";
68
+ const FLAGSMITH_TRAIT_ANALYTICS_KEY = "flagsmith_trait_";
69
+ const DEFAULT_PIPELINE_FLUSH_INTERVAL = 10000;
70
+
71
+ const Flagsmith = class {
72
+ _trigger?:(()=>void)|null= null
73
+ _triggerLoadingState?:(()=>void)|null= null
74
+ timestamp: number|null = null
75
+ isLoading = false
76
+ eventSource:EventSource|null = null
77
+ applicationMetadata: IInitConfig['applicationMetadata'];
78
+ constructor(props: Config) {
79
+ if (props.fetch) {
80
+ _fetch = props.fetch as LikeFetch;
81
+ } else {
82
+ _fetch = (typeof fetch !== 'undefined' ? fetch : global?.fetch) as LikeFetch;
83
+ }
84
+
85
+ this.canUseStorage = typeof window !== 'undefined' || !!props.browserlessStorage;
86
+ this.applicationMetadata = props.applicationMetadata;
87
+
88
+ this.log("Constructing flagsmith instance " + props)
89
+ if (props.eventSource) {
90
+ eventSource = props.eventSource;
91
+ }
92
+ if (props.AsyncStorage) {
93
+ AsyncStorage = props.AsyncStorage;
94
+ }
95
+ }
96
+
97
+ getFlags = () => {
98
+ const { api, evaluationContext } = this;
99
+ this.log("Get Flags")
100
+ this.isLoading = true;
101
+
102
+ if (!this.loadingState.isFetching) {
103
+ this.setLoadingState({
104
+ ...this.loadingState,
105
+ isFetching: true
106
+ })
107
+ }
108
+ const previousIdentity = `${this.getContext().identity}`;
109
+ const handleResponse = (response: IFlagsmithResponse | null) => {
110
+ if(!response || previousIdentity !== `${this.getContext().identity}`) {
111
+ return // getJSON returned null due to request/response mismatch
112
+ }
113
+ let { flags: features, traits }: IFlagsmithResponse = response
114
+ const {identifier} = response
115
+ this.isLoading = false;
116
+ // Handle server response
117
+ const flags: IFlags = {};
118
+ const userTraits: Traits = {};
119
+ features = features || [];
120
+ traits = traits || [];
121
+ features.forEach(feature => {
122
+ flags[feature.feature.name.toLowerCase().replace(/ /g, '_')] = {
123
+ id: feature.feature.id,
124
+ enabled: feature.enabled,
125
+ value: feature.feature_state_value
126
+ };
127
+ });
128
+ traits.forEach(trait => {
129
+ userTraits[trait.trait_key.toLowerCase().replace(/ /g, '_')] = {
130
+ transient: trait.transient,
131
+ value: trait.trait_value,
132
+ }
133
+ });
134
+
135
+ this.oldFlags = { ...this.flags };
136
+ const flagsChanged = getChanges(this.oldFlags, flags);
137
+ const traitsChanged = getChanges(this.evaluationContext.identity?.traits, userTraits);
138
+ if (identifier || Object.keys(userTraits).length) {
139
+ this.evaluationContext.identity = {
140
+ ...this.evaluationContext.identity,
141
+ traits: userTraits,
142
+ };
143
+ if (identifier) {
144
+ this.evaluationContext.identity.identifier = identifier;
145
+ this.identity = identifier;
146
+ }
147
+ }
148
+ this.flags = flags;
149
+ this.updateStorage();
150
+ this._onChange(this.oldFlags, {
151
+ isFromServer: true,
152
+ flagsChanged,
153
+ traitsChanged
154
+ }, this._loadedState(null, FlagSource.SERVER));
155
+
156
+ if (this.datadogRum) {
157
+ try {
158
+ if (this.datadogRum!.trackTraits) {
159
+ const traits: Parameters<IDatadogRum["client"]["setUser"]>["0"] = {};
160
+ Object.keys(this.evaluationContext.identity?.traits || {}).map((key) => {
161
+ traits[FLAGSMITH_TRAIT_ANALYTICS_KEY + key] = this.getTrait(key);
162
+ });
163
+ const datadogRumData = {
164
+ ...this.datadogRum.client.getUser(),
165
+ id: this.datadogRum.client.getUser().id || this.evaluationContext.identity?.identifier,
166
+ ...traits,
167
+ };
168
+ this.log("Setting Datadog user", datadogRumData);
169
+ this.datadogRum.client.setUser(datadogRumData);
170
+ }
171
+ } catch (e) {
172
+ console.error(e)
173
+ }
174
+ }
175
+ if (this.dtrum) {
176
+ try {
177
+ const traits: DynatraceObject = {
178
+ javaDouble: {},
179
+ date: {},
180
+ shortString: {},
181
+ javaLongOrObject: {},
182
+ }
183
+ Object.keys(this.flags).map((key) => {
184
+ setDynatraceValue(traits, FLAGSMITH_CONFIG_ANALYTICS_KEY + key, this.getValue(key, { skipAnalytics: true }))
185
+ setDynatraceValue(traits, FLAGSMITH_FLAG_ANALYTICS_KEY + key, this.hasFeature(key, { skipAnalytics: true }))
186
+ })
187
+ Object.keys(this.evaluationContext.identity?.traits || {}).map((key) => {
188
+ setDynatraceValue(traits, FLAGSMITH_TRAIT_ANALYTICS_KEY + key, this.getTrait(key))
189
+ })
190
+ this.log("Sending javaLongOrObject traits to dynatrace", traits.javaLongOrObject)
191
+ this.log("Sending date traits to dynatrace", traits.date)
192
+ this.log("Sending shortString traits to dynatrace", traits.shortString)
193
+ this.log("Sending javaDouble to dynatrace", traits.javaDouble)
194
+ // @ts-expect-error
195
+ this.dtrum.sendSessionProperties(
196
+ traits.javaLongOrObject, traits.date, traits.shortString, traits.javaDouble
197
+ )
198
+ } catch (e) {
199
+ console.error(e)
200
+ }
201
+ }
202
+
203
+ };
204
+
205
+ if (evaluationContext.identity) {
206
+ return Promise.all([
207
+ (evaluationContext.identity.traits && Object.keys(evaluationContext.identity.traits).length) || !evaluationContext.identity.identifier ?
208
+ this.getJSON(api + 'identities/', "POST", JSON.stringify({
209
+ "identifier": evaluationContext.identity.identifier,
210
+ "transient": evaluationContext.identity.transient,
211
+ traits: Object.entries(evaluationContext.identity.traits!).map(([tKey, tContext]) => {
212
+ return {
213
+ trait_key: tKey,
214
+ trait_value: tContext?.value,
215
+ transient: tContext?.transient,
216
+ }
217
+ }).filter((v) => {
218
+ if (typeof v.trait_value === 'undefined') {
219
+ this.log("Warning - attempted to set an undefined trait value for key", v.trait_key)
220
+ return false
221
+ }
222
+ return true
223
+ })
224
+ })) :
225
+ this.getJSON(api + 'identities/?identifier=' + encodeURIComponent(evaluationContext.identity.identifier) + (evaluationContext.identity.transient ? '&transient=true' : '')),
226
+ ])
227
+ .then((res) => {
228
+ this.evaluationContext.identity = {...this.evaluationContext.identity, traits: {}}
229
+ return handleResponse(res?.[0] as IFlagsmithResponse | null)
230
+ }).catch(({ message }) => {
231
+ const error = new Error(message)
232
+ return Promise.reject(error)
233
+ });
234
+ } else {
235
+ return this.getJSON(api + "flags/")
236
+ .then((res) => {
237
+ return handleResponse({ flags: res as IFlagsmithResponse['flags'], traits:undefined })
238
+ })
239
+ }
240
+ };
241
+
242
+ analyticsFlags = () => {
243
+ const { api } = this;
244
+
245
+ if (!this.evaluationEvent || !this.evaluationContext.environment || !this.evaluationEvent[this.evaluationContext.environment.apiKey]) {
246
+ return
247
+ }
248
+
249
+ if (this.evaluationEvent && Object.getOwnPropertyNames(this.evaluationEvent).length !== 0 && Object.getOwnPropertyNames(this.evaluationEvent[this.evaluationContext.environment.apiKey]).length !== 0) {
250
+ return this.getJSON(api + 'analytics/flags/', 'POST', JSON.stringify(this.evaluationEvent[this.evaluationContext.environment.apiKey]))
251
+ .then((res) => {
252
+ if (!this.evaluationContext.environment) {
253
+ return;
254
+ }
255
+ const state = this.getState();
256
+ if (!this.evaluationEvent) {
257
+ this.evaluationEvent = {}
258
+ }
259
+ this.evaluationEvent[this.evaluationContext.environment.apiKey] = {}
260
+ this.setState({
261
+ ...state,
262
+ evaluationEvent: this.evaluationEvent,
263
+ });
264
+ this.updateEventStorage();
265
+ }).catch((err) => {
266
+ this.log("Exception fetching evaluationEvent", err);
267
+ });
268
+ }
269
+ };
270
+
271
+ flushPipelineAnalytics = async () => {
272
+ const isEvaluationEnabled = this.evaluationAnalyticsUrl && this.evaluationContext.environment;
273
+ const isReadyToFlush = this.pipelineEvents.length > 0 && (!this.isPipelineFlushing || this.pipelineFlushInterval === 0);
274
+ if (!isEvaluationEnabled || !isReadyToFlush) {
275
+ return;
276
+ }
277
+
278
+ const environmentKey = this.evaluationContext.environment!.apiKey;
279
+ this.isPipelineFlushing = true;
280
+ const eventsToSend = this.pipelineEvents;
281
+ this.pipelineEvents = [];
282
+
283
+ const batch: IPipelineEventBatch = {
284
+ events: eventsToSend,
285
+ environment_key: environmentKey,
286
+ };
287
+
288
+ try {
289
+ const res = await _fetch(this.evaluationAnalyticsUrl + 'v1/analytics/batch', {
290
+ method: 'POST',
291
+ body: JSON.stringify(batch),
292
+ headers: {
293
+ 'Content-Type': 'application/json; charset=utf-8',
294
+ 'X-Environment-Key': environmentKey,
295
+ ...(SDK_VERSION ? { 'Flagsmith-SDK-User-Agent': `flagsmith-js-sdk/${SDK_VERSION}` } : {}),
296
+ },
297
+ });
298
+ if (!res.status || res.status < 200 || res.status >= 300) {
299
+ throw new Error(`Pipeline analytics: unexpected status ${res.status}`);
300
+ }
301
+ this.log('Pipeline analytics: flush successful');
302
+ } catch (err) {
303
+ this.pipelineEvents = eventsToSend.concat(this.pipelineEvents);
304
+ this.trimPipelineBuffer();
305
+ this.log('Pipeline analytics: flush failed, events re-queued', err);
306
+ } finally {
307
+ this.isPipelineFlushing = false;
308
+ }
309
+ };
310
+
311
+ datadogRum: IDatadogRum | null = null;
312
+ loadingState: LoadingState = {isLoading: true, isFetching: true, error: null, source: FlagSource.NONE}
313
+ canUseStorage = false
314
+ analyticsInterval: NodeJS.Timer | null= null
315
+ api: string|null= null
316
+ cacheFlags= false
317
+ ts?: number
318
+ enableAnalytics= false
319
+ enableLogs= false
320
+ evaluationContext: EvaluationContext= {}
321
+ evaluationEvent: Record<string, Record<string, number>> | null= null
322
+ flags:IFlags|null= null
323
+ getFlagInterval: NodeJS.Timer|null= null
324
+ headers?: object | null= null
325
+ identity:string|null|undefined = null
326
+ initialised= false
327
+ oldFlags:IFlags|null= null
328
+ onChange:IInitConfig['onChange']|null= null
329
+ onError:IInitConfig['onError']|null = null
330
+ ticks: number|null= null
331
+ timer: number|null= null
332
+ dtrum= null
333
+ sentryClient: ISentryClient | null = null
334
+ withTraits?: ITraits|null= null
335
+ cacheOptions = {ttl:0, skipAPI: false, loadStale: false, storageKey: undefined as string|undefined}
336
+ evaluationAnalyticsUrl: string | null = null
337
+ evaluationAnalyticsMaxBuffer: number = 1000
338
+ pipelineEvents: IPipelineEvent[] = []
339
+ pipelineAnalyticsInterval: ReturnType<typeof setInterval> | null = null
340
+ isPipelineFlushing = false
341
+ async init(config: IInitConfig) {
342
+ const evaluationContext = toEvaluationContext(config.evaluationContext || this.evaluationContext);
343
+ try {
344
+ const {
345
+ AsyncStorage: _AsyncStorage,
346
+ _trigger,
347
+ _triggerLoadingState,
348
+ angularHttpClient,
349
+ api = defaultAPI,
350
+ applicationMetadata,
351
+ cacheFlags,
352
+ cacheOptions,
353
+ datadogRum,
354
+ defaultFlags,
355
+ enableAnalytics,
356
+ enableDynatrace,
357
+ enableLogs,
358
+ environmentID,
359
+ evaluationAnalyticsConfig,
360
+ eventSourceUrl= "https://realtime.flagsmith.com/",
361
+ fetch: fetchImplementation,
362
+ headers,
363
+ identity,
364
+ onChange,
365
+ onError,
366
+ preventFetch,
367
+ realtime,
368
+ sentryClient,
369
+ state,
370
+ traits,
371
+ } = config;
372
+ evaluationContext.environment = environmentID ? {apiKey: environmentID} : evaluationContext.environment;
373
+ if (!evaluationContext.environment || !evaluationContext.environment.apiKey) {
374
+ throw new Error('Please provide `evaluationContext.environment` with non-empty `apiKey`');
375
+ }
376
+ evaluationContext.identity = identity || traits ? {
377
+ identifier: identity,
378
+ traits: traits ? Object.fromEntries(
379
+ Object.entries(traits).map(
380
+ ([tKey, tValue]) => [tKey, {value: tValue}]
381
+ )
382
+ ) : {},
383
+ } : evaluationContext.identity;
384
+ this.evaluationContext = evaluationContext;
385
+ this.api = ensureTrailingSlash(api);
386
+ this.headers = headers;
387
+ this.getFlagInterval = null;
388
+ this.analyticsInterval = null;
389
+ this.onChange = onChange;
390
+ const WRONG_FLAGSMITH_CONFIG = 'Wrong Flagsmith Configuration: preventFetch is true and no defaulFlags provided'
391
+ this._trigger = _trigger || this._trigger;
392
+ this._triggerLoadingState = _triggerLoadingState || this._triggerLoadingState;
393
+ this.onError = (message: Error) => {
394
+ this.setLoadingState({
395
+ ...this.loadingState,
396
+ isFetching: false,
397
+ isLoading: false,
398
+ error: message,
399
+ });
400
+ onError?.(message);
401
+ };
402
+ this.enableLogs = enableLogs || false;
403
+ this.cacheOptions = cacheOptions ? { skipAPI: !!cacheOptions.skipAPI, ttl: cacheOptions.ttl || 0, storageKey:cacheOptions.storageKey, loadStale: !!cacheOptions.loadStale } : this.cacheOptions;
404
+ if (!this.cacheOptions.ttl && this.cacheOptions.skipAPI) {
405
+ console.warn("Flagsmith: you have set a cache ttl of 0 and are skipping API calls, this means the API will not be hit unless you clear local storage.")
406
+ }
407
+ if (fetchImplementation) {
408
+ _fetch = fetchImplementation;
409
+ }
410
+ this.enableAnalytics = enableAnalytics ? enableAnalytics : false;
411
+ this.flags = Object.assign({}, defaultFlags) || {};
412
+ this.datadogRum = datadogRum || null;
413
+ this.initialised = true;
414
+ this.ticks = 10000;
415
+ this.timer = this.enableLogs ? new Date().valueOf() : null;
416
+ this.cacheFlags = typeof AsyncStorage !== 'undefined' && !!cacheFlags;
417
+ this.applicationMetadata = applicationMetadata;
418
+
419
+ FlagsmithEvent = DEFAULT_FLAGSMITH_EVENT + "_" + evaluationContext.environment.apiKey;
420
+
421
+ if (_AsyncStorage) {
422
+ AsyncStorage = _AsyncStorage;
423
+ }
424
+ if (realtime && typeof window !== 'undefined') {
425
+ this.setupRealtime(eventSourceUrl, evaluationContext.environment.apiKey);
426
+ }
427
+
428
+ if (Object.keys(this.flags).length) {
429
+ //Flags have been passed as part of SSR / default flags, update state silently for initial render
430
+ this.loadingState = {
431
+ ...this.loadingState,
432
+ isLoading: false,
433
+ source: FlagSource.DEFAULT_FLAGS
434
+ }
435
+ }
436
+
437
+ this.setState(state as IState);
438
+
439
+ this.log('Initialising with properties', config, this);
440
+
441
+ if (enableDynatrace) {
442
+ // @ts-expect-error Dynatrace's dtrum is exposed to global scope
443
+ if (typeof dtrum === 'undefined') {
444
+ console.error("You have attempted to enable dynatrace but dtrum is undefined, please check you have the Dynatrace RUM JavaScript API installed.")
445
+ } else {
446
+ // @ts-expect-error Dynatrace's dtrum is exposed to global scope
447
+ this.dtrum = dtrum;
448
+ }
449
+ }
450
+
451
+ if(sentryClient) {
452
+ this.sentryClient = sentryClient
453
+ }
454
+ if (angularHttpClient) {
455
+ // @ts-expect-error
456
+ _fetch = angularFetch(angularHttpClient);
457
+ }
458
+
459
+ if (AsyncStorage && this.canUseStorage) {
460
+ AsyncStorage.getItem(FlagsmithEvent)
461
+ .then((res)=>{
462
+ try {
463
+ this.evaluationEvent = JSON.parse(res!) || {}
464
+ } catch (e) {
465
+ this.evaluationEvent = {};
466
+ }
467
+ this.analyticsInterval = setInterval(this.analyticsFlags, this.ticks!);
468
+ })
469
+ }
470
+
471
+ if (this.enableAnalytics) {
472
+ if (this.analyticsInterval) {
473
+ clearInterval(this.analyticsInterval);
474
+ }
475
+
476
+ if (AsyncStorage && this.canUseStorage) {
477
+ AsyncStorage.getItem(FlagsmithEvent, (err, res) => {
478
+ if (res && this.evaluationContext.environment) {
479
+ const json = JSON.parse(res);
480
+ if (json[this.evaluationContext.environment.apiKey]) {
481
+ const state = this.getState();
482
+ this.log("Retrieved events from cache", res);
483
+ this.setState({
484
+ ...state,
485
+ evaluationEvent: json[this.evaluationContext.environment.apiKey],
486
+ });
487
+ }
488
+ }
489
+ });
490
+ }
491
+ }
492
+
493
+ if (evaluationAnalyticsConfig) {
494
+ this.initPipelineAnalytics(evaluationAnalyticsConfig);
495
+ } else {
496
+ this.stopPipelineAnalytics();
497
+ }
498
+
499
+ //If the user specified default flags emit a changed event immediately
500
+ if (cacheFlags) {
501
+ if (AsyncStorage && this.canUseStorage) {
502
+ const onRetrievedStorage = async (error: Error | null, res: string | null) => {
503
+ if (res) {
504
+ let flagsChanged = null
505
+ const traitsChanged = null
506
+ try {
507
+ const json = JSON.parse(res) as IState;
508
+ let cachePopulated = false;
509
+ let staleCachePopulated = false;
510
+ if (json && json.api === this.api && json.evaluationContext?.environment?.apiKey === this.evaluationContext.environment?.apiKey) {
511
+ let setState = true;
512
+ if (this.evaluationContext.identity && (json.evaluationContext?.identity?.identifier !== this.evaluationContext.identity.identifier)) {
513
+ this.log("Ignoring cache, identity has changed from " + json.evaluationContext?.identity?.identifier + " to " + this.evaluationContext.identity.identifier )
514
+ setState = false;
515
+ }
516
+ if (this.cacheOptions.ttl) {
517
+ if (!json.ts || (new Date().valueOf() - json.ts > this.cacheOptions.ttl)) {
518
+ if (json.ts && !this.cacheOptions.loadStale) {
519
+ this.log("Ignoring cache, timestamp is too old ts:" + json.ts + " ttl: " + this.cacheOptions.ttl + " time elapsed since cache: " + (new Date().valueOf()-json.ts)+"ms")
520
+ setState = false;
521
+ }
522
+ else if (json.ts && this.cacheOptions.loadStale) {
523
+ this.log("Loading stale cache, timestamp ts:" + json.ts + " ttl: " + this.cacheOptions.ttl + " time elapsed since cache: " + (new Date().valueOf()-json.ts)+"ms")
524
+ staleCachePopulated = true;
525
+ setState = true;
526
+ }
527
+ }
528
+ }
529
+ if (setState) {
530
+ cachePopulated = true;
531
+ flagsChanged = getChanges(this.flags, json.flags)
532
+ this.setState({
533
+ ...json,
534
+ evaluationContext: toEvaluationContext({
535
+ ...json.evaluationContext,
536
+ identity: json.evaluationContext?.identity ? {
537
+ ...json.evaluationContext?.identity,
538
+ traits: {
539
+ // Traits passed in flagsmith.init will overwrite server values
540
+ ...traits || {},
541
+ }
542
+ } : undefined,
543
+ })
544
+ });
545
+ this.log("Retrieved flags from cache", json);
546
+ }
547
+ }
548
+
549
+ if (cachePopulated) { // retrieved flags from local storage
550
+ // fetch the flags if the cache is stale, or if we're not skipping api on cache hits
551
+ const shouldFetchFlags = !preventFetch && (!this.cacheOptions.skipAPI || staleCachePopulated)
552
+ this._onChange(null,
553
+ { isFromServer: false, flagsChanged, traitsChanged },
554
+ this._loadedState(null, FlagSource.CACHE, shouldFetchFlags)
555
+ );
556
+ this.oldFlags = this.flags;
557
+ if (this.cacheOptions.skipAPI && cachePopulated && !staleCachePopulated) {
558
+ this.log("Skipping API, using cache")
559
+ }
560
+ if (shouldFetchFlags) {
561
+ // We want to resolve init since we have cached flags
562
+
563
+ this.getFlags().catch((error) => {
564
+ this.onError?.(error)
565
+ })
566
+ }
567
+ } else {
568
+ if (!preventFetch) {
569
+ await this.getFlags();
570
+ }
571
+ }
572
+ } catch (e) {
573
+ this.log("Exception fetching cached logs", e);
574
+ throw e;
575
+ }
576
+ } else {
577
+ if (!preventFetch) {
578
+ await this.getFlags();
579
+ } else {
580
+ if (defaultFlags) {
581
+ this._onChange(null,
582
+ { isFromServer: false, flagsChanged: getChanges({}, this.flags), traitsChanged: getChanges({}, this.evaluationContext.identity?.traits) },
583
+ this._loadedState(null, FlagSource.DEFAULT_FLAGS),
584
+ );
585
+ } else if (this.flags) { // flags exist due to set state being called e.g. from nextJS serverState
586
+ this._onChange(null,
587
+ { isFromServer: false, flagsChanged: getChanges({}, this.flags), traitsChanged: getChanges({}, this.evaluationContext.identity?.traits) },
588
+ this._loadedState(null, FlagSource.DEFAULT_FLAGS),
589
+ );
590
+ } else {
591
+ throw new Error(WRONG_FLAGSMITH_CONFIG);
592
+ }
593
+ }
594
+ }
595
+ };
596
+ try {
597
+ const res = AsyncStorage.getItemSync? AsyncStorage.getItemSync(this.getStorageKey()) : await AsyncStorage.getItem(this.getStorageKey());
598
+ await onRetrievedStorage(null, res)
599
+ } catch (e) {
600
+ // Only re-throw if we don't have fallback flags (defaultFlags or cached flags)
601
+ if (!this.flags || Object.keys(this.flags).length === 0) {
602
+ throw e;
603
+ }
604
+ // We have fallback flags, so call onError but don't reject init()
605
+ const typedError = e instanceof Error ? e : new Error(`${e}`);
606
+ this.onError?.(typedError);
607
+ }
608
+ }
609
+ } else if (!preventFetch) {
610
+ await this.getFlags();
611
+ } else {
612
+ if (defaultFlags) {
613
+ this._onChange(null, { isFromServer: false, flagsChanged: getChanges({}, defaultFlags), traitsChanged: getChanges({}, evaluationContext.identity?.traits) }, this._loadedState(null, FlagSource.DEFAULT_FLAGS));
614
+ } else if (this.flags) {
615
+ let error = null;
616
+ if (Object.keys(this.flags).length === 0) {
617
+ error = WRONG_FLAGSMITH_CONFIG;
618
+ }
619
+ this._onChange(null, { isFromServer: false, flagsChanged: getChanges({}, this.flags), traitsChanged: getChanges({}, evaluationContext.identity?.traits) }, this._loadedState(error, FlagSource.DEFAULT_FLAGS));
620
+ if(error) {
621
+ throw new Error(error)
622
+ }
623
+ }
624
+ }
625
+ } catch (error) {
626
+ this.log('Error during initialisation ', error);
627
+ const typedError = error instanceof Error ? error : new Error(`${error}`);
628
+ this.onError?.(typedError);
629
+ throw error;
630
+ }
631
+ }
632
+
633
+ getAllFlags() {
634
+ return this.flags;
635
+ }
636
+
637
+ identify(userId?: string | null, traits?: ITraits, transient?: boolean) {
638
+ this.identity = userId
639
+ this.evaluationContext.identity = {
640
+ identifier: userId,
641
+ transient: transient,
642
+ // clear out old traits when switching identity
643
+ traits: this.evaluationContext.identity && this.evaluationContext.identity.identifier == userId ? this.evaluationContext.identity.traits : {}
644
+ }
645
+ this.evaluationContext.identity.identifier = userId;
646
+ this.log("Identify: " + this.evaluationContext.identity.identifier)
647
+
648
+ if (traits) {
649
+ this.evaluationContext.identity.traits = Object.fromEntries(
650
+ Object.entries(traits).map(
651
+ ([tKey, tValue]) => [tKey, isTraitEvaluationContext(tValue) ? tValue : {value: tValue}]
652
+ )
653
+ );
654
+ }
655
+ if (this.initialised) {
656
+ return this.getFlags();
657
+ }
658
+ return Promise.resolve();
659
+ }
660
+
661
+ getState() {
662
+ return {
663
+ api: this.api,
664
+ flags: this.flags,
665
+ ts: this.ts,
666
+ evaluationContext: this.evaluationContext,
667
+ identity: this.identity,
668
+ evaluationEvent: this.evaluationEvent,
669
+ } as IState
670
+ }
671
+
672
+ setState(state: IState) {
673
+ if (state) {
674
+ this.initialised = true;
675
+ this.api = state.api || this.api || defaultAPI;
676
+ this.flags = state.flags || this.flags;
677
+ this.evaluationContext = state.evaluationContext || this.evaluationContext,
678
+ this.evaluationEvent = state.evaluationEvent || this.evaluationEvent;
679
+ this.identity = this.getContext()?.identity?.identifier
680
+ this.log("setState called", this)
681
+ }
682
+ }
683
+
684
+ logout() {
685
+ this.identity = null
686
+ this.evaluationContext.identity = null;
687
+ if (this.initialised) {
688
+ return this.getFlags();
689
+ }
690
+ return Promise.resolve();
691
+ }
692
+
693
+ startListening(ticks = 1000) {
694
+ if (this.getFlagInterval) {
695
+ clearInterval(this.getFlagInterval);
696
+ }
697
+ this.getFlagInterval = setInterval(this.getFlags, ticks);
698
+ }
699
+
700
+ stopListening() {
701
+ if (this.getFlagInterval) {
702
+ clearInterval(this.getFlagInterval);
703
+ this.getFlagInterval = null;
704
+ }
705
+ }
706
+
707
+ getValue = (key: string, options?: GetValueOptions, skipAnalytics?: boolean) => {
708
+ const flag = this.flags && this.flags[key.toLowerCase().replace(/ /g, '_')];
709
+ let res = null;
710
+ if (flag) {
711
+ res = flag.value;
712
+ }
713
+
714
+ if (!options?.skipAnalytics && !skipAnalytics) {
715
+ this.evaluateFlag(key, "VALUE");
716
+ }
717
+
718
+ if (res === null && typeof options?.fallback !== 'undefined') {
719
+ return options.fallback;
720
+ }
721
+
722
+ if (options?.json) {
723
+ try {
724
+ if (res === null) {
725
+ this.log("Tried to parse null flag as JSON: " + key);
726
+ return null;
727
+ }
728
+ return JSON.parse(res as string);
729
+ } catch (e) {
730
+ return options.fallback;
731
+ }
732
+ }
733
+ //todo record check for value
734
+ return res;
735
+ }
736
+
737
+ getTrait = (key: string) => {
738
+ return this.evaluationContext.identity?.traits && this.evaluationContext.identity.traits[key.toLowerCase().replace(/ /g, '_')]?.value;
739
+ }
740
+
741
+ getAllTraits = () => {
742
+ return Object.fromEntries(
743
+ Object.entries(this.evaluationContext.identity?.traits || {}).map(
744
+ ([tKey, tContext]) => [tKey, tContext?.value]
745
+ )
746
+ );
747
+ }
748
+
749
+ setContext = (clientEvaluationContext: ClientEvaluationContext) => {
750
+ const evaluationContext = toEvaluationContext(clientEvaluationContext);
751
+ this.evaluationContext = {
752
+ ...evaluationContext,
753
+ environment: evaluationContext.environment || this.evaluationContext.environment,
754
+ };
755
+ this.identity = this.getContext()?.identity?.identifier
756
+
757
+ if (this.initialised) {
758
+ return this.getFlags();
759
+ }
760
+
761
+ return Promise.resolve();
762
+ }
763
+
764
+ getContext = () => {
765
+ return this.evaluationContext;
766
+ }
767
+
768
+ updateContext = (evaluationContext: ClientEvaluationContext) => {
769
+ return this.setContext({
770
+ ...this.getContext(),
771
+ ...evaluationContext,
772
+ })
773
+ }
774
+
775
+ setTrait = (key: string, trait_value: IFlagsmithTrait) => {
776
+ const { api } = this;
777
+
778
+ if (!api) {
779
+ return
780
+ }
781
+
782
+ return this.setContext({
783
+ ...this.evaluationContext,
784
+ identity: {
785
+ ...this.evaluationContext.identity,
786
+ traits: {
787
+ ...this.evaluationContext.identity?.traits,
788
+ ...toTraitEvaluationContextObject(Object.fromEntries(
789
+ [[key, trait_value]],
790
+ ))
791
+ }
792
+ }
793
+ });
794
+ };
795
+
796
+ setTraits = (traits: ITraits) => {
797
+
798
+ if (!this.api) {
799
+ console.error(initError("setTraits"))
800
+ return
801
+ }
802
+
803
+ return this.setContext({
804
+ ...this.evaluationContext,
805
+ identity: {
806
+ ...this.evaluationContext.identity,
807
+ traits: {
808
+ ...this.evaluationContext.identity?.traits,
809
+ ...Object.fromEntries(
810
+ Object.entries(traits).map(
811
+ (([tKey, tValue]) => [tKey, isTraitEvaluationContext(tValue) ? tValue : {value: tValue}])
812
+ )
813
+ )
814
+ }
815
+ }
816
+ });
817
+ };
818
+
819
+ hasFeature = (key: string, options?: HasFeatureOptions) => {
820
+ // Support legacy skipAnalytics boolean parameter
821
+ const usingNewOptions = typeof options === 'object'
822
+ const flag = this.flags && this.flags[key.toLowerCase().replace(/ /g, '_')];
823
+ let res = false;
824
+ if (!flag && usingNewOptions && typeof options.fallback !== 'undefined') {
825
+ res = options?.fallback
826
+ } else if (flag && flag.enabled) {
827
+ res = true;
828
+ }
829
+ if ((usingNewOptions && !options.skipAnalytics) || !options) {
830
+ this.evaluateFlag(key, "ENABLED");
831
+ }
832
+ if(this.sentryClient) {
833
+ try {
834
+ this.sentryClient.getIntegrationByName(
835
+ "FeatureFlags",
836
+ )?.addFeatureFlag?.(key, res);
837
+ } catch (e) {
838
+ console.error(e)
839
+ }
840
+ }
841
+
842
+ return res;
843
+ };
844
+
845
+ private _loadedState(error: any = null, source: FlagSource, isFetching = false) {
846
+ return {
847
+ error,
848
+ isFetching,
849
+ isLoading: false,
850
+ source
851
+ }
852
+ }
853
+
854
+ private getStorageKey = ()=> {
855
+ return this.cacheOptions?.storageKey || DEFAULT_FLAGSMITH_KEY + "_" + this.evaluationContext.environment?.apiKey
856
+ }
857
+
858
+ private log(...args: (unknown)[]) {
859
+ if (this.enableLogs) {
860
+ console.log.apply(this, ['FLAGSMITH:', new Date().valueOf() - (this.timer || 0), 'ms', ...args]);
861
+ }
862
+ }
863
+
864
+ private updateStorage() {
865
+ if (this.cacheFlags) {
866
+ this.ts = new Date().valueOf();
867
+ const state = JSON.stringify(this.getState());
868
+ this.log('Setting storage', state);
869
+ AsyncStorage!.setItem(this.getStorageKey(), state);
870
+ }
871
+ }
872
+
873
+ private getJSON = (url: string, method?: 'GET' | 'POST' | 'PUT', body?: string) => {
874
+ const { headers } = this;
875
+ const options: RequestOptions = {
876
+ method: method || 'GET',
877
+ body,
878
+ // @ts-ignore next-js overrides fetch
879
+ cache: 'no-cache',
880
+ headers: {},
881
+ };
882
+ if (this.evaluationContext.environment)
883
+ options.headers['X-Environment-Key'] = this.evaluationContext.environment.apiKey;
884
+ if (method && method !== 'GET')
885
+ options.headers['Content-Type'] = 'application/json; charset=utf-8';
886
+
887
+
888
+ if (this.applicationMetadata?.name) {
889
+ options.headers['Flagsmith-Application-Name'] = this.applicationMetadata.name;
890
+ }
891
+
892
+ if (this.applicationMetadata?.version) {
893
+ options.headers['Flagsmith-Application-Version'] = this.applicationMetadata.version;
894
+ }
895
+
896
+ if (SDK_VERSION) {
897
+ options.headers['Flagsmith-SDK-User-Agent'] = `flagsmith-js-sdk/${SDK_VERSION}`
898
+ }
899
+
900
+ if (headers) {
901
+ Object.assign(options.headers, headers);
902
+ }
903
+
904
+ if (!_fetch) {
905
+ console.error('Flagsmith: fetch is undefined, please specify a fetch implementation into flagsmith.init to support SSR.');
906
+ }
907
+
908
+ const requestedIdentity = `${this.evaluationContext.identity?.identifier}`;
909
+ return _fetch(url, options)
910
+ .then(res => {
911
+ const newIdentity = `${this.evaluationContext.identity?.identifier}`;
912
+ if (requestedIdentity !== newIdentity) {
913
+ this.log(`Received response with identity mismatch, ignoring response. Requested: ${requestedIdentity}, Current: ${newIdentity}`);
914
+ return;
915
+ }
916
+ const lastUpdated = res.headers?.get('x-flagsmith-document-updated-at');
917
+ if (lastUpdated) {
918
+ try {
919
+ const lastUpdatedFloat = parseFloat(lastUpdated);
920
+ if (isNaN(lastUpdatedFloat)) {
921
+ return Promise.reject('Failed to parse x-flagsmith-document-updated-at');
922
+ }
923
+ this.timestamp = lastUpdatedFloat;
924
+ } catch (e) {
925
+ this.log(e, 'Failed to parse x-flagsmith-document-updated-at', lastUpdated);
926
+ }
927
+ }
928
+ this.log('Fetch response: ' + res.status + ' ' + (method || 'GET') + +' ' + url);
929
+ return res.text!()
930
+ .then((text) => {
931
+ let err = text;
932
+ try {
933
+ err = JSON.parse(text);
934
+ } catch (e) {}
935
+ if(!err && res.status) {
936
+ err = `API Response: ${res.status}`
937
+ }
938
+ return res.status && res.status >= 200 && res.status < 300 ? err : Promise.reject(new Error(err));
939
+ });
940
+ });
941
+ };
942
+
943
+ private updateEventStorage() {
944
+ if (this.enableAnalytics) {
945
+ const events = JSON.stringify(this.getState().evaluationEvent);
946
+ AsyncStorage!.setItem(FlagsmithEvent, events)
947
+ .catch((e) => console.error("Flagsmith: Error setting item in async storage", e));
948
+ }
949
+ }
950
+
951
+ private evaluateFlag =(key: string, method: 'VALUE' | 'ENABLED') => {
952
+ if (this.datadogRum) {
953
+ if (!this.datadogRum!.client!.addFeatureFlagEvaluation) {
954
+ console.error('Flagsmith: Your datadog RUM client does not support the function addFeatureFlagEvaluation, please update it.');
955
+ } else {
956
+ if (method === 'VALUE') {
957
+ this.datadogRum!.client!.addFeatureFlagEvaluation(FLAGSMITH_CONFIG_ANALYTICS_KEY + key, this.getValue(key, {}, true));
958
+ } else {
959
+ this.datadogRum!.client!.addFeatureFlagEvaluation(FLAGSMITH_FLAG_ANALYTICS_KEY + key, this.hasFeature(key, true));
960
+ }
961
+ }
962
+ }
963
+
964
+ if (this.enableAnalytics) {
965
+ if (!this.evaluationEvent || !this.evaluationContext.environment) return;
966
+ if (!this.evaluationEvent[this.evaluationContext.environment.apiKey]) {
967
+ this.evaluationEvent[this.evaluationContext.environment.apiKey] = {};
968
+ }
969
+ if (this.evaluationEvent[this.evaluationContext.environment.apiKey][key] === undefined) {
970
+ this.evaluationEvent[this.evaluationContext.environment.apiKey][key] = 0;
971
+ }
972
+ this.evaluationEvent[this.evaluationContext.environment.apiKey][key] += 1;
973
+ }
974
+
975
+ if (this.evaluationAnalyticsUrl) {
976
+ this.recordPipelineEvent(key);
977
+ }
978
+
979
+ this.updateEventStorage();
980
+ };
981
+
982
+ private pipelineFlushInterval: number = DEFAULT_PIPELINE_FLUSH_INTERVAL;
983
+
984
+ private initPipelineAnalytics(config: NonNullable<IInitConfig['evaluationAnalyticsConfig']>) {
985
+ this.stopPipelineAnalytics();
986
+ this.evaluationAnalyticsUrl = ensureTrailingSlash(config.analyticsServerUrl);
987
+ this.evaluationAnalyticsMaxBuffer = config.maxBuffer ?? 1000;
988
+ this.pipelineFlushInterval = config.flushInterval ?? DEFAULT_PIPELINE_FLUSH_INTERVAL;
989
+ this.pipelineEvents = [];
990
+ if (this.pipelineFlushInterval > 0) {
991
+ this.pipelineAnalyticsInterval = setInterval(
992
+ this.flushPipelineAnalytics,
993
+ this.pipelineFlushInterval,
994
+ );
995
+ this.pipelineAnalyticsInterval?.unref?.();
996
+ }
997
+ }
998
+
999
+ private stopPipelineAnalytics() {
1000
+ if (this.pipelineAnalyticsInterval) {
1001
+ clearInterval(this.pipelineAnalyticsInterval);
1002
+ this.pipelineAnalyticsInterval = null;
1003
+ }
1004
+ this.evaluationAnalyticsUrl = null;
1005
+ this.pipelineEvents = [];
1006
+ }
1007
+
1008
+ private trimPipelineBuffer() {
1009
+ if (this.pipelineEvents.length > this.evaluationAnalyticsMaxBuffer) {
1010
+ const excess = this.pipelineEvents.length - this.evaluationAnalyticsMaxBuffer;
1011
+ this.pipelineEvents = this.pipelineEvents.slice(excess);
1012
+ }
1013
+ }
1014
+
1015
+ // Pipeline event schema — must match the pipeline server's Event struct.
1016
+ // To update: 1) IPipelineEvent in types.d.ts 2) event object below 3) tests in test/analytics-pipeline.test.ts
1017
+ private recordPipelineEvent(key: string) {
1018
+ const flagKey = key.toLowerCase().replace(/ /g, '_');
1019
+ const flag = this.flags && this.flags[flagKey];
1020
+ const event: IPipelineEvent = {
1021
+ event_id: flagKey,
1022
+ event_type: 'flag_evaluation',
1023
+ evaluated_at: Date.now(),
1024
+ identity_identifier: this.evaluationContext.identity?.identifier ?? null,
1025
+ enabled: flag ? flag.enabled : null,
1026
+ value: flag ? flag.value : null,
1027
+ traits: this.evaluationContext.identity?.traits
1028
+ ? { ...this.evaluationContext.identity.traits }
1029
+ : null,
1030
+ metadata: {
1031
+ ...(flag ? { id: flag.id } : {}),
1032
+ ...(typeof window !== 'undefined' && window.location ? { page_url: window.location.href } : {}),
1033
+ ...(SDK_VERSION ? { sdk_version: SDK_VERSION } : {}),
1034
+ },
1035
+ };
1036
+ this.pipelineEvents.push(event);
1037
+ this.trimPipelineBuffer();
1038
+
1039
+ if (this.pipelineFlushInterval === 0) {
1040
+ this.flushPipelineAnalytics();
1041
+ }
1042
+ }
1043
+
1044
+ private setLoadingState(loadingState: LoadingState) {
1045
+ if (!deepEqual(loadingState, this.loadingState)) {
1046
+ this.loadingState = { ...loadingState };
1047
+ this.log('Loading state changed', loadingState);
1048
+ this._triggerLoadingState?.();
1049
+ }
1050
+ }
1051
+
1052
+ private _onChange: OnChange = (previousFlags, params, loadingState) => {
1053
+ this.setLoadingState(loadingState);
1054
+ this.onChange?.(previousFlags, params, this.loadingState);
1055
+ this._trigger?.();
1056
+ };
1057
+
1058
+ private setupRealtime(eventSourceUrl: string, environmentID: string) {
1059
+ const connectionUrl = eventSourceUrl + 'sse/environments/' + environmentID + '/stream';
1060
+ if (!eventSource) {
1061
+ this.log('Error, EventSource is undefined');
1062
+ } else if (!this.eventSource) {
1063
+ this.log('Creating event source with url ' + connectionUrl);
1064
+ this.eventSource = new eventSource(connectionUrl);
1065
+ this.eventSource.addEventListener('environment_updated', (e) => {
1066
+ let updated_at;
1067
+ try {
1068
+ const data = JSON.parse(e.data);
1069
+ updated_at = data.updated_at;
1070
+ } catch (e) {
1071
+ this.log('Could not parse sse event', e);
1072
+ }
1073
+ if (!updated_at) {
1074
+ this.log('No updated_at received, fetching flags', e);
1075
+ } else if (!this.timestamp || updated_at > this.timestamp) {
1076
+ if (this.isLoading) {
1077
+ this.log('updated_at is new, but flags are loading', e.data, this.timestamp);
1078
+ } else {
1079
+ this.log('updated_at is new, fetching flags', e.data, this.timestamp);
1080
+ this.getFlags();
1081
+ }
1082
+ } else {
1083
+ this.log('updated_at is outdated, skipping get flags', e.data, this.timestamp);
1084
+ }
1085
+ });
1086
+ }
1087
+ }
1088
+ };
1089
+
1090
+ export default function({ fetch, AsyncStorage, eventSource }: Config): IFlagsmith {
1091
+ return new Flagsmith({ fetch, AsyncStorage, eventSource }) as IFlagsmith;
1092
+ }