@posthog/browser-common 0.9.2 → 0.9.3

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.
@@ -0,0 +1,1172 @@
1
+ import { addEventListener, eachArray, entries, extend } from "./utils/general-utils.mjs";
2
+ import { DOM_EVENT_VISIBILITYCHANGE, ENABLED_FEATURE_FLAGS, FLAG_CALL_REPORTED, FLAG_CALL_REPORTED_SESSION_ID, PERSISTENCE_ACTIVE_FEATURE_FLAGS, PERSISTENCE_EARLY_ACCESS_FEATURES, PERSISTENCE_FEATURE_FLAG_DETAILS, PERSISTENCE_FEATURE_FLAG_ERRORS, PERSISTENCE_FEATURE_FLAG_EVALUATED_AT, PERSISTENCE_FEATURE_FLAG_PAYLOADS, PERSISTENCE_FEATURE_FLAG_REQUEST_ID, PERSISTENCE_MINIMAL_FLAG_CALLED_EVENTS, PERSISTENCE_OVERRIDE_FEATURE_FLAGS, PERSISTENCE_OVERRIDE_FEATURE_FLAG_PAYLOADS, STORED_GROUP_PROPERTIES_KEY, STORED_PERSON_PROPERTIES_KEY } from "./constants.mjs";
3
+ import { getEnabledFromValue, getVariantFromValue, isArray, isUndefined, parsePayload } from "@posthog/core";
4
+ import { createLogger } from "./utils/logger.mjs";
5
+ import { getTimezone } from "./utils/timezone.mjs";
6
+ import { continueWith } from "./utils/promise-utils.mjs";
7
+ import { isStatusZeroFailureCircuitBreakerTripped, updateStatusZeroFailureCount } from "./utils/request-reachability.mjs";
8
+ const logger = createLogger('[FeatureFlags]');
9
+ const forceDebugLogger = createLogger('[FeatureFlags]', {
10
+ debugEnabled: true
11
+ });
12
+ const FLAG_TIMEOUT_MSG = '" failed. Feature flags didn\'t load in time.';
13
+ const MAX_CONSECUTIVE_FLAGS_STATUS_ZERO_FAILURES = 3;
14
+ const RETRYABLE_FLAGS_HTTP_STATUSES = [
15
+ 502,
16
+ 504
17
+ ];
18
+ const FLAGS_RETRY_DELAY_MS = 500;
19
+ const isFlagsTimeout = (response)=>0 === response.statusCode && response.error instanceof Error && 'AbortError' === response.error.name;
20
+ const isRetryableFlagsResponse = (response)=>RETRYABLE_FLAGS_HTTP_STATUSES.includes(response.statusCode) || isFlagsTimeout(response);
21
+ const MAX_IDLE_REFRESH_INTERVAL_MS = 3600000;
22
+ const USER_INTERACTION_EVENTS = [
23
+ 'click',
24
+ 'keydown',
25
+ 'wheel',
26
+ 'touchstart',
27
+ 'pointerdown'
28
+ ];
29
+ const FeatureFlagError = {
30
+ ERRORS_WHILE_COMPUTING: 'errors_while_computing_flags',
31
+ FLAG_MISSING: 'flag_missing',
32
+ QUOTA_LIMITED: 'quota_limited',
33
+ TIMEOUT: 'timeout',
34
+ CONNECTION_ERROR: 'connection_error',
35
+ UNKNOWN_ERROR: 'unknown_error',
36
+ apiError: (status)=>`api_error_${status}`
37
+ };
38
+ const arrayToFlagsRecord = (flags)=>{
39
+ const flagsObj = {};
40
+ for(let i = 0; i < flags.length; i++)flagsObj[flags[i]] = true;
41
+ return flagsObj;
42
+ };
43
+ const filterActiveFeatureFlags = (featureFlags)=>{
44
+ const activeFeatureFlags = {};
45
+ for (const [key, value] of entries(featureFlags || {}))if (value) activeFeatureFlags[key] = value;
46
+ return activeFeatureFlags;
47
+ };
48
+ const parseFlagsResponse = (response, currentFlags = {}, currentFlagPayloads = {}, currentFlagDetails = {}, options, responseLogger = logger)=>{
49
+ const normalizedResponse = normalizeFlagsResponse(response, responseLogger);
50
+ const flagDetails = normalizedResponse.flags;
51
+ const featureFlags = normalizedResponse.featureFlags;
52
+ const flagPayloads = normalizedResponse.featureFlagPayloads;
53
+ if (!featureFlags) return;
54
+ const requestId = response['requestId'];
55
+ const evaluatedAt = response['evaluatedAt'];
56
+ if (isArray(featureFlags)) {
57
+ responseLogger.warn('v1 of the feature flags endpoint is deprecated. Please use the latest version.');
58
+ const $enabled_feature_flags = {};
59
+ if (featureFlags) for(let i = 0; i < featureFlags.length; i++)$enabled_feature_flags[featureFlags[i]] = true;
60
+ return {
61
+ [PERSISTENCE_ACTIVE_FEATURE_FLAGS]: featureFlags,
62
+ [ENABLED_FEATURE_FLAGS]: $enabled_feature_flags,
63
+ [PERSISTENCE_MINIMAL_FLAG_CALLED_EVENTS]: false
64
+ };
65
+ }
66
+ let newFeatureFlags = featureFlags;
67
+ let newFeatureFlagPayloads = flagPayloads;
68
+ let newFeatureFlagDetails = flagDetails;
69
+ if (options?.partialResponse) {
70
+ const evaluatedFlagKeys = Object.keys(newFeatureFlags);
71
+ const evaluatedPayloads = newFeatureFlagPayloads || {};
72
+ newFeatureFlags = {
73
+ ...currentFlags,
74
+ ...newFeatureFlags
75
+ };
76
+ newFeatureFlagPayloads = {
77
+ ...currentFlagPayloads,
78
+ ...evaluatedPayloads
79
+ };
80
+ evaluatedFlagKeys.forEach((key)=>{
81
+ if (!(key in evaluatedPayloads)) delete newFeatureFlagPayloads?.[key];
82
+ });
83
+ newFeatureFlagDetails = {
84
+ ...currentFlagDetails,
85
+ ...newFeatureFlagDetails
86
+ };
87
+ } else if (response.errorsWhileComputingFlags) if (flagDetails) {
88
+ const successfulKeys = new Set(Object.keys(flagDetails).filter((key)=>!flagDetails[key]?.failed));
89
+ newFeatureFlags = {
90
+ ...currentFlags,
91
+ ...Object.fromEntries(Object.entries(newFeatureFlags).filter(([key])=>successfulKeys.has(key)))
92
+ };
93
+ const successfulPayloads = Object.fromEntries(Object.entries(newFeatureFlagPayloads || {}).filter(([key])=>successfulKeys.has(key)));
94
+ newFeatureFlagPayloads = {
95
+ ...currentFlagPayloads,
96
+ ...successfulPayloads
97
+ };
98
+ successfulKeys.forEach((key)=>{
99
+ if (!(key in successfulPayloads)) delete newFeatureFlagPayloads?.[key];
100
+ });
101
+ newFeatureFlagDetails = {
102
+ ...currentFlagDetails,
103
+ ...Object.fromEntries(Object.entries(newFeatureFlagDetails || {}).filter(([key])=>successfulKeys.has(key)))
104
+ };
105
+ } else {
106
+ newFeatureFlags = {
107
+ ...currentFlags,
108
+ ...newFeatureFlags
109
+ };
110
+ newFeatureFlagPayloads = {
111
+ ...currentFlagPayloads,
112
+ ...newFeatureFlagPayloads
113
+ };
114
+ newFeatureFlagDetails = {
115
+ ...currentFlagDetails,
116
+ ...newFeatureFlagDetails
117
+ };
118
+ }
119
+ return {
120
+ [PERSISTENCE_ACTIVE_FEATURE_FLAGS]: Object.keys(filterActiveFeatureFlags(newFeatureFlags)),
121
+ [ENABLED_FEATURE_FLAGS]: newFeatureFlags || {},
122
+ [PERSISTENCE_FEATURE_FLAG_PAYLOADS]: newFeatureFlagPayloads || {},
123
+ [PERSISTENCE_FEATURE_FLAG_DETAILS]: newFeatureFlagDetails || {},
124
+ [PERSISTENCE_MINIMAL_FLAG_CALLED_EVENTS]: true === response.minimalFlagCalledEvents,
125
+ ...requestId ? {
126
+ [PERSISTENCE_FEATURE_FLAG_REQUEST_ID]: requestId
127
+ } : {},
128
+ ...evaluatedAt ? {
129
+ [PERSISTENCE_FEATURE_FLAG_EVALUATED_AT]: evaluatedAt
130
+ } : {}
131
+ };
132
+ };
133
+ const normalizeFlagsResponse = (response, responseLogger)=>{
134
+ const flagDetails = response['flags'];
135
+ if (flagDetails) {
136
+ const featureFlags = Object.fromEntries(Object.keys(flagDetails).map((flag)=>[
137
+ flag,
138
+ flagDetails[flag].variant ?? flagDetails[flag].enabled
139
+ ]));
140
+ const featureFlagPayloads = Object.fromEntries(Object.keys(flagDetails).filter((flag)=>flagDetails[flag].enabled).filter((flag)=>!isUndefined(flagDetails[flag].metadata?.payload)).map((flag)=>[
141
+ flag,
142
+ flagDetails[flag].metadata?.payload
143
+ ]));
144
+ return {
145
+ ...response,
146
+ featureFlags,
147
+ featureFlagPayloads
148
+ };
149
+ }
150
+ if (response['featureFlags']) responseLogger.warn('Using an older version of the feature flags endpoint. Please upgrade your PostHog server to the latest version');
151
+ return response;
152
+ };
153
+ const QuotaLimitedResource = {
154
+ FeatureFlags: 'feature_flags',
155
+ Recordings: 'recordings'
156
+ };
157
+ class PostHogFeatureFlags {
158
+ constructor(configSource){
159
+ this.name = 'featureFlags';
160
+ this._override_warning = false;
161
+ this.featureFlagEventHandlers = [];
162
+ this._logger = logger;
163
+ this._baseEventProperties = {};
164
+ this._eventPropertiesWithFlagValues = {};
165
+ this._reloadingHandlers = [];
166
+ this._pendingReloads = new Set();
167
+ this._activeReloads = new Set();
168
+ this._hasLoadedFlags = false;
169
+ this._requestInFlight = false;
170
+ this._requestGeneration = 0;
171
+ this._reloadingDisabled = false;
172
+ this._additionalReloadRequested = false;
173
+ this._flagsLoadedFromRemote = false;
174
+ this._staleCacheRefreshTriggered = false;
175
+ this._consecutiveStatusZeroFailures = 0;
176
+ this._hadUserInteraction = false;
177
+ this._onOnline = ()=>{
178
+ const wasTripped = this._hasStatusZeroCircuitBreakerTripped();
179
+ this._consecutiveStatusZeroFailures = 0;
180
+ if (wasTripped) this.reloadFeatureFlags();
181
+ };
182
+ this._refreshIfDue = ()=>{
183
+ const refreshIntervalMs = this._refreshIntervalMs;
184
+ const dueIntervalMs = this._dueRefreshIntervalMs ?? refreshIntervalMs;
185
+ if (isUndefined(refreshIntervalMs) || isUndefined(dueIntervalMs) || this._config.remoteRequestsDisabled || !this._document || 'hidden' === this._document.visibilityState || Date.now() - (this._lastRefreshAt ?? 0) < dueIntervalMs) return;
186
+ this._dueRefreshIntervalMs = this._hadUserInteraction || !this._config.idleRefreshBackoff ? refreshIntervalMs : Math.min(2 * dueIntervalMs, MAX_IDLE_REFRESH_INTERVAL_MS);
187
+ this._hadUserInteraction = false;
188
+ this.reloadFeatureFlags();
189
+ this._scheduleNextRefresh();
190
+ };
191
+ this._onUserInteraction = ()=>{
192
+ if (this._hadUserInteraction && this._dueRefreshIntervalMs === this._refreshIntervalMs) return;
193
+ this._resumeConfiguredInterval();
194
+ };
195
+ this._onVisibilityChange = ()=>{
196
+ if (this._document?.visibilityState === 'visible') this._resumeConfiguredInterval();
197
+ };
198
+ this._configSource = configSource;
199
+ }
200
+ get _crossTabPersistence() {}
201
+ _onConfigUpdated() {
202
+ if (this._client) this._syncAutomaticRefresh();
203
+ }
204
+ setup(client) {
205
+ try {
206
+ this._window = "u" < typeof window ? void 0 : window;
207
+ this._document = "u" < typeof document ? void 0 : document;
208
+ } catch {
209
+ this._window = void 0;
210
+ this._document = void 0;
211
+ }
212
+ this._initializingClient = client;
213
+ this._logger = client.logger.createLogger('[FeatureFlags]');
214
+ return continueWith(client.kv.initialize(), ()=>{
215
+ if (this._initializingClient !== client) return;
216
+ this._initializingClient = void 0;
217
+ this._client = client;
218
+ this._finishSetup(client);
219
+ });
220
+ }
221
+ _finishSetup(client) {
222
+ if (this._client !== client) return;
223
+ if (this._window) addEventListener(this._window, 'online', this._onOnline);
224
+ this._syncAutomaticRefresh();
225
+ this._dynamicProperties = client.registerDynamicEventProperties(()=>this._isCacheStale() ? this._baseEventProperties : this._eventPropertiesWithFlagValues);
226
+ this._crossTabPersistenceUnsubscribe = this._crossTabPersistence?.onCrossTabFeatureFlagChange(()=>{
227
+ this._clearBootstrapState();
228
+ this._rebuildEventProperties();
229
+ this._fireFeatureFlagsCallbacks();
230
+ });
231
+ this._rebuildEventProperties();
232
+ return this.initialize();
233
+ }
234
+ _resumeConfiguredInterval() {
235
+ this._hadUserInteraction = true;
236
+ this._dueRefreshIntervalMs = this._refreshIntervalMs;
237
+ this._refreshIfDue();
238
+ }
239
+ _syncAutomaticRefresh() {
240
+ const configuredIntervalMs = this._config.refreshIntervalMs;
241
+ const refreshIntervalMs = !this._config.remoteRequestsDisabled && this._document && !isUndefined(configuredIntervalMs) && configuredIntervalMs > 0 ? configuredIntervalMs : void 0;
242
+ if (!this._config.idleRefreshBackoff) this._dueRefreshIntervalMs = refreshIntervalMs;
243
+ if (refreshIntervalMs === this._refreshIntervalMs) return;
244
+ this._stopAutomaticRefresh();
245
+ if (isUndefined(refreshIntervalMs)) return;
246
+ this._refreshIntervalMs = refreshIntervalMs;
247
+ this._dueRefreshIntervalMs = refreshIntervalMs;
248
+ this._scheduleNextRefresh();
249
+ if (this._document?.addEventListener) {
250
+ addEventListener(this._document, DOM_EVENT_VISIBILITYCHANGE, this._onVisibilityChange);
251
+ eachArray(USER_INTERACTION_EVENTS, (eventName)=>{
252
+ addEventListener(this._document, eventName, this._onUserInteraction, {
253
+ capture: true
254
+ });
255
+ });
256
+ }
257
+ }
258
+ _scheduleNextRefresh() {
259
+ if (isUndefined(this._refreshIntervalMs)) return;
260
+ if (!isUndefined(this._refreshInterval)) clearInterval(this._refreshInterval);
261
+ this._lastRefreshAt = Date.now();
262
+ this._refreshInterval = setInterval(this._refreshIfDue, this._refreshIntervalMs);
263
+ }
264
+ _stopAutomaticRefresh() {
265
+ if (!isUndefined(this._refreshInterval)) {
266
+ clearInterval(this._refreshInterval);
267
+ this._refreshInterval = void 0;
268
+ this._document?.removeEventListener?.(DOM_EVENT_VISIBILITYCHANGE, this._onVisibilityChange);
269
+ eachArray(USER_INTERACTION_EVENTS, (eventName)=>{
270
+ this._document?.removeEventListener?.(eventName, this._onUserInteraction, {
271
+ capture: true
272
+ });
273
+ });
274
+ }
275
+ this._refreshIntervalMs = void 0;
276
+ this._dueRefreshIntervalMs = void 0;
277
+ this._hadUserInteraction = false;
278
+ this._lastRefreshAt = void 0;
279
+ }
280
+ destroy() {
281
+ this._dispose();
282
+ }
283
+ dispose() {
284
+ this._dispose();
285
+ }
286
+ _dispose() {
287
+ this._stopAutomaticRefresh();
288
+ this._requestGeneration++;
289
+ this._additionalReloadRequested = false;
290
+ this._initializingClient = void 0;
291
+ this._settleReloads(this._pendingReloads, 'cancelled');
292
+ this._settleReloads(this._activeReloads, 'cancelled');
293
+ if (!this._client) return;
294
+ this._clearDebouncer();
295
+ this._dynamicProperties?.dispose();
296
+ this._dynamicProperties = void 0;
297
+ this._crossTabPersistenceUnsubscribe?.();
298
+ this._crossTabPersistenceUnsubscribe = void 0;
299
+ this._reloadingHandlers = [];
300
+ this._window?.removeEventListener('online', this._onOnline);
301
+ this._client = void 0;
302
+ }
303
+ get _config() {
304
+ return this._configSource.get();
305
+ }
306
+ _prop(key) {
307
+ if (this._bootstrapState && key in this._bootstrapState) return this._bootstrapState[key];
308
+ return this._client?.kv.get(key);
309
+ }
310
+ _clearBootstrapState() {
311
+ if (!this._bootstrapState) return false;
312
+ this._bootstrapState = void 0;
313
+ return true;
314
+ }
315
+ _fallBackToPersistedFlags() {
316
+ if (isUndefined(this._client?.kv.get(ENABLED_FEATURE_FLAGS))) return false;
317
+ return this._clearBootstrapState();
318
+ }
319
+ _set(properties) {
320
+ this._persist(()=>this._client?.kv.set(properties));
321
+ }
322
+ _markCrossTabFeatureFlagSnapshot(statePatch, response, partialResponse) {
323
+ if (!this._crossTabPersistence || !statePatch[ENABLED_FEATURE_FLAGS]) return;
324
+ const previousFlags = this._prop(ENABLED_FEATURE_FLAGS) || {};
325
+ const nextFlags = statePatch[ENABLED_FEATURE_FLAGS] || {};
326
+ const responseFlags = response.flags || response.featureFlags;
327
+ const successfulResponseFlagKeys = response.flags ? Object.entries(response.flags).filter(([, detail])=>!detail?.failed).map(([key])=>key) : [];
328
+ const isIncompleteResponse = !!response.errorsWhileComputingFlags && !!response.flags;
329
+ const ownedFlagKeys = isIncompleteResponse ? successfulResponseFlagKeys : partialResponse && !isArray(responseFlags) ? Object.keys(responseFlags || {}) : Array.from(new Set([
330
+ ...Object.keys(previousFlags),
331
+ ...Object.keys(nextFlags)
332
+ ]));
333
+ const previousPayloads = this._prop(PERSISTENCE_FEATURE_FLAG_PAYLOADS) || {};
334
+ const nextPayloads = statePatch[PERSISTENCE_FEATURE_FLAG_PAYLOADS] || {};
335
+ const ownedPayloadKeys = isIncompleteResponse || partialResponse ? ownedFlagKeys : Array.from(new Set([
336
+ ...Object.keys(previousPayloads),
337
+ ...Object.keys(nextPayloads)
338
+ ]));
339
+ const ownsCompleteSnapshot = !isIncompleteResponse && !partialResponse;
340
+ const flagOwnership = ownsCompleteSnapshot ? true : ownedFlagKeys;
341
+ const payloadOwnership = ownsCompleteSnapshot ? true : ownedPayloadKeys;
342
+ const changes = {
343
+ [PERSISTENCE_ACTIVE_FEATURE_FLAGS]: flagOwnership,
344
+ [ENABLED_FEATURE_FLAGS]: flagOwnership,
345
+ [PERSISTENCE_FEATURE_FLAG_PAYLOADS]: payloadOwnership
346
+ };
347
+ if (statePatch[PERSISTENCE_FEATURE_FLAG_DETAILS]) changes[PERSISTENCE_FEATURE_FLAG_DETAILS] = flagOwnership;
348
+ for (const key of [
349
+ PERSISTENCE_FEATURE_FLAG_REQUEST_ID,
350
+ PERSISTENCE_FEATURE_FLAG_EVALUATED_AT,
351
+ PERSISTENCE_MINIMAL_FLAG_CALLED_EVENTS
352
+ ])if (!isUndefined(statePatch[key])) changes[key] = true;
353
+ this._crossTabPersistence.markCrossTabFeatureFlagChanges(changes);
354
+ }
355
+ _remove(keys) {
356
+ this._persist(()=>this._client?.kv.remove(keys));
357
+ }
358
+ _persist(operation) {
359
+ try {
360
+ operation();
361
+ } catch (error) {
362
+ this._logger.error('Failed to update feature flag persistence', error);
363
+ }
364
+ }
365
+ _rebuildEventProperties() {
366
+ const common = {};
367
+ for (const key of [
368
+ PERSISTENCE_ACTIVE_FEATURE_FLAGS,
369
+ PERSISTENCE_FEATURE_FLAG_PAYLOADS,
370
+ PERSISTENCE_FEATURE_FLAG_REQUEST_ID,
371
+ PERSISTENCE_OVERRIDE_FEATURE_FLAGS
372
+ ]){
373
+ const value = this._prop(key);
374
+ if (!isUndefined(value)) common[key] = value;
375
+ }
376
+ this._baseEventProperties = common;
377
+ const withFlagValues = {
378
+ ...common
379
+ };
380
+ const flags = this._prop(ENABLED_FEATURE_FLAGS);
381
+ if (flags) for (const [key, value] of Object.entries(flags))withFlagValues[`$feature/${key}`] = value;
382
+ this._eventPropertiesWithFlagValues = withFlagValues;
383
+ }
384
+ _isCacheStale() {
385
+ const ttl = this._config.cacheTtlMs;
386
+ if (!ttl || ttl <= 0) return false;
387
+ const evaluatedAt = this._prop(PERSISTENCE_FEATURE_FLAG_EVALUATED_AT);
388
+ return 'number' != typeof evaluatedAt || Date.now() - evaluatedAt > ttl;
389
+ }
390
+ _checkAndTriggerStaleRefresh() {
391
+ if (!this._isCacheStale()) return false;
392
+ if (!this._staleCacheRefreshTriggered && !this._requestInFlight) {
393
+ this._staleCacheRefreshTriggered = true;
394
+ this._logger.warn('Feature flag cache is stale, triggering refresh...');
395
+ this.reloadFeatureFlags();
396
+ }
397
+ return true;
398
+ }
399
+ _getValidEvaluationEnvironments() {
400
+ const envs = this._config.evaluationContexts;
401
+ if (!envs?.length) return [];
402
+ return envs.filter((env)=>{
403
+ const isValid = env && 'string' == typeof env && env.trim().length > 0;
404
+ if (!isValid) this._logger.error('Invalid evaluation context found:', env, 'Expected non-empty string');
405
+ return isValid;
406
+ });
407
+ }
408
+ _getValidFlagKeys() {
409
+ const flagKeys = this._config.flagKeys;
410
+ if (isUndefined(flagKeys)) return;
411
+ return flagKeys.filter((flagKey)=>{
412
+ const isValid = flagKey && 'string' == typeof flagKey && flagKey.trim().length > 0;
413
+ if (!isValid) this._logger.error('Invalid flag key found:', flagKey, 'Expected non-empty string');
414
+ return isValid;
415
+ });
416
+ }
417
+ initialize() {
418
+ const config = this._config;
419
+ const bootstrapFlags = config.bootstrap?.featureFlags ?? {};
420
+ const hasBootstrappedFlags = Object.keys(bootstrapFlags).length;
421
+ if (hasBootstrappedFlags) {
422
+ const bootstrapPayloads = config.bootstrap?.featureFlagPayloads ?? {};
423
+ const featureFlags = Object.keys(bootstrapFlags).filter((flag)=>!isUndefined(bootstrapFlags[flag])).reduce((res, key)=>{
424
+ res[key] = bootstrapFlags[key];
425
+ return res;
426
+ }, {});
427
+ const featureFlagPayloads = Object.keys(bootstrapPayloads).filter((key)=>featureFlags[key]).reduce((res, key)=>{
428
+ res[key] = bootstrapPayloads[key];
429
+ return res;
430
+ }, {});
431
+ return this._receivedFeatureFlags({
432
+ featureFlags,
433
+ featureFlagPayloads
434
+ }, void 0, {
435
+ persist: false
436
+ });
437
+ }
438
+ }
439
+ updateFlags(flags, payloads, options) {
440
+ const existingFlags = options?.merge ? this._prop(ENABLED_FEATURE_FLAGS) ?? {} : {};
441
+ const existingPayloads = options?.merge ? this._prop(PERSISTENCE_FEATURE_FLAG_PAYLOADS) ?? {} : {};
442
+ const finalFlags = {
443
+ ...existingFlags,
444
+ ...flags
445
+ };
446
+ const finalPayloads = {
447
+ ...existingPayloads,
448
+ ...payloads
449
+ };
450
+ const flagDetails = {};
451
+ for (const [key, value] of Object.entries(finalFlags))flagDetails[key] = {
452
+ key,
453
+ enabled: getEnabledFromValue(value),
454
+ variant: getVariantFromValue(value),
455
+ reason: void 0,
456
+ metadata: isUndefined(finalPayloads?.[key]) ? void 0 : {
457
+ id: 0,
458
+ version: void 0,
459
+ description: void 0,
460
+ payload: finalPayloads[key]
461
+ }
462
+ };
463
+ this._receivedFeatureFlags({
464
+ flags: flagDetails
465
+ });
466
+ }
467
+ get hasLoadedFlags() {
468
+ return this._hasLoadedFlags;
469
+ }
470
+ getFlags() {
471
+ return Object.keys(this.getFlagVariants());
472
+ }
473
+ getFlagsWithDetails() {
474
+ const flagDetails = this._prop(PERSISTENCE_FEATURE_FLAG_DETAILS);
475
+ const overridenFlags = this._prop(PERSISTENCE_OVERRIDE_FEATURE_FLAGS);
476
+ const overriddenPayloads = this._prop(PERSISTENCE_OVERRIDE_FEATURE_FLAG_PAYLOADS);
477
+ if (!overriddenPayloads && !overridenFlags) return flagDetails || {};
478
+ const finalDetails = extend({}, flagDetails || {});
479
+ const overriddenKeys = [
480
+ ...new Set([
481
+ ...Object.keys(overriddenPayloads || {}),
482
+ ...Object.keys(overridenFlags || {})
483
+ ])
484
+ ];
485
+ for (const key of overriddenKeys){
486
+ const originalDetail = finalDetails[key];
487
+ const overrideFlagValue = overridenFlags?.[key];
488
+ const finalEnabled = isUndefined(overrideFlagValue) ? originalDetail?.enabled ?? false : !!overrideFlagValue;
489
+ const overrideVariant = isUndefined(overrideFlagValue) ? originalDetail?.variant : 'string' == typeof overrideFlagValue ? overrideFlagValue : void 0;
490
+ const overridePayload = overriddenPayloads?.[key];
491
+ const overridenDetail = {
492
+ ...originalDetail,
493
+ enabled: finalEnabled,
494
+ variant: finalEnabled ? overrideVariant ?? originalDetail?.variant : void 0
495
+ };
496
+ if (finalEnabled !== originalDetail?.enabled) overridenDetail.original_enabled = originalDetail?.enabled;
497
+ if (overrideVariant !== originalDetail?.variant) overridenDetail.original_variant = originalDetail?.variant;
498
+ if (overridePayload) overridenDetail.metadata = {
499
+ ...originalDetail?.metadata,
500
+ payload: overridePayload,
501
+ original_payload: originalDetail?.metadata?.payload
502
+ };
503
+ finalDetails[key] = overridenDetail;
504
+ }
505
+ if (!this._override_warning) {
506
+ this._logger.warn(' Overriding feature flag details!', {
507
+ flagDetails,
508
+ overriddenPayloads,
509
+ finalDetails
510
+ });
511
+ this._override_warning = true;
512
+ }
513
+ return finalDetails;
514
+ }
515
+ getAllFeatureFlags() {
516
+ const flagVariants = this.getFlagVariants();
517
+ const payloads = this.getFlagPayloads();
518
+ return Object.keys(flagVariants).map((key)=>{
519
+ const flagValue = flagVariants[key];
520
+ return {
521
+ key,
522
+ enabled: getEnabledFromValue(flagValue),
523
+ variant: getVariantFromValue(flagValue),
524
+ payload: parsePayload(payloads[key])
525
+ };
526
+ });
527
+ }
528
+ getFlagVariants() {
529
+ const enabledFlags = this._prop(ENABLED_FEATURE_FLAGS);
530
+ const overriddenFlags = this._prop(PERSISTENCE_OVERRIDE_FEATURE_FLAGS);
531
+ if (!overriddenFlags) return enabledFlags || {};
532
+ const finalFlags = extend({}, enabledFlags || {});
533
+ const overriddenKeys = Object.keys(overriddenFlags);
534
+ for(let i = 0; i < overriddenKeys.length; i++)finalFlags[overriddenKeys[i]] = overriddenFlags[overriddenKeys[i]];
535
+ if (!this._override_warning) {
536
+ this._logger.warn(' Overriding feature flags!', {
537
+ enabledFlags,
538
+ overriddenFlags,
539
+ finalFlags
540
+ });
541
+ this._override_warning = true;
542
+ }
543
+ return finalFlags;
544
+ }
545
+ getFlagPayloads() {
546
+ const flagPayloads = this._prop(PERSISTENCE_FEATURE_FLAG_PAYLOADS);
547
+ const overriddenPayloads = this._prop(PERSISTENCE_OVERRIDE_FEATURE_FLAG_PAYLOADS);
548
+ if (!overriddenPayloads) return flagPayloads || {};
549
+ const finalPayloads = extend({}, flagPayloads || {});
550
+ const overriddenKeys = Object.keys(overriddenPayloads);
551
+ for(let i = 0; i < overriddenKeys.length; i++)finalPayloads[overriddenKeys[i]] = overriddenPayloads[overriddenKeys[i]];
552
+ if (!this._override_warning) {
553
+ this._logger.warn(' Overriding feature flag payloads!', {
554
+ flagPayloads,
555
+ overriddenPayloads,
556
+ finalPayloads
557
+ });
558
+ this._override_warning = true;
559
+ }
560
+ return finalPayloads;
561
+ }
562
+ reloadFeatureFlags() {
563
+ if (this._reloadingDisabled || this._config.featureFlagsDisabled) return;
564
+ if (this._hasStatusZeroCircuitBreakerTripped()) return;
565
+ if (this._requestInFlight) this._additionalReloadRequested = true;
566
+ if (this._reloadDebouncer) return;
567
+ this._reloadingHandlers.slice().forEach((handler)=>{
568
+ try {
569
+ handler();
570
+ } catch (error) {
571
+ this._logger.error('Error while running feature flags reloading callback', error);
572
+ }
573
+ });
574
+ this._reloadDebouncer = setTimeout(()=>{
575
+ this._callFlagsEndpoint();
576
+ }, 5);
577
+ }
578
+ reloadFeatureFlagsAsync() {
579
+ if (!this._client || this._reloadingDisabled || this._config.featureFlagsDisabled || this._config.remoteRequestsDisabled || this._hasStatusZeroCircuitBreakerTripped()) return Promise.resolve({
580
+ status: 'skipped'
581
+ });
582
+ return new Promise((resolve)=>{
583
+ this._pendingReloads.add(resolve);
584
+ this.reloadFeatureFlags();
585
+ });
586
+ }
587
+ _settleReloads(completions, status) {
588
+ completions.forEach((complete)=>complete({
589
+ status
590
+ }));
591
+ completions.clear();
592
+ }
593
+ _clearDebouncer() {
594
+ clearTimeout(this._reloadDebouncer);
595
+ this._reloadDebouncer = void 0;
596
+ }
597
+ onReloading(handler) {
598
+ this._reloadingHandlers.push(handler);
599
+ return ()=>{
600
+ this._reloadingHandlers = this._reloadingHandlers.filter((entry)=>entry !== handler);
601
+ };
602
+ }
603
+ ensureFlagsLoaded() {
604
+ if (this._hasLoadedFlags || this._requestInFlight || this._reloadDebouncer) return;
605
+ this.reloadFeatureFlags();
606
+ }
607
+ setAnonymousDistinctId(anon_distinct_id) {
608
+ this.$anon_distinct_id = anon_distinct_id;
609
+ }
610
+ setReloadingPaused(isPaused) {
611
+ this._reloadingDisabled = isPaused;
612
+ }
613
+ resetFlagCallReported() {
614
+ this._remove(FLAG_CALL_REPORTED);
615
+ }
616
+ _callFlagsEndpoint(options) {
617
+ this._clearDebouncer();
618
+ const client = this._client;
619
+ if (!client || this._config.remoteRequestsDisabled || this._hasStatusZeroCircuitBreakerTripped()) return void this._settleReloads(this._pendingReloads, 'skipped');
620
+ if (this._requestInFlight) {
621
+ this._additionalReloadRequested = true;
622
+ return;
623
+ }
624
+ const data = {
625
+ token: client.projectToken,
626
+ distinct_id: client.distinctId,
627
+ groups: client.groups,
628
+ $anon_distinct_id: this.$anon_distinct_id,
629
+ person_properties: {
630
+ ...client.initialPersonProperties,
631
+ ...this._prop(STORED_PERSON_PROPERTIES_KEY) || {},
632
+ $lib: client.library.name,
633
+ $lib_version: client.library.version
634
+ },
635
+ group_properties: this._prop(STORED_GROUP_PROPERTIES_KEY),
636
+ timezone: getTimezone()
637
+ };
638
+ if (!isUndefined(client.deviceId)) data.$device_id = client.deviceId;
639
+ if (options?.disableFlags || this._config.featureFlagsDisabled) data.disable_flags = true;
640
+ const evaluationContexts = this._getValidEvaluationEnvironments();
641
+ if (evaluationContexts.length) data.evaluation_contexts = evaluationContexts;
642
+ const flagKeys = this._getValidFlagKeys();
643
+ if (!isUndefined(flagKeys)) data.flag_keys = flagKeys;
644
+ const isPartialFlagsResponse = this._config.onlyEvaluateSurveyFeatureFlags;
645
+ const path = `/flags/?v=2${isPartialFlagsResponse ? '&only_evaluate_survey_feature_flags=true' : ''}`;
646
+ const requestGeneration = this._requestGeneration;
647
+ const completions = this._pendingReloads;
648
+ this._pendingReloads = new Set();
649
+ this._activeReloads = completions;
650
+ this._requestInFlight = true;
651
+ const requestAdditionalReload = ()=>{
652
+ if (this._additionalReloadRequested) {
653
+ this._additionalReloadRequested = false;
654
+ this._callFlagsEndpoint();
655
+ }
656
+ };
657
+ const handleResponse = (response)=>{
658
+ const json = response.json ?? {};
659
+ const errorsLoading = 200 !== response.statusCode;
660
+ this._requestInFlight = false;
661
+ if (requestGeneration !== this._requestGeneration) return void requestAdditionalReload();
662
+ this._trackStatusZeroReachability(response.statusCode);
663
+ if (!errorsLoading && !this._additionalReloadRequested) this.$anon_distinct_id = void 0;
664
+ if (data.disable_flags && !this._additionalReloadRequested) return void this._settleReloads(completions, 'skipped');
665
+ this._flagsLoadedFromRemote = !errorsLoading;
666
+ const flagErrors = [];
667
+ if (response.error) flagErrors.push(response.error instanceof Error && 'AbortError' === response.error.name ? FeatureFlagError.TIMEOUT : response.error instanceof Error ? FeatureFlagError.CONNECTION_ERROR : FeatureFlagError.UNKNOWN_ERROR);
668
+ else if (200 !== response.statusCode) flagErrors.push(FeatureFlagError.apiError(response.statusCode));
669
+ if (json.errorsWhileComputingFlags) flagErrors.push(FeatureFlagError.ERRORS_WHILE_COMPUTING);
670
+ const isQuotaLimited = !!json.quotaLimited?.includes(QuotaLimitedResource.FeatureFlags);
671
+ if (isQuotaLimited) flagErrors.push(FeatureFlagError.QUOTA_LIMITED);
672
+ this._set({
673
+ [PERSISTENCE_FEATURE_FLAG_ERRORS]: flagErrors
674
+ });
675
+ if (isQuotaLimited) this._logger.warn('You have hit your feature flags quota limit, and will not be able to load feature flags until the quota is reset. Please visit https://posthog.com/docs/billing/limits-alerts to learn more.');
676
+ else if (!data.disable_flags) this._receivedFeatureFlags(json, errorsLoading, {
677
+ partialResponse: isPartialFlagsResponse
678
+ });
679
+ this._settleReloads(completions, data.disable_flags ? 'skipped' : !flagErrors.length && (json.flags || json.featureFlags) ? 'loaded' : 'error');
680
+ requestAdditionalReload();
681
+ };
682
+ const handleError = (error)=>{
683
+ this._requestInFlight = false;
684
+ if (requestGeneration !== this._requestGeneration) return void requestAdditionalReload();
685
+ this._set({
686
+ [PERSISTENCE_FEATURE_FLAG_ERRORS]: [
687
+ FeatureFlagError.CONNECTION_ERROR
688
+ ]
689
+ });
690
+ this._logger.error('Feature flag request failed', error);
691
+ if (this._fallBackToPersistedFlags()) this._fireFeatureFlagsCallbacks(true);
692
+ this._settleReloads(completions, 'error');
693
+ requestAdditionalReload();
694
+ };
695
+ let attemptsLeft = this._config.requestMaxRetries ?? 1;
696
+ const shouldRetry = ()=>attemptsLeft > 0 && requestGeneration === this._requestGeneration;
697
+ const attempt = ()=>{
698
+ if (requestGeneration !== this._requestGeneration || !this._client || this._config.remoteRequestsDisabled) {
699
+ this._requestInFlight = false;
700
+ this._settleReloads(completions, 'skipped');
701
+ requestAdditionalReload();
702
+ return;
703
+ }
704
+ const retryLater = ()=>{
705
+ attemptsLeft--;
706
+ setTimeout(attempt, FLAGS_RETRY_DELAY_MS);
707
+ };
708
+ try {
709
+ client.sendRequest(path, {
710
+ target: 'flags',
711
+ method: 'POST',
712
+ body: data,
713
+ compression: this._config.compression,
714
+ sentAt: 'body',
715
+ timeoutMs: this._config.requestTimeoutMs
716
+ }).then((response)=>{
717
+ if (isRetryableFlagsResponse(response) && shouldRetry()) return void retryLater();
718
+ handleResponse(response);
719
+ }).catch(handleError);
720
+ } catch (error) {
721
+ handleError(error);
722
+ }
723
+ };
724
+ attempt();
725
+ }
726
+ _hasStatusZeroCircuitBreakerTripped() {
727
+ return isStatusZeroFailureCircuitBreakerTripped(this._consecutiveStatusZeroFailures, MAX_CONSECUTIVE_FLAGS_STATUS_ZERO_FAILURES);
728
+ }
729
+ _trackStatusZeroReachability(statusCode) {
730
+ this._consecutiveStatusZeroFailures = updateStatusZeroFailureCount(statusCode, this._consecutiveStatusZeroFailures, MAX_CONSECUTIVE_FLAGS_STATUS_ZERO_FAILURES, ()=>this._logger.warn('Feature flag requests are failing before receiving an HTTP response; this can happen due to network issues, CORS, browser blocking, or ad blockers. Stopped refreshing feature flags; will try again when connectivity changes.'));
731
+ }
732
+ getFeatureFlag(key, options = {}) {
733
+ if (options.fresh && !this._flagsLoadedFromRemote) return;
734
+ if (!this._hasLoadedFlags && !(this.getFlags() && this.getFlags().length > 0)) return void this._logger.warn('getFeatureFlag for key "' + key + FLAG_TIMEOUT_MSG);
735
+ if (this._checkAndTriggerStaleRefresh()) return;
736
+ const result = this.getFeatureFlagResult(key, options);
737
+ return result?.variant ?? result?.enabled;
738
+ }
739
+ getFeatureFlagDetails(key) {
740
+ const details = this.getFlagsWithDetails();
741
+ return details[key];
742
+ }
743
+ getFeatureFlagPayload(key) {
744
+ const result = this.getFeatureFlagResult(key, {
745
+ send_event: false
746
+ });
747
+ return result?.payload;
748
+ }
749
+ getFeatureFlagResult(key, options = {}) {
750
+ if (options.fresh && !this._flagsLoadedFromRemote) return;
751
+ if (!this._hasLoadedFlags && !(this.getFlags() && this.getFlags().length > 0)) return void this._logger.warn('getFeatureFlagResult for key "' + key + FLAG_TIMEOUT_MSG);
752
+ if (this._checkAndTriggerStaleRefresh()) return;
753
+ const flagVariants = this.getFlagVariants();
754
+ const flagExists = key in flagVariants;
755
+ const flagValue = flagVariants[key];
756
+ const payloads = this.getFlagPayloads();
757
+ const payload = payloads[key];
758
+ const flagReportValue = String(flagValue);
759
+ const requestId = this._prop(PERSISTENCE_FEATURE_FLAG_REQUEST_ID) || void 0;
760
+ const evaluatedAt = this._prop(PERSISTENCE_FEATURE_FLAG_EVALUATED_AT) || void 0;
761
+ let flagCallReported = this._prop(FLAG_CALL_REPORTED) || {};
762
+ let sessionIdToPersist;
763
+ if (this._config.deduplicateCallsPerSession) {
764
+ const currentSessionId = this._client?.session.sessionId;
765
+ const storedSessionId = this._prop(FLAG_CALL_REPORTED_SESSION_ID);
766
+ if (currentSessionId && currentSessionId !== storedSessionId) {
767
+ flagCallReported = {};
768
+ sessionIdToPersist = currentSessionId;
769
+ }
770
+ }
771
+ if (!options.send_event && 'send_event' in options) {
772
+ if (sessionIdToPersist) this._set({
773
+ [FLAG_CALL_REPORTED]: flagCallReported,
774
+ [FLAG_CALL_REPORTED_SESSION_ID]: sessionIdToPersist
775
+ });
776
+ } else if (key in flagCallReported && flagCallReported[key].includes(flagReportValue)) {
777
+ if (sessionIdToPersist) this._set({
778
+ [FLAG_CALL_REPORTED]: flagCallReported,
779
+ [FLAG_CALL_REPORTED_SESSION_ID]: sessionIdToPersist
780
+ });
781
+ } else {
782
+ if (isArray(flagCallReported[key])) flagCallReported[key].push(flagReportValue);
783
+ else flagCallReported[key] = [
784
+ flagReportValue
785
+ ];
786
+ this._set({
787
+ [FLAG_CALL_REPORTED]: flagCallReported,
788
+ ...sessionIdToPersist ? {
789
+ [FLAG_CALL_REPORTED_SESSION_ID]: sessionIdToPersist
790
+ } : {}
791
+ });
792
+ const flagDetails = this.getFeatureFlagDetails(key);
793
+ const errors = [
794
+ ...this._prop(PERSISTENCE_FEATURE_FLAG_ERRORS) ?? []
795
+ ];
796
+ if (isUndefined(flagValue)) errors.push(FeatureFlagError.FLAG_MISSING);
797
+ const properties = {
798
+ $feature_flag: key,
799
+ $feature_flag_response: flagValue,
800
+ $feature_flag_payload: payload ?? null,
801
+ $feature_flag_request_id: requestId,
802
+ $feature_flag_evaluated_at: evaluatedAt,
803
+ $feature_flag_bootstrapped_response: this._config.bootstrap?.featureFlags?.[key] ?? null,
804
+ $feature_flag_bootstrapped_payload: this._config.bootstrap?.featureFlagPayloads?.[key] ?? null,
805
+ $used_bootstrap_value: !this._flagsLoadedFromRemote
806
+ };
807
+ if (!isUndefined(flagDetails?.metadata?.has_experiment)) properties.$feature_flag_has_experiment = flagDetails.metadata.has_experiment;
808
+ if (!isUndefined(flagDetails?.metadata?.version)) properties.$feature_flag_version = flagDetails.metadata.version;
809
+ const reason = flagDetails?.reason?.description ?? flagDetails?.reason?.code;
810
+ if (reason) properties.$feature_flag_reason = reason;
811
+ if (flagDetails?.metadata?.id) properties.$feature_flag_id = flagDetails.metadata.id;
812
+ if (!isUndefined(flagDetails?.original_variant) || !isUndefined(flagDetails?.original_enabled)) properties.$feature_flag_original_response = isUndefined(flagDetails.original_variant) ? flagDetails.original_enabled : flagDetails.original_variant;
813
+ if (flagDetails?.metadata?.original_payload) properties.$feature_flag_original_payload = flagDetails?.metadata?.original_payload;
814
+ if (errors.length) properties.$feature_flag_error = errors.join(',');
815
+ this._captureFeatureFlagCalled(properties);
816
+ }
817
+ if (!flagExists) return;
818
+ return {
819
+ key,
820
+ enabled: !!flagValue,
821
+ variant: 'string' == typeof flagValue ? flagValue : void 0,
822
+ payload: parsePayload(payload)
823
+ };
824
+ }
825
+ _captureFeatureFlagCalled(properties) {
826
+ try {
827
+ this._client?.capture('$feature_flag_called', properties);
828
+ } catch (error) {
829
+ this._logger.error('Failed to capture feature flag call', error);
830
+ }
831
+ }
832
+ getRemoteConfigPayload(key, callback) {
833
+ this._getRemoteConfigPayload(key, callback);
834
+ }
835
+ async _getRemoteConfigPayload(key, callback) {
836
+ const client = this._client;
837
+ if (!client) return;
838
+ const data = {
839
+ distinct_id: client.distinctId,
840
+ token: client.projectToken,
841
+ person_properties: {
842
+ $lib: client.library.name,
843
+ $lib_version: client.library.version
844
+ }
845
+ };
846
+ const evaluationContexts = this._getValidEvaluationEnvironments();
847
+ if (evaluationContexts.length) data.evaluation_contexts = evaluationContexts;
848
+ const flagKeys = this._getValidFlagKeys();
849
+ if (!isUndefined(flagKeys)) data.flag_keys = flagKeys;
850
+ let payload;
851
+ try {
852
+ const response = await client.sendRequest('/flags/?v=2', {
853
+ target: 'flags',
854
+ method: 'POST',
855
+ body: data,
856
+ compression: this._config.compression,
857
+ sentAt: 'body',
858
+ timeoutMs: this._config.requestTimeoutMs
859
+ });
860
+ const payloads = response.json?.featureFlagPayloads;
861
+ payload = payloads?.[key] || void 0;
862
+ } catch (error) {
863
+ this._logger.error('Remote config feature flag request failed', error);
864
+ return;
865
+ }
866
+ try {
867
+ callback(payload);
868
+ } catch (error) {
869
+ this._logger.error('Remote config feature flag callback failed', error);
870
+ }
871
+ }
872
+ isFeatureEnabled(key, options = {}) {
873
+ if (options.fresh && !this._flagsLoadedFromRemote) return options.defaultValue;
874
+ if (!this._hasLoadedFlags && !(this.getFlags() && this.getFlags().length > 0)) {
875
+ this._logger.warn('isFeatureEnabled for key "' + key + FLAG_TIMEOUT_MSG);
876
+ return options.defaultValue;
877
+ }
878
+ const flagValue = this.getFeatureFlag(key, options);
879
+ return isUndefined(flagValue) ? options.defaultValue : !!flagValue;
880
+ }
881
+ addFeatureFlagsHandler(handler) {
882
+ this.featureFlagEventHandlers.push(handler);
883
+ }
884
+ removeFeatureFlagsHandler(handler) {
885
+ this.featureFlagEventHandlers = this.featureFlagEventHandlers.filter((h)=>h !== handler);
886
+ }
887
+ receivedFeatureFlags(response, errorsLoading, options) {
888
+ this._receivedFeatureFlags(response, errorsLoading, options);
889
+ }
890
+ _receivedFeatureFlags(response, errorsLoading, options) {
891
+ if (!this._client) return;
892
+ this._hasLoadedFlags = true;
893
+ const currentFlags = this.getFlagVariants();
894
+ const currentFlagPayloads = this.getFlagPayloads();
895
+ const currentFlagDetails = this.getFlagsWithDetails();
896
+ const statePatch = parseFlagsResponse(response, currentFlags, currentFlagPayloads, currentFlagDetails, options, this._logger);
897
+ if (statePatch) if (options?.persist === false) {
898
+ const hasPersistedFlags = !isUndefined(this._client.kv.get(ENABLED_FEATURE_FLAGS));
899
+ this._bootstrapState = statePatch;
900
+ if (!hasPersistedFlags) this._set(statePatch);
901
+ } else {
902
+ this._clearBootstrapState();
903
+ this._markCrossTabFeatureFlagSnapshot(statePatch, response, !!options?.partialResponse);
904
+ this._set(statePatch);
905
+ }
906
+ else if (errorsLoading) this._fallBackToPersistedFlags();
907
+ if (!errorsLoading) this._staleCacheRefreshTriggered = false;
908
+ this._fireFeatureFlagsCallbacks(errorsLoading);
909
+ }
910
+ override(flags, suppressWarning = false) {
911
+ this._logger.warn('override is deprecated. Please use overrideFeatureFlags instead.');
912
+ this.overrideFeatureFlags({
913
+ flags: flags,
914
+ suppressWarning: suppressWarning
915
+ });
916
+ }
917
+ overrideFeatureFlags(overrideOptions) {
918
+ this._overrideFeatureFlags(overrideOptions);
919
+ }
920
+ _overrideFeatureFlags(overrideOptions) {
921
+ if (!this._client) return void this._logger.warn('posthog.featureFlags.overrideFeatureFlags called before feature flags were ready');
922
+ if (false === overrideOptions) {
923
+ this._remove([
924
+ PERSISTENCE_OVERRIDE_FEATURE_FLAGS,
925
+ PERSISTENCE_OVERRIDE_FEATURE_FLAG_PAYLOADS
926
+ ]);
927
+ this._fireFeatureFlagsCallbacks();
928
+ forceDebugLogger.info('All overrides cleared');
929
+ return;
930
+ }
931
+ if (isArray(overrideOptions)) {
932
+ this._set({
933
+ [PERSISTENCE_OVERRIDE_FEATURE_FLAGS]: arrayToFlagsRecord(overrideOptions)
934
+ });
935
+ this._fireFeatureFlagsCallbacks();
936
+ forceDebugLogger.info('Flag overrides set', {
937
+ flags: overrideOptions
938
+ });
939
+ return;
940
+ }
941
+ if (overrideOptions && 'object' == typeof overrideOptions && ('flags' in overrideOptions || 'payloads' in overrideOptions)) {
942
+ const options = overrideOptions;
943
+ this._override_warning = Boolean(options.suppressWarning ?? false);
944
+ const statePatch = {};
945
+ const flags = options.flags;
946
+ const payloads = options.payloads;
947
+ if (flags) statePatch[PERSISTENCE_OVERRIDE_FEATURE_FLAGS] = isArray(flags) ? arrayToFlagsRecord(flags) : flags;
948
+ if (payloads) statePatch[PERSISTENCE_OVERRIDE_FEATURE_FLAG_PAYLOADS] = payloads;
949
+ if (Object.keys(statePatch).length) this._set(statePatch);
950
+ if (false === flags && false === payloads) this._remove([
951
+ PERSISTENCE_OVERRIDE_FEATURE_FLAGS,
952
+ PERSISTENCE_OVERRIDE_FEATURE_FLAG_PAYLOADS
953
+ ]);
954
+ else if (false === flags) this._remove(PERSISTENCE_OVERRIDE_FEATURE_FLAGS);
955
+ else if (false === payloads) this._remove(PERSISTENCE_OVERRIDE_FEATURE_FLAG_PAYLOADS);
956
+ this._fireFeatureFlagsCallbacks();
957
+ if (false === flags) forceDebugLogger.info('Flag overrides cleared');
958
+ else if (flags) forceDebugLogger.info('Flag overrides set', {
959
+ flags
960
+ });
961
+ if (false === payloads) forceDebugLogger.info('Payload overrides cleared');
962
+ else if (payloads) forceDebugLogger.info('Payload overrides set', {
963
+ payloads
964
+ });
965
+ return;
966
+ }
967
+ if (overrideOptions && 'object' == typeof overrideOptions) {
968
+ this._set({
969
+ [PERSISTENCE_OVERRIDE_FEATURE_FLAGS]: overrideOptions
970
+ });
971
+ this._fireFeatureFlagsCallbacks();
972
+ forceDebugLogger.info('Flag overrides set', {
973
+ flags: overrideOptions
974
+ });
975
+ return;
976
+ }
977
+ this._logger.warn('Invalid overrideOptions provided to overrideFeatureFlags', {
978
+ overrideOptions
979
+ });
980
+ }
981
+ onFeatureFlags(callback) {
982
+ this.addFeatureFlagsHandler(callback);
983
+ if (this._hasLoadedFlags) {
984
+ const { flags, flagVariants } = this._prepareFeatureFlagsForCallbacks();
985
+ try {
986
+ callback(flags, flagVariants);
987
+ } catch (error) {
988
+ this._logger.error('Error while running feature flags callback', error);
989
+ }
990
+ }
991
+ return ()=>this.removeFeatureFlagsHandler(callback);
992
+ }
993
+ updateEarlyAccessFeatureEnrollment(key, isEnrolled, stage) {
994
+ const existing_early_access_features = this._prop(PERSISTENCE_EARLY_ACCESS_FEATURES) || [];
995
+ const feature = existing_early_access_features.find((f)=>f.flagKey === key);
996
+ const enrollmentPersonProp = {
997
+ [`$feature_enrollment/${key}`]: isEnrolled
998
+ };
999
+ const properties = {
1000
+ $feature_flag: key,
1001
+ $feature_enrollment: isEnrolled,
1002
+ $set: enrollmentPersonProp
1003
+ };
1004
+ if (feature) properties['$early_access_feature_name'] = feature.name;
1005
+ if (stage) properties['$feature_enrollment_stage'] = stage;
1006
+ const newFlags = {
1007
+ ...this.getFlagVariants(),
1008
+ [key]: isEnrolled
1009
+ };
1010
+ this._crossTabPersistence?.markCrossTabFeatureFlagChanges({
1011
+ [PERSISTENCE_ACTIVE_FEATURE_FLAGS]: [
1012
+ key
1013
+ ],
1014
+ [ENABLED_FEATURE_FLAGS]: [
1015
+ key
1016
+ ],
1017
+ [STORED_PERSON_PROPERTIES_KEY]: Object.keys(enrollmentPersonProp)
1018
+ });
1019
+ this._set({
1020
+ [PERSISTENCE_ACTIVE_FEATURE_FLAGS]: Object.keys(filterActiveFeatureFlags(newFlags)),
1021
+ [ENABLED_FEATURE_FLAGS]: newFlags,
1022
+ [STORED_PERSON_PROPERTIES_KEY]: {
1023
+ ...this._prop(STORED_PERSON_PROPERTIES_KEY) || {},
1024
+ ...enrollmentPersonProp
1025
+ }
1026
+ });
1027
+ this._fireFeatureFlagsCallbacks();
1028
+ try {
1029
+ this._client?.capture('$feature_enrollment_update', properties);
1030
+ } catch (error) {
1031
+ this._logger.error('Failed to capture early access feature enrollment', error);
1032
+ }
1033
+ }
1034
+ getEarlyAccessFeatures(callback, force_reload = false, stages) {
1035
+ const existing_early_access_features = this._prop(PERSISTENCE_EARLY_ACCESS_FEATURES);
1036
+ if (existing_early_access_features && !force_reload) return void callback(existing_early_access_features);
1037
+ this._getEarlyAccessFeatures(callback, stages);
1038
+ }
1039
+ async _getEarlyAccessFeatures(callback, stages) {
1040
+ const client = this._client;
1041
+ if (!client) return;
1042
+ const stageParams = stages ? `&${stages.map((s)=>`stage=${s}`).join('&')}` : '';
1043
+ let earlyAccessFeatures;
1044
+ try {
1045
+ const response = await client.sendRequest(`/api/early_access_features/?token=${client.projectToken}${stageParams}`, {
1046
+ target: 'api',
1047
+ method: 'GET',
1048
+ sentAt: 'query'
1049
+ });
1050
+ if (!response.json) return;
1051
+ earlyAccessFeatures = response.json.earlyAccessFeatures;
1052
+ this._set({
1053
+ [PERSISTENCE_EARLY_ACCESS_FEATURES]: earlyAccessFeatures
1054
+ });
1055
+ } catch (error) {
1056
+ this._logger.error('Early access feature request failed', error);
1057
+ return;
1058
+ }
1059
+ try {
1060
+ callback(earlyAccessFeatures);
1061
+ } catch (error) {
1062
+ this._logger.error('Early access feature callback failed', error);
1063
+ }
1064
+ }
1065
+ _prepareFeatureFlagsForCallbacks() {
1066
+ const flags = this.getFlags();
1067
+ const flagVariants = this.getFlagVariants();
1068
+ const truthyFlags = flags.filter((flag)=>flagVariants[flag]);
1069
+ const truthyFlagVariants = Object.keys(flagVariants).filter((variantKey)=>flagVariants[variantKey]).reduce((res, key)=>{
1070
+ res[key] = flagVariants[key];
1071
+ return res;
1072
+ }, {});
1073
+ return {
1074
+ flags: truthyFlags,
1075
+ flagVariants: truthyFlagVariants
1076
+ };
1077
+ }
1078
+ _fireFeatureFlagsCallbacks(errorsLoading) {
1079
+ this._rebuildEventProperties();
1080
+ const { flags, flagVariants } = this._prepareFeatureFlagsForCallbacks();
1081
+ this.featureFlagEventHandlers.forEach((handler)=>{
1082
+ try {
1083
+ handler(flags, flagVariants, {
1084
+ errorsLoading
1085
+ });
1086
+ } catch (error) {
1087
+ this._logger.error('Error while running feature flags callback', error);
1088
+ }
1089
+ });
1090
+ }
1091
+ setPersonPropertiesForFlags(properties, reloadFeatureFlags = true) {
1092
+ this._setPersonPropertiesForFlags(properties, reloadFeatureFlags);
1093
+ }
1094
+ _setPersonPropertiesForFlags(properties, reloadFeatureFlags = true) {
1095
+ const existingProperties = this._prop(STORED_PERSON_PROPERTIES_KEY) || {};
1096
+ const propsToSet = properties?.['$set'] || (properties?.['$set_once'] ? {} : properties);
1097
+ const propsToSetOnce = properties?.['$set_once'];
1098
+ const setOnceProps = {};
1099
+ if (propsToSetOnce) {
1100
+ for(const key in propsToSetOnce)if (Object.prototype.hasOwnProperty.call(propsToSetOnce, key)) {
1101
+ if (!(key in existingProperties)) setOnceProps[key] = propsToSetOnce[key];
1102
+ }
1103
+ }
1104
+ this._set({
1105
+ [STORED_PERSON_PROPERTIES_KEY]: {
1106
+ ...existingProperties,
1107
+ ...setOnceProps,
1108
+ ...propsToSet
1109
+ }
1110
+ });
1111
+ if (reloadFeatureFlags) this.reloadFeatureFlags();
1112
+ }
1113
+ unsetPersonPropertiesForFlags(propertyNames, reloadFeatureFlags = true) {
1114
+ const existingProperties = this._prop(STORED_PERSON_PROPERTIES_KEY) || {};
1115
+ const nextProperties = {
1116
+ ...existingProperties
1117
+ };
1118
+ propertyNames.forEach((name)=>{
1119
+ delete nextProperties[name];
1120
+ });
1121
+ this._set({
1122
+ [STORED_PERSON_PROPERTIES_KEY]: nextProperties
1123
+ });
1124
+ if (reloadFeatureFlags) this.reloadFeatureFlags();
1125
+ }
1126
+ resetPersonPropertiesForFlags(reloadFeatureFlags = true) {
1127
+ this._remove(STORED_PERSON_PROPERTIES_KEY);
1128
+ if (reloadFeatureFlags) this.reloadFeatureFlags();
1129
+ }
1130
+ setGroupPropertiesForFlags(properties, reloadFeatureFlags = true) {
1131
+ const existingProperties = this._prop(STORED_GROUP_PROPERTIES_KEY) || {};
1132
+ const nextProperties = {
1133
+ ...existingProperties
1134
+ };
1135
+ for (const groupType of Object.keys(properties))nextProperties[groupType] = {
1136
+ ...existingProperties[groupType],
1137
+ ...properties[groupType]
1138
+ };
1139
+ this._set({
1140
+ [STORED_GROUP_PROPERTIES_KEY]: nextProperties
1141
+ });
1142
+ if (reloadFeatureFlags) this.reloadFeatureFlags();
1143
+ }
1144
+ resetGroupPropertiesForFlags(group_type) {
1145
+ if (group_type) {
1146
+ const existingProperties = this._prop(STORED_GROUP_PROPERTIES_KEY) || {};
1147
+ this._set({
1148
+ [STORED_GROUP_PROPERTIES_KEY]: {
1149
+ ...existingProperties,
1150
+ [group_type]: {}
1151
+ }
1152
+ });
1153
+ } else this._remove(STORED_GROUP_PROPERTIES_KEY);
1154
+ }
1155
+ reset() {
1156
+ this._requestGeneration++;
1157
+ this._settleReloads(this._pendingReloads, 'cancelled');
1158
+ this._settleReloads(this._activeReloads, 'cancelled');
1159
+ this._additionalReloadRequested = false;
1160
+ this._baseEventProperties = {};
1161
+ this._eventPropertiesWithFlagValues = {};
1162
+ this._bootstrapState = void 0;
1163
+ this._hasLoadedFlags = false;
1164
+ this._reloadingDisabled = false;
1165
+ this._flagsLoadedFromRemote = false;
1166
+ this.$anon_distinct_id = void 0;
1167
+ this._clearDebouncer();
1168
+ this._override_warning = false;
1169
+ this._consecutiveStatusZeroFailures = 0;
1170
+ }
1171
+ }
1172
+ export { FeatureFlagError, PostHogFeatureFlags, QuotaLimitedResource, filterActiveFeatureFlags, parseFlagsResponse };