@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.
- package/README.md +38 -0
- package/evaluation-context.d.ts +27 -0
- package/evaluation-context.ts +29 -0
- package/flagsmith-core.d.ts +18 -0
- package/index.d.ts +8 -0
- package/index.js +2 -0
- package/index.js.map +1 -0
- package/index.mjs +2 -0
- package/index.mjs.map +1 -0
- package/isomorphic.d.ts +5 -0
- package/isomorphic.js +2 -0
- package/isomorphic.js.map +1 -0
- package/isomorphic.mjs +2 -0
- package/isomorphic.mjs.map +1 -0
- package/next-middleware.d.ts +5 -0
- package/next-middleware.js +2 -0
- package/next-middleware.js.map +1 -0
- package/next-middleware.mjs +2 -0
- package/next-middleware.mjs.map +1 -0
- package/package.json +57 -0
- package/react.d.ts +32 -0
- package/react.js +2 -0
- package/react.js.map +1 -0
- package/react.mjs +2 -0
- package/react.mjs.map +1 -0
- package/src/evaluation-context.d.ts +27 -0
- package/src/flagsmith-core.d.ts +18 -0
- package/src/flagsmith-core.ts +1092 -0
- package/src/index.d.ts +8 -0
- package/src/index.ts +22 -0
- package/src/isomorphic.d.ts +5 -0
- package/src/isomorphic.ts +27 -0
- package/src/next-middleware.d.ts +5 -0
- package/src/next-middleware.ts +8 -0
- package/src/react.d.ts +32 -0
- package/src/react.tsx +200 -0
- package/src/readme.md +1 -0
- package/src/types.d.ts +338 -0
- package/src/utils/angular-fetch.ts +36 -0
- package/src/utils/async-storage.ts +41 -0
- package/src/utils/emitter.ts +90 -0
- package/src/utils/ensureTrailingSlash.ts +3 -0
- package/src/utils/get-changes.ts +19 -0
- package/src/utils/set-dynatrace-value.ts +15 -0
- package/src/utils/types.ts +24 -0
- package/src/utils/version.ts +2 -0
- package/types.d.ts +338 -0
- package/utils/angular-fetch.d.ts +6 -0
- package/utils/async-storage.d.ts +7 -0
- package/utils/emitter.d.ts +11 -0
- package/utils/ensureTrailingSlash.d.ts +1 -0
- package/utils/get-changes.d.ts +2 -0
- package/utils/set-dynatrace-value.d.ts +2 -0
- package/utils/types.d.ts +7 -0
- 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
|
+
}
|