react-native-featureflags 0.3.0 → 1.0.0
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/dist/client.d.ts +1 -83
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -858
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/core.d.ts +1 -90
- package/dist/internal/core.d.ts.map +1 -1
- package/dist/internal/core.js +1 -190
- package/dist/internal/core.js.map +1 -1
- package/dist/singleton.d.ts +1 -1
- package/dist/singleton.d.ts.map +1 -1
- package/dist/singleton.js +1 -1
- package/dist/singleton.js.map +1 -1
- package/dist/storage.d.ts +1 -15
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1 -20
- package/dist/storage.js.map +1 -1
- package/dist/store.d.ts +1 -13
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +1 -32
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +1 -36
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
package/dist/client.js
CHANGED
|
@@ -1,859 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { FlagsStore } from './store.js';
|
|
3
|
-
import { createAsyncStorageAdapter } from './storage.js';
|
|
4
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
5
|
-
const ANON_ID_KEY = 'truflag:rn:anon-id';
|
|
6
|
-
const USER_CACHE_KEY = 'truflag:rn:user';
|
|
7
|
-
const SNAPSHOT_KEY = 'truflag:rn:snapshot';
|
|
8
|
-
const ASSIGNMENT_KEYS_CACHE_KEY = 'truflag:rn:assignment-keys';
|
|
9
|
-
const MAX_PERSISTED_ASSIGNMENT_KEYS = 1000;
|
|
10
|
-
const RELAY_BASE_URL = 'https://sdk.truflag.com/';
|
|
11
|
-
const DEFAULT_STREAM_URL = 'wss://stream.sdk.truflag.com';
|
|
12
|
-
const DEFAULT_CACHE_TTL_MS = 5 * 60_000;
|
|
13
|
-
const DEFAULT_POLL_MS = 60_000;
|
|
14
|
-
const DEFAULT_TIMEOUT_MS = 6_000;
|
|
15
|
-
const DEFAULT_TELEMETRY_FLUSH_MS = 10_000;
|
|
16
|
-
const DEFAULT_TELEMETRY_BATCH_SIZE = 50;
|
|
17
|
-
const DEFAULT_STREAM_RECONNECT_MS = 2_000;
|
|
18
|
-
const BLOCKED_ATTRIBUTE_KEYS = new Set([
|
|
19
|
-
'projectID',
|
|
20
|
-
'id',
|
|
21
|
-
'environmentID',
|
|
22
|
-
'currentUserID',
|
|
23
|
-
'deviceID',
|
|
24
|
-
'tenantGroup',
|
|
25
|
-
'isAnonymous',
|
|
26
|
-
'anonymousID',
|
|
27
|
-
'firstSeenAt',
|
|
28
|
-
'lastSeenAt',
|
|
29
|
-
'userAttributes',
|
|
30
|
-
]);
|
|
31
|
-
export class ReactNativeFlagsClient {
|
|
32
|
-
store = new FlagsStore();
|
|
33
|
-
sdkSource = "react-native";
|
|
34
|
-
pollTimer;
|
|
35
|
-
telemetryTimer;
|
|
36
|
-
config;
|
|
37
|
-
requestSequence = 0;
|
|
38
|
-
anonymousDeviceId;
|
|
39
|
-
attributesByUserId = new Map();
|
|
40
|
-
offlineFallbackFlags = {};
|
|
41
|
-
telemetryQueue = [];
|
|
42
|
-
exposureIdentityByFlag = new Map();
|
|
43
|
-
assignmentIdentityByFlag = new Map();
|
|
44
|
-
readEvaluationIdentityByFlag = new Map();
|
|
45
|
-
streamSocket;
|
|
46
|
-
streamReconnectTimer;
|
|
47
|
-
latestConfigVersion;
|
|
48
|
-
configureSignature;
|
|
49
|
-
configureInFlight;
|
|
50
|
-
subscribe = (callback) => this.store.subscribe(callback);
|
|
51
|
-
getState = () => this.store.getState();
|
|
52
|
-
async configure(options) {
|
|
53
|
-
const signature = this.buildConfigureSignature(options);
|
|
54
|
-
if (this.configureInFlight && this.configureSignature === signature) {
|
|
55
|
-
return this.configureInFlight;
|
|
56
|
-
}
|
|
57
|
-
if (this.store.getState().configured && this.configureSignature === signature) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const configureTask = this.configureInternal(options, signature);
|
|
61
|
-
this.configureInFlight = configureTask;
|
|
62
|
-
return configureTask.finally(() => {
|
|
63
|
-
if (this.configureInFlight === configureTask) {
|
|
64
|
-
this.configureInFlight = undefined;
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
async configureInternal(options, signature) {
|
|
69
|
-
validatePublicApiKey(options.apiKey);
|
|
70
|
-
this.stopStreaming();
|
|
71
|
-
this.stopPolling();
|
|
72
|
-
this.configureSignature = signature;
|
|
73
|
-
const resolvedStorage = options.storage ?? this.resolveDefaultStorageAdapter();
|
|
74
|
-
this.config = { ...options, storage: resolvedStorage };
|
|
75
|
-
const logger = options.logger ?? createDefaultLogger(true);
|
|
76
|
-
logger.info('Configuring Truflag RN SDK', { apiKey: maskApiKey(options.apiKey) });
|
|
77
|
-
const storage = resolvedStorage;
|
|
78
|
-
this.anonymousDeviceId = (await storage.getItem(ANON_ID_KEY)) ?? undefined;
|
|
79
|
-
const cachedUser = await this.loadCachedUser(storage);
|
|
80
|
-
const user = this.normalizeUser(options.user ?? cachedUser ?? (await this.resolveAnonymousUser(storage)));
|
|
81
|
-
await this.persistCachedUser(storage, user);
|
|
82
|
-
await this.ensureAnonymousDeviceId(storage, user);
|
|
83
|
-
const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
84
|
-
const cachedSnapshot = await loadCachedSnapshot(storage, SNAPSHOT_KEY, cacheTtlMs);
|
|
85
|
-
this.offlineFallbackFlags = indexFlags(cachedSnapshot?.offlineFallbackFlags ?? []);
|
|
86
|
-
const initialState = {
|
|
87
|
-
configured: true,
|
|
88
|
-
ready: Boolean(cachedSnapshot),
|
|
89
|
-
apiKey: options.apiKey,
|
|
90
|
-
user,
|
|
91
|
-
flags: indexFlags(cachedSnapshot?.flags ?? []),
|
|
92
|
-
};
|
|
93
|
-
if (cachedSnapshot?.fetchedAt !== undefined) {
|
|
94
|
-
initialState.lastFetchAt = cachedSnapshot.fetchedAt;
|
|
95
|
-
}
|
|
96
|
-
this.store.setState(initialState);
|
|
97
|
-
this.latestConfigVersion = undefined;
|
|
98
|
-
await this.loadPersistedAssignmentKeys(storage);
|
|
99
|
-
this.clearError();
|
|
100
|
-
await this.refresh();
|
|
101
|
-
this.startStreamOrPolling();
|
|
102
|
-
this.startTelemetryFlush();
|
|
103
|
-
}
|
|
104
|
-
async login(user) {
|
|
105
|
-
this.requireConfigured();
|
|
106
|
-
const nextUser = this.mergeUser(user);
|
|
107
|
-
this.store.patch({ user: nextUser });
|
|
108
|
-
await this.persistCachedUser(this.getStorage(), nextUser);
|
|
109
|
-
await this.refresh();
|
|
110
|
-
}
|
|
111
|
-
async setAttributes(attributes) {
|
|
112
|
-
this.requireConfigured();
|
|
113
|
-
const currentUser = this.getUser();
|
|
114
|
-
const nextUser = this.mergeUser({ id: currentUser.id, attributes });
|
|
115
|
-
this.store.patch({ user: nextUser });
|
|
116
|
-
await this.persistCachedUser(this.getStorage(), nextUser);
|
|
117
|
-
await this.refresh();
|
|
118
|
-
}
|
|
119
|
-
async identify(user) {
|
|
120
|
-
await this.login(user);
|
|
121
|
-
}
|
|
122
|
-
async logout() {
|
|
123
|
-
this.requireConfigured();
|
|
124
|
-
const storage = this.getStorage();
|
|
125
|
-
await this.ensureAnonymousDeviceId(storage, this.getUser());
|
|
126
|
-
const user = this.normalizeUser(toAnonymousUser(createAnonymousId()));
|
|
127
|
-
this.store.patch({ user });
|
|
128
|
-
await this.persistCachedUser(storage, user);
|
|
129
|
-
await this.refresh();
|
|
130
|
-
}
|
|
131
|
-
async refresh() {
|
|
132
|
-
this.requireConfigured();
|
|
133
|
-
const config = this.config;
|
|
134
|
-
const logger = config.logger ?? createDefaultLogger(true);
|
|
135
|
-
const storage = this.getStorage();
|
|
136
|
-
const sequence = ++this.requestSequence;
|
|
137
|
-
try {
|
|
138
|
-
const snapshot = await withRetry(async () => {
|
|
139
|
-
return withTimeout(config.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS, async (signal) => {
|
|
140
|
-
const activeUser = this.getUser();
|
|
141
|
-
const query = {
|
|
142
|
-
userId: activeUser.id,
|
|
143
|
-
};
|
|
144
|
-
const userAttributes = activeUser.attributes ?? this.attributesByUserId.get(activeUser.id);
|
|
145
|
-
if (userAttributes) {
|
|
146
|
-
query.userAttributes = JSON.stringify(userAttributes);
|
|
147
|
-
}
|
|
148
|
-
if (this.anonymousDeviceId) {
|
|
149
|
-
query.anonymousId = this.anonymousDeviceId;
|
|
150
|
-
}
|
|
151
|
-
const response = await this.getTransport().get('/v1/flags', {
|
|
152
|
-
apiKey: config.apiKey,
|
|
153
|
-
signal,
|
|
154
|
-
query,
|
|
155
|
-
});
|
|
156
|
-
const typed = response;
|
|
157
|
-
const meta = response.meta;
|
|
158
|
-
const offlineFallbackFlags = this.extractOfflineFallbackFlags(typed.flags ?? []);
|
|
159
|
-
const configVersion = typeof meta?.configVersion === 'string' ? meta.configVersion : undefined;
|
|
160
|
-
const now = Date.now();
|
|
161
|
-
return {
|
|
162
|
-
flags: typed.flags ?? [],
|
|
163
|
-
fetchedAt: now,
|
|
164
|
-
offlineFallbackFlags: Object.values(offlineFallbackFlags),
|
|
165
|
-
configVersion,
|
|
166
|
-
};
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
if (sequence !== this.requestSequence)
|
|
170
|
-
return;
|
|
171
|
-
const nextFlags = indexFlags(snapshot.flags);
|
|
172
|
-
this.offlineFallbackFlags = indexFlags(snapshot.offlineFallbackFlags ?? []);
|
|
173
|
-
this.store.patch({
|
|
174
|
-
flags: nextFlags,
|
|
175
|
-
lastFetchAt: snapshot.fetchedAt,
|
|
176
|
-
ready: true,
|
|
177
|
-
});
|
|
178
|
-
this.latestConfigVersion = snapshot.configVersion;
|
|
179
|
-
this.enqueueAssignmentEvents(snapshot.flags);
|
|
180
|
-
this.clearError();
|
|
181
|
-
await persistCachedSnapshot(storage, SNAPSHOT_KEY, snapshot);
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
logger.error('Truflag refresh failed', {
|
|
185
|
-
error: error instanceof Error ? error.message : String(error),
|
|
186
|
-
});
|
|
187
|
-
this.store.patch({
|
|
188
|
-
error: error instanceof Error ? error.message : 'Unknown refresh error',
|
|
189
|
-
});
|
|
190
|
-
if (Object.keys(this.offlineFallbackFlags).length > 0) {
|
|
191
|
-
this.store.patch({
|
|
192
|
-
flags: this.offlineFallbackFlags,
|
|
193
|
-
ready: true,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
else if (Object.keys(this.store.getState().flags).length > 0) {
|
|
197
|
-
this.store.patch({ ready: true });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
async track(eventName, properties, options) {
|
|
202
|
-
this.requireConfigured();
|
|
203
|
-
if (!eventName || !eventName.trim())
|
|
204
|
-
return;
|
|
205
|
-
const user = this.getUser();
|
|
206
|
-
this.enqueueTelemetryEvent({
|
|
207
|
-
name: eventName.trim(),
|
|
208
|
-
timestamp: new Date().toISOString(),
|
|
209
|
-
userId: user.id,
|
|
210
|
-
anonymousId: this.anonymousDeviceId,
|
|
211
|
-
userAttributes: user.attributes ?? this.attributesByUserId.get(user.id),
|
|
212
|
-
properties: properties ?? {},
|
|
213
|
-
}, 'track');
|
|
214
|
-
const maxBatch = this.config?.telemetryBatchSize ?? DEFAULT_TELEMETRY_BATCH_SIZE;
|
|
215
|
-
if (options?.immediate || this.telemetryQueue.length >= maxBatch) {
|
|
216
|
-
await this.flushTelemetry();
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
async expose(flagKey, options) {
|
|
220
|
-
this.requireConfigured();
|
|
221
|
-
const flag = this.getFlagOrThrow(flagKey);
|
|
222
|
-
await this.enqueueExposure(flag, options);
|
|
223
|
-
}
|
|
224
|
-
notifyFlagRead(flagKey) {
|
|
225
|
-
const state = this.store.getState();
|
|
226
|
-
if (!state.ready)
|
|
227
|
-
return;
|
|
228
|
-
const flag = state.flags[flagKey];
|
|
229
|
-
if (!flag)
|
|
230
|
-
return;
|
|
231
|
-
this.enqueueReadEvaluationEvents(flag);
|
|
232
|
-
void this.enqueueExposure(flag);
|
|
233
|
-
}
|
|
234
|
-
getFlag(key, defaultValue = false) {
|
|
235
|
-
const flags = this.store.getState().flags;
|
|
236
|
-
const flag = this.getFlagOrThrow(key);
|
|
237
|
-
this.enqueueReadEvaluationEvents(flag);
|
|
238
|
-
void this.enqueueExposure(flag);
|
|
239
|
-
return evaluateBooleanFlag(flags, key, defaultValue);
|
|
240
|
-
}
|
|
241
|
-
getFeatureFlagValue(key, defaultValue) {
|
|
242
|
-
const flags = this.store.getState().flags;
|
|
243
|
-
const flag = this.getFlagOrThrow(key);
|
|
244
|
-
this.enqueueReadEvaluationEvents(flag);
|
|
245
|
-
void this.enqueueExposure(flag);
|
|
246
|
-
return evaluateFlagValue(flags, key, defaultValue);
|
|
247
|
-
}
|
|
248
|
-
getFeatureFlagPayload(key) {
|
|
249
|
-
const flags = this.store.getState().flags;
|
|
250
|
-
this.getFlagOrThrow(key);
|
|
251
|
-
return getFlagPayload(flags, key);
|
|
252
|
-
}
|
|
253
|
-
getAllFlags() {
|
|
254
|
-
const output = {};
|
|
255
|
-
for (const [key, flag] of Object.entries(this.store.getState().flags)) {
|
|
256
|
-
this.enqueueReadEvaluationEvents(flag);
|
|
257
|
-
void this.enqueueExposure(flag);
|
|
258
|
-
output[key] = flag.value;
|
|
259
|
-
}
|
|
260
|
-
return output;
|
|
261
|
-
}
|
|
262
|
-
isReady() {
|
|
263
|
-
return this.store.getState().ready;
|
|
264
|
-
}
|
|
265
|
-
destroy() {
|
|
266
|
-
this.stopPolling();
|
|
267
|
-
if (this.telemetryTimer)
|
|
268
|
-
clearTimeout(this.telemetryTimer);
|
|
269
|
-
this.stopStreaming();
|
|
270
|
-
}
|
|
271
|
-
getStorage() {
|
|
272
|
-
const storage = this.config?.storage;
|
|
273
|
-
if (!storage) {
|
|
274
|
-
throw new Error('React Native SDK requires AsyncStorage. Install @react-native-async-storage/async-storage or pass storage explicitly.');
|
|
275
|
-
}
|
|
276
|
-
return storage;
|
|
277
|
-
}
|
|
278
|
-
resolveDefaultStorageAdapter() {
|
|
279
|
-
const candidate = AsyncStorage;
|
|
280
|
-
if (!candidate ||
|
|
281
|
-
typeof candidate.getItem !== 'function' ||
|
|
282
|
-
typeof candidate.setItem !== 'function' ||
|
|
283
|
-
typeof candidate.removeItem !== 'function') {
|
|
284
|
-
throw new Error('React Native SDK requires @react-native-async-storage/async-storage. Install it or pass storage explicitly.');
|
|
285
|
-
}
|
|
286
|
-
return createAsyncStorageAdapter(candidate);
|
|
287
|
-
}
|
|
288
|
-
getUser() {
|
|
289
|
-
const user = this.store.getState().user;
|
|
290
|
-
if (!user) {
|
|
291
|
-
throw new Error('SDK has no active user identity');
|
|
292
|
-
}
|
|
293
|
-
return user;
|
|
294
|
-
}
|
|
295
|
-
getTransport() {
|
|
296
|
-
const config = this.config;
|
|
297
|
-
const baseUrl = (config.baseUrl?.trim() || RELAY_BASE_URL).replace(/\/+$/, '/');
|
|
298
|
-
return new FetchTransport(baseUrl, config.fetchFn);
|
|
299
|
-
}
|
|
300
|
-
getFlagOrThrow(key) {
|
|
301
|
-
const flag = this.store.getState().flags[key];
|
|
302
|
-
if (flag)
|
|
303
|
-
return flag;
|
|
304
|
-
const logger = this.config?.logger ?? createDefaultLogger(true);
|
|
305
|
-
logger.error('Truflag missing flag access', { flagKey: key });
|
|
306
|
-
throw new Error(`Flag "${key}" does not exist in SDK state.`);
|
|
307
|
-
}
|
|
308
|
-
enqueueTelemetryEvent(event, reason) {
|
|
309
|
-
this.telemetryQueue.push(event);
|
|
310
|
-
const logger = this.config?.logger ?? createDefaultLogger(true);
|
|
311
|
-
const eventName = typeof event.name === 'string' ? event.name : 'unknown';
|
|
312
|
-
const properties = event.properties && typeof event.properties === 'object' && !Array.isArray(event.properties)
|
|
313
|
-
? event.properties
|
|
314
|
-
: undefined;
|
|
315
|
-
logger.debug('Truflag telemetry queued', {
|
|
316
|
-
reason,
|
|
317
|
-
eventName,
|
|
318
|
-
queueSize: this.telemetryQueue.length,
|
|
319
|
-
userId: typeof event.userId === 'string' ? event.userId : undefined,
|
|
320
|
-
flagKey: typeof properties?.flagKey === 'string' ? properties.flagKey : undefined,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
buildConfigureSignature(options) {
|
|
324
|
-
const attrs = options.user?.attributes;
|
|
325
|
-
const normalizedAttrs = attrs && typeof attrs === 'object'
|
|
326
|
-
? Object.keys(attrs)
|
|
327
|
-
.sort()
|
|
328
|
-
.map((key) => [key, attrs[key]])
|
|
329
|
-
: [];
|
|
330
|
-
return JSON.stringify({
|
|
331
|
-
apiKey: options.apiKey,
|
|
332
|
-
baseUrl: options.baseUrl ?? '',
|
|
333
|
-
userId: options.user?.id ?? '',
|
|
334
|
-
userAttributes: normalizedAttrs,
|
|
335
|
-
cacheTtlMs: options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
336
|
-
pollingIntervalMs: options.pollingIntervalMs ?? DEFAULT_POLL_MS,
|
|
337
|
-
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
338
|
-
streamEnabled: options.streamEnabled ?? true,
|
|
339
|
-
streamUrl: options.streamUrl ?? '',
|
|
340
|
-
telemetryEnabled: options.telemetryEnabled ?? true,
|
|
341
|
-
telemetryFlushIntervalMs: options.telemetryFlushIntervalMs ?? DEFAULT_TELEMETRY_FLUSH_MS,
|
|
342
|
-
telemetryBatchSize: options.telemetryBatchSize ?? DEFAULT_TELEMETRY_BATCH_SIZE,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
requireConfigured() {
|
|
346
|
-
if (!this.config || !this.store.getState().configured) {
|
|
347
|
-
throw new Error('Flags.configure must be called before this operation');
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
async resolveAnonymousUser(storage) {
|
|
351
|
-
const existing = await storage.getItem(ANON_ID_KEY);
|
|
352
|
-
if (existing) {
|
|
353
|
-
this.anonymousDeviceId = existing;
|
|
354
|
-
return toAnonymousUser(existing);
|
|
355
|
-
}
|
|
356
|
-
const next = createAnonymousId();
|
|
357
|
-
await storage.setItem(ANON_ID_KEY, next);
|
|
358
|
-
this.anonymousDeviceId = next;
|
|
359
|
-
return toAnonymousUser(next);
|
|
360
|
-
}
|
|
361
|
-
async ensureAnonymousDeviceId(storage, user) {
|
|
362
|
-
if (this.anonymousDeviceId)
|
|
363
|
-
return;
|
|
364
|
-
const persisted = await storage.getItem(ANON_ID_KEY);
|
|
365
|
-
if (persisted) {
|
|
366
|
-
this.anonymousDeviceId = persisted;
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const deviceId = user.id.startsWith('anon_') ? user.id : createAnonymousId();
|
|
370
|
-
this.anonymousDeviceId = deviceId;
|
|
371
|
-
await storage.setItem(ANON_ID_KEY, deviceId);
|
|
372
|
-
}
|
|
373
|
-
async loadCachedUser(storage) {
|
|
374
|
-
const raw = await storage.getItem(USER_CACHE_KEY);
|
|
375
|
-
if (!raw)
|
|
376
|
-
return undefined;
|
|
377
|
-
try {
|
|
378
|
-
const parsed = this.normalizeUser(JSON.parse(raw));
|
|
379
|
-
if (!parsed?.id || typeof parsed.id !== 'string')
|
|
380
|
-
return undefined;
|
|
381
|
-
return parsed;
|
|
382
|
-
}
|
|
383
|
-
catch {
|
|
384
|
-
return undefined;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
async persistCachedUser(storage, user) {
|
|
388
|
-
await storage.setItem(USER_CACHE_KEY, JSON.stringify(user));
|
|
389
|
-
}
|
|
390
|
-
async loadPersistedAssignmentKeys(storage) {
|
|
391
|
-
this.assignmentIdentityByFlag.clear();
|
|
392
|
-
const raw = await storage.getItem(ASSIGNMENT_KEYS_CACHE_KEY);
|
|
393
|
-
if (!raw)
|
|
394
|
-
return;
|
|
395
|
-
try {
|
|
396
|
-
const parsed = JSON.parse(raw);
|
|
397
|
-
if (Array.isArray(parsed)) {
|
|
398
|
-
const keys = parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
399
|
-
const tail = keys.slice(-MAX_PERSISTED_ASSIGNMENT_KEYS);
|
|
400
|
-
for (const key of tail) {
|
|
401
|
-
this.assignmentIdentityByFlag.set(`legacy:${this.assignmentIdentityByFlag.size}`, key);
|
|
402
|
-
}
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
if (!parsed || typeof parsed !== 'object')
|
|
406
|
-
return;
|
|
407
|
-
const entries = Object.entries(parsed)
|
|
408
|
-
.filter((entry) => typeof entry[0] === 'string' && typeof entry[1] === 'string')
|
|
409
|
-
.slice(-MAX_PERSISTED_ASSIGNMENT_KEYS);
|
|
410
|
-
for (const [flagKey, identity] of entries) {
|
|
411
|
-
if (!flagKey || !identity)
|
|
412
|
-
continue;
|
|
413
|
-
this.assignmentIdentityByFlag.set(flagKey, identity);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
catch {
|
|
417
|
-
// Ignore malformed cache and proceed with empty set.
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
async persistAssignmentKeys() {
|
|
421
|
-
const storage = this.config?.storage;
|
|
422
|
-
if (!storage)
|
|
423
|
-
return;
|
|
424
|
-
const entries = Array.from(this.assignmentIdentityByFlag.entries()).slice(-MAX_PERSISTED_ASSIGNMENT_KEYS);
|
|
425
|
-
this.assignmentIdentityByFlag.clear();
|
|
426
|
-
for (const [flagKey, identity] of entries) {
|
|
427
|
-
this.assignmentIdentityByFlag.set(flagKey, identity);
|
|
428
|
-
}
|
|
429
|
-
await storage.setItem(ASSIGNMENT_KEYS_CACHE_KEY, JSON.stringify(Object.fromEntries(entries)));
|
|
430
|
-
}
|
|
431
|
-
normalizeUser(user) {
|
|
432
|
-
const normalized = { id: String(user.id) };
|
|
433
|
-
const attrs = this.normalizeAttributes(user.attributes);
|
|
434
|
-
if (attrs) {
|
|
435
|
-
normalized.attributes = attrs;
|
|
436
|
-
this.attributesByUserId.set(normalized.id, attrs);
|
|
437
|
-
}
|
|
438
|
-
return normalized;
|
|
439
|
-
}
|
|
440
|
-
normalizeAttributes(attributes) {
|
|
441
|
-
if (!attributes || typeof attributes !== 'object' || Array.isArray(attributes)) {
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
const blocked = Object.keys(attributes).filter((key) => BLOCKED_ATTRIBUTE_KEYS.has(key));
|
|
445
|
-
if (blocked.length > 0) {
|
|
446
|
-
throw new Error(`Blocked attribute key(s): ${blocked.join(', ')}. Remove reserved keys from user.attributes.`);
|
|
447
|
-
}
|
|
448
|
-
return { ...attributes };
|
|
449
|
-
}
|
|
450
|
-
mergeUser(user) {
|
|
451
|
-
const userId = String(user.id);
|
|
452
|
-
const incoming = this.normalizeAttributes(user.attributes);
|
|
453
|
-
const existing = this.attributesByUserId.get(userId);
|
|
454
|
-
const merged = { ...(existing ?? {}), ...(incoming ?? {}) };
|
|
455
|
-
const normalized = { id: userId };
|
|
456
|
-
if (Object.keys(merged).length > 0) {
|
|
457
|
-
normalized.attributes = merged;
|
|
458
|
-
this.attributesByUserId.set(userId, merged);
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
this.attributesByUserId.delete(userId);
|
|
462
|
-
}
|
|
463
|
-
return normalized;
|
|
464
|
-
}
|
|
465
|
-
startPolling() {
|
|
466
|
-
if (this.pollTimer)
|
|
467
|
-
clearTimeout(this.pollTimer);
|
|
468
|
-
const interval = this.config?.pollingIntervalMs ?? DEFAULT_POLL_MS;
|
|
469
|
-
this.pollTimer = setTimeout(() => {
|
|
470
|
-
void this.refresh().finally(() => this.startPolling());
|
|
471
|
-
}, interval);
|
|
472
|
-
const timer = this.pollTimer;
|
|
473
|
-
timer.unref?.();
|
|
474
|
-
}
|
|
475
|
-
stopPolling() {
|
|
476
|
-
if (!this.pollTimer)
|
|
477
|
-
return;
|
|
478
|
-
clearTimeout(this.pollTimer);
|
|
479
|
-
this.pollTimer = undefined;
|
|
480
|
-
}
|
|
481
|
-
startStreamOrPolling() {
|
|
482
|
-
const streamEnabled = this.config?.streamEnabled ?? true;
|
|
483
|
-
const streamUrl = this.resolveStreamUrl();
|
|
484
|
-
if (!streamEnabled || !streamUrl) {
|
|
485
|
-
this.startPolling();
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
this.startStreaming(streamUrl);
|
|
489
|
-
}
|
|
490
|
-
startStreaming(streamBaseUrl) {
|
|
491
|
-
this.stopStreaming();
|
|
492
|
-
const config = this.config;
|
|
493
|
-
const logger = config.logger ?? createDefaultLogger(true);
|
|
494
|
-
const url = new URL(streamBaseUrl);
|
|
495
|
-
url.searchParams.set('apiKey', config.apiKey);
|
|
496
|
-
let socket;
|
|
497
|
-
try {
|
|
498
|
-
socket = new WebSocket(url.toString());
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
logger.warn('Truflag stream open failed, using polling fallback');
|
|
502
|
-
this.startPolling();
|
|
503
|
-
this.scheduleStreamReconnect();
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
this.streamSocket = socket;
|
|
507
|
-
this.startPolling();
|
|
508
|
-
socket.onopen = () => {
|
|
509
|
-
logger.info('Truflag stream connected');
|
|
510
|
-
this.stopPolling();
|
|
511
|
-
};
|
|
512
|
-
socket.onmessage = (event) => {
|
|
513
|
-
this.handleStreamMessage(event.data);
|
|
514
|
-
};
|
|
515
|
-
socket.onerror = () => {
|
|
516
|
-
logger.warn('Truflag stream error, polling fallback active');
|
|
517
|
-
this.startPolling();
|
|
518
|
-
};
|
|
519
|
-
socket.onclose = (event) => {
|
|
520
|
-
logger.warn('Truflag stream closed, reconnect scheduled', {
|
|
521
|
-
code: event.code,
|
|
522
|
-
reason: event.reason,
|
|
523
|
-
});
|
|
524
|
-
this.startPolling();
|
|
525
|
-
this.scheduleStreamReconnect();
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
stopStreaming() {
|
|
529
|
-
if (this.streamReconnectTimer) {
|
|
530
|
-
clearTimeout(this.streamReconnectTimer);
|
|
531
|
-
this.streamReconnectTimer = undefined;
|
|
532
|
-
}
|
|
533
|
-
if (!this.streamSocket)
|
|
534
|
-
return;
|
|
535
|
-
const socket = this.streamSocket;
|
|
536
|
-
this.streamSocket = undefined;
|
|
537
|
-
socket.onopen = null;
|
|
538
|
-
socket.onmessage = null;
|
|
539
|
-
socket.onerror = null;
|
|
540
|
-
socket.onclose = null;
|
|
541
|
-
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
542
|
-
socket.close();
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
scheduleStreamReconnect() {
|
|
546
|
-
if (this.streamReconnectTimer)
|
|
547
|
-
return;
|
|
548
|
-
const delayMs = Math.max(DEFAULT_STREAM_RECONNECT_MS, (this.config?.pollingIntervalMs ?? DEFAULT_POLL_MS) / 2);
|
|
549
|
-
this.streamReconnectTimer = setTimeout(() => {
|
|
550
|
-
this.streamReconnectTimer = undefined;
|
|
551
|
-
if (!this.config)
|
|
552
|
-
return;
|
|
553
|
-
const logger = this.config.logger ?? createDefaultLogger(true);
|
|
554
|
-
logger.debug('Truflag stream reconnecting');
|
|
555
|
-
this.startStreamOrPolling();
|
|
556
|
-
}, delayMs);
|
|
557
|
-
const timer = this.streamReconnectTimer;
|
|
558
|
-
timer.unref?.();
|
|
559
|
-
}
|
|
560
|
-
handleStreamMessage(raw) {
|
|
561
|
-
if (typeof raw !== 'string')
|
|
562
|
-
return;
|
|
563
|
-
let payload;
|
|
564
|
-
try {
|
|
565
|
-
payload = JSON.parse(raw);
|
|
566
|
-
}
|
|
567
|
-
catch {
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
const messageType = typeof payload.type === 'string' ? payload.type : '';
|
|
571
|
-
if (messageType !== 'config_published')
|
|
572
|
-
return;
|
|
573
|
-
const incomingVersion = typeof payload.version === 'string' ? payload.version : '';
|
|
574
|
-
if (incomingVersion && this.latestConfigVersion && incomingVersion === this.latestConfigVersion) {
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
void this.refresh();
|
|
578
|
-
}
|
|
579
|
-
resolveStreamUrl() {
|
|
580
|
-
const configured = this.config?.streamUrl?.trim();
|
|
581
|
-
if (configured)
|
|
582
|
-
return configured;
|
|
583
|
-
return DEFAULT_STREAM_URL;
|
|
584
|
-
}
|
|
585
|
-
startTelemetryFlush() {
|
|
586
|
-
if (this.telemetryTimer)
|
|
587
|
-
clearTimeout(this.telemetryTimer);
|
|
588
|
-
const interval = this.config?.telemetryFlushIntervalMs ?? DEFAULT_TELEMETRY_FLUSH_MS;
|
|
589
|
-
this.telemetryTimer = setTimeout(() => {
|
|
590
|
-
void this.flushTelemetry().finally(() => this.startTelemetryFlush());
|
|
591
|
-
}, interval);
|
|
592
|
-
const timer = this.telemetryTimer;
|
|
593
|
-
timer.unref?.();
|
|
594
|
-
}
|
|
595
|
-
async flushTelemetry() {
|
|
596
|
-
this.requireConfigured();
|
|
597
|
-
if ((this.config?.telemetryEnabled ?? true) === false)
|
|
598
|
-
return;
|
|
599
|
-
if (!this.telemetryQueue.length)
|
|
600
|
-
return;
|
|
601
|
-
const logger = this.config?.logger ?? createDefaultLogger(true);
|
|
602
|
-
const batchSize = this.config?.telemetryBatchSize ?? DEFAULT_TELEMETRY_BATCH_SIZE;
|
|
603
|
-
const events = this.telemetryQueue.slice(0, batchSize);
|
|
604
|
-
try {
|
|
605
|
-
logger.debug('Truflag telemetry flush started', {
|
|
606
|
-
count: events.length,
|
|
607
|
-
eventNames: events.map((event) => String(event.name ?? 'unknown')),
|
|
608
|
-
});
|
|
609
|
-
await this.getTransport().post('/v1/events/batch', {
|
|
610
|
-
apiKey: this.config.apiKey,
|
|
611
|
-
body: {
|
|
612
|
-
events,
|
|
613
|
-
},
|
|
614
|
-
});
|
|
615
|
-
this.telemetryQueue.splice(0, events.length);
|
|
616
|
-
logger.debug('Truflag telemetry flush succeeded', {
|
|
617
|
-
flushedCount: events.length,
|
|
618
|
-
queueSize: this.telemetryQueue.length,
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
logger.warn('Truflag telemetry flush failed; keeping events queued', {
|
|
623
|
-
attemptedCount: events.length,
|
|
624
|
-
queueSize: this.telemetryQueue.length,
|
|
625
|
-
});
|
|
626
|
-
// Keep events queued for next flush attempt.
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
enqueueReadEvaluationEvents(flag) {
|
|
630
|
-
if ((this.config?.telemetryEnabled ?? true) === false)
|
|
631
|
-
return;
|
|
632
|
-
const now = new Date().toISOString();
|
|
633
|
-
const user = this.store.getState().user;
|
|
634
|
-
const payload = this.getFlagPayloadRecord(flag);
|
|
635
|
-
const variationId = typeof payload === 'object' && typeof payload.variationId === 'string' ? payload.variationId : '';
|
|
636
|
-
if (!this.shouldEmitFlagIdentity(this.readEvaluationIdentityByFlag, flag.key, this.buildReadIdentity(flag))) {
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
const rawValue = flag.value === null || typeof flag.value === 'boolean' || typeof flag.value === 'number'
|
|
640
|
-
? flag.value
|
|
641
|
-
: typeof flag.value === 'string'
|
|
642
|
-
? flag.value.length > 120
|
|
643
|
-
? `${flag.value.slice(0, 117)}...`
|
|
644
|
-
: flag.value
|
|
645
|
-
: '[complex]';
|
|
646
|
-
this.enqueueTelemetryEvent({
|
|
647
|
-
name: 'truflag.system.fetch_evaluation',
|
|
648
|
-
timestamp: now,
|
|
649
|
-
userId: user?.id ?? '',
|
|
650
|
-
anonymousId: this.anonymousDeviceId,
|
|
651
|
-
properties: {
|
|
652
|
-
sdkSource: this.sdkSource,
|
|
653
|
-
flagKey: flag.key,
|
|
654
|
-
variationId: variationId || undefined,
|
|
655
|
-
enabled: typeof payload === 'object' ? payload.enabled : undefined,
|
|
656
|
-
kind: typeof payload === 'object' ? payload.kind : undefined,
|
|
657
|
-
reason: typeof payload === 'object' ? payload.reason : undefined,
|
|
658
|
-
rolloutId: typeof payload === 'object' ? payload.rolloutId : undefined,
|
|
659
|
-
rolloutStepIndex: typeof payload === 'object' ? payload.rolloutStepIndex : undefined,
|
|
660
|
-
rolloutReason: typeof payload === 'object' ? payload.rolloutReason : undefined,
|
|
661
|
-
bucketBy: typeof payload === 'object' ? payload.bucketBy : undefined,
|
|
662
|
-
source: 'sdk-read',
|
|
663
|
-
},
|
|
664
|
-
}, 'read-evaluation');
|
|
665
|
-
this.enqueueTelemetryEvent({
|
|
666
|
-
name: 'truflag.system.fetch_evaluation_result',
|
|
667
|
-
timestamp: now,
|
|
668
|
-
userId: user?.id ?? '',
|
|
669
|
-
anonymousId: this.anonymousDeviceId,
|
|
670
|
-
properties: {
|
|
671
|
-
sdkSource: this.sdkSource,
|
|
672
|
-
flagKey: flag.key,
|
|
673
|
-
variationId: variationId || undefined,
|
|
674
|
-
value: rawValue,
|
|
675
|
-
source: 'sdk-read',
|
|
676
|
-
},
|
|
677
|
-
}, 'read-evaluation-result');
|
|
678
|
-
}
|
|
679
|
-
enqueueAssignmentEvents(flags) {
|
|
680
|
-
if ((this.config?.telemetryEnabled ?? true) === false)
|
|
681
|
-
return;
|
|
682
|
-
const now = new Date().toISOString();
|
|
683
|
-
const user = this.store.getState().user;
|
|
684
|
-
let addedIdentity = false;
|
|
685
|
-
for (const flag of flags) {
|
|
686
|
-
const payload = this.getFlagPayloadRecord(flag);
|
|
687
|
-
const experimentId = typeof payload.experimentId === 'string' ? payload.experimentId : '';
|
|
688
|
-
const experimentArmId = typeof payload.experimentArmId === 'string' ? payload.experimentArmId : '';
|
|
689
|
-
const assignmentId = typeof payload.assignmentId === 'string' ? payload.assignmentId : '';
|
|
690
|
-
const configVersion = typeof payload.configVersion === 'string' ? payload.configVersion : '';
|
|
691
|
-
if (!experimentId || !experimentArmId || !assignmentId)
|
|
692
|
-
continue;
|
|
693
|
-
if (!this.shouldEmitFlagIdentity(this.assignmentIdentityByFlag, flag.key, this.buildAssignmentIdentity(flag))) {
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
addedIdentity = true;
|
|
697
|
-
this.enqueueTelemetryEvent({
|
|
698
|
-
name: 'truflag.system.assignment',
|
|
699
|
-
timestamp: now,
|
|
700
|
-
userId: user?.id ?? '',
|
|
701
|
-
anonymousId: this.anonymousDeviceId,
|
|
702
|
-
properties: {
|
|
703
|
-
sdkSource: this.sdkSource,
|
|
704
|
-
flagKey: flag.key,
|
|
705
|
-
variationId: typeof payload.variationId === 'string' ? payload.variationId : undefined,
|
|
706
|
-
experimentId,
|
|
707
|
-
experimentArmId,
|
|
708
|
-
assignmentId,
|
|
709
|
-
configVersion: configVersion || undefined,
|
|
710
|
-
source: 'sdk',
|
|
711
|
-
},
|
|
712
|
-
}, 'assignment');
|
|
713
|
-
}
|
|
714
|
-
if (addedIdentity)
|
|
715
|
-
void this.persistAssignmentKeys();
|
|
716
|
-
}
|
|
717
|
-
async enqueueExposure(flag, options) {
|
|
718
|
-
const payload = this.getFlagPayloadRecord(flag);
|
|
719
|
-
const variationId = typeof payload.variationId === 'string' ? payload.variationId : '';
|
|
720
|
-
const configVersion = typeof payload.configVersion === 'string' ? payload.configVersion : '';
|
|
721
|
-
const experimentId = typeof payload.experimentId === 'string' ? payload.experimentId : '';
|
|
722
|
-
const experimentArmId = typeof payload.experimentArmId === 'string' ? payload.experimentArmId : '';
|
|
723
|
-
const assignmentId = typeof payload.assignmentId === 'string' ? payload.assignmentId : '';
|
|
724
|
-
const includeExperimentProperties = Boolean(experimentId && experimentArmId && assignmentId);
|
|
725
|
-
if (!this.shouldEmitFlagIdentity(this.exposureIdentityByFlag, flag.key, this.buildExposureIdentity(flag))) {
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
await this.track('truflag.system.exposure', {
|
|
729
|
-
sdkSource: this.sdkSource,
|
|
730
|
-
flagKey: flag.key,
|
|
731
|
-
variationId: variationId || undefined,
|
|
732
|
-
experimentId: includeExperimentProperties ? experimentId : undefined,
|
|
733
|
-
experimentArmId: includeExperimentProperties ? experimentArmId : undefined,
|
|
734
|
-
assignmentId: includeExperimentProperties ? assignmentId : undefined,
|
|
735
|
-
configVersion: configVersion || undefined,
|
|
736
|
-
reason: typeof payload.reason === 'string' ? payload.reason : undefined,
|
|
737
|
-
rolloutId: typeof payload.rolloutId === 'string' ? payload.rolloutId : undefined,
|
|
738
|
-
rolloutStepIndex: typeof payload.rolloutStepIndex === 'number' ? payload.rolloutStepIndex : undefined,
|
|
739
|
-
rolloutReason: typeof payload.rolloutReason === 'string' ? payload.rolloutReason : undefined,
|
|
740
|
-
source: 'sdk',
|
|
741
|
-
...(options?.properties ?? {}),
|
|
742
|
-
}, options?.immediate !== undefined ? { immediate: options.immediate } : undefined);
|
|
743
|
-
}
|
|
744
|
-
getFlagPayloadRecord(flag) {
|
|
745
|
-
const payload = flag.payload;
|
|
746
|
-
return payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
747
|
-
? payload
|
|
748
|
-
: {};
|
|
749
|
-
}
|
|
750
|
-
shouldEmitFlagIdentity(cache, cacheKey, identity) {
|
|
751
|
-
if (cache.get(cacheKey) === identity)
|
|
752
|
-
return false;
|
|
753
|
-
cache.set(cacheKey, identity);
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
buildReadIdentity(flag) {
|
|
757
|
-
return this.buildFlagTelemetryIdentity(flag, 'read');
|
|
758
|
-
}
|
|
759
|
-
buildExposureIdentity(flag) {
|
|
760
|
-
return this.buildFlagTelemetryIdentity(flag, 'exposure');
|
|
761
|
-
}
|
|
762
|
-
buildAssignmentIdentity(flag) {
|
|
763
|
-
const payload = this.getFlagPayloadRecord(flag);
|
|
764
|
-
return this.joinTelemetryIdentityParts([
|
|
765
|
-
'assignment',
|
|
766
|
-
this.getTelemetryIdentitySegment('user', this.store.getState().user?.id),
|
|
767
|
-
this.getTelemetryIdentitySegment('anon', this.anonymousDeviceId),
|
|
768
|
-
this.getTelemetryIdentitySegment('env', this.readStringProperty(payload, 'environmentId')),
|
|
769
|
-
this.getTelemetryIdentitySegment('flag', flag.key),
|
|
770
|
-
this.getTelemetryIdentitySegment('config', this.readStringProperty(payload, 'configVersion')),
|
|
771
|
-
this.getTelemetryIdentitySegment('assignment', this.readStringProperty(payload, 'assignmentId')),
|
|
772
|
-
this.getTelemetryIdentitySegment('experiment', this.readStringProperty(payload, 'experimentId')),
|
|
773
|
-
this.getTelemetryIdentitySegment('arm', this.readStringProperty(payload, 'experimentArmId')),
|
|
774
|
-
this.getTelemetryIdentitySegment('variation', this.readStringProperty(payload, 'variationId')),
|
|
775
|
-
]);
|
|
776
|
-
}
|
|
777
|
-
buildFlagTelemetryIdentity(flag, eventType) {
|
|
778
|
-
const payload = this.getFlagPayloadRecord(flag);
|
|
779
|
-
return this.joinTelemetryIdentityParts([
|
|
780
|
-
eventType,
|
|
781
|
-
this.getTelemetryIdentitySegment('user', this.store.getState().user?.id),
|
|
782
|
-
this.getTelemetryIdentitySegment('anon', this.anonymousDeviceId),
|
|
783
|
-
this.getTelemetryIdentitySegment('env', this.readStringProperty(payload, 'environmentId')),
|
|
784
|
-
this.getTelemetryIdentitySegment('flag', flag.key),
|
|
785
|
-
this.getTelemetryIdentitySegment('config', this.readStringProperty(payload, 'configVersion')),
|
|
786
|
-
this.getTelemetryIdentitySegment('assignment', this.readStringProperty(payload, 'assignmentId')),
|
|
787
|
-
this.getTelemetryIdentitySegment('variation', this.readStringProperty(payload, 'variationId')),
|
|
788
|
-
this.getTelemetryIdentitySegment('value', this.fingerprintTelemetryValue(flag.value)),
|
|
789
|
-
]);
|
|
790
|
-
}
|
|
791
|
-
readStringProperty(payload, key) {
|
|
792
|
-
const value = payload[key];
|
|
793
|
-
return typeof value === 'string' ? value : '';
|
|
794
|
-
}
|
|
795
|
-
getTelemetryIdentitySegment(label, value) {
|
|
796
|
-
return `${label}=${String(value ?? '')}`;
|
|
797
|
-
}
|
|
798
|
-
joinTelemetryIdentityParts(parts) {
|
|
799
|
-
return parts.join('|');
|
|
800
|
-
}
|
|
801
|
-
fingerprintTelemetryValue(value) {
|
|
802
|
-
const serialized = this.serializeTelemetryValue(value);
|
|
803
|
-
let hash = 2166136261;
|
|
804
|
-
for (let index = 0; index < serialized.length; index += 1) {
|
|
805
|
-
hash ^= serialized.charCodeAt(index);
|
|
806
|
-
hash = Math.imul(hash, 16777619);
|
|
807
|
-
}
|
|
808
|
-
return `${serialized.length}:${(hash >>> 0).toString(16)}`;
|
|
809
|
-
}
|
|
810
|
-
serializeTelemetryValue(value) {
|
|
811
|
-
if (value === null)
|
|
812
|
-
return 'null';
|
|
813
|
-
if (value === undefined)
|
|
814
|
-
return 'undefined';
|
|
815
|
-
if (typeof value === 'string')
|
|
816
|
-
return JSON.stringify(value);
|
|
817
|
-
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
818
|
-
return String(value);
|
|
819
|
-
}
|
|
820
|
-
if (Array.isArray(value)) {
|
|
821
|
-
return `[${value.map((entry) => this.serializeTelemetryValue(entry)).join(',')}]`;
|
|
822
|
-
}
|
|
823
|
-
if (typeof value === 'object') {
|
|
824
|
-
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
825
|
-
return `{${entries
|
|
826
|
-
.map(([key, entryValue]) => `${JSON.stringify(key)}:${this.serializeTelemetryValue(entryValue)}`)
|
|
827
|
-
.join(',')}}`;
|
|
828
|
-
}
|
|
829
|
-
return String(value);
|
|
830
|
-
}
|
|
831
|
-
clearError() {
|
|
832
|
-
const state = this.store.getState();
|
|
833
|
-
if (state.error === undefined)
|
|
834
|
-
return;
|
|
835
|
-
const { error: _error, ...rest } = state;
|
|
836
|
-
this.store.setState(rest);
|
|
837
|
-
}
|
|
838
|
-
extractOfflineFallbackFlags(flags) {
|
|
839
|
-
const out = [];
|
|
840
|
-
for (const flag of flags) {
|
|
841
|
-
const payload = flag.payload;
|
|
842
|
-
const fallbackValue = payload && typeof payload === 'object'
|
|
843
|
-
? payload.offlineFallbackValue
|
|
844
|
-
: undefined;
|
|
845
|
-
if (fallbackValue === undefined)
|
|
846
|
-
continue;
|
|
847
|
-
out.push({
|
|
848
|
-
key: flag.key,
|
|
849
|
-
value: fallbackValue,
|
|
850
|
-
payload: {
|
|
851
|
-
...(payload ?? {}),
|
|
852
|
-
source: 'offline-fallback',
|
|
853
|
-
},
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
return indexFlags(out);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
1
|
+
export { ReactNativeFlagsClient } from '@truflag/sdk-platform-react-native';
|
|
859
2
|
//# sourceMappingURL=client.js.map
|