@posthog/browser-common 0.9.2 → 0.9.4
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 +4 -2
- package/dist/config.js +1 -1
- package/dist/config.mjs +1 -1
- package/dist/console-logs.d.ts +15 -0
- package/dist/console-logs.js +317 -0
- package/dist/console-logs.mjs +276 -0
- package/dist/constants.d.ts +16 -0
- package/dist/constants.js +64 -0
- package/dist/constants.mjs +17 -1
- package/dist/extension-tokens.d.ts +4 -0
- package/dist/extension-tokens.js +40 -0
- package/dist/extension-tokens.mjs +2 -0
- package/dist/extension.d.ts +4 -0
- package/dist/feature-flags-config.d.ts +23 -0
- package/dist/feature-flags-config.js +18 -0
- package/dist/feature-flags-config.mjs +1 -0
- package/dist/feature-flags.d.ts +329 -0
- package/dist/feature-flags.js +1222 -0
- package/dist/feature-flags.mjs +1172 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -7
- package/dist/index.mjs +1 -0
- package/dist/logs-config.d.ts +24 -0
- package/dist/logs-config.js +80 -0
- package/dist/logs-config.mjs +42 -0
- package/dist/logs-types.d.ts +31 -0
- package/dist/logs-types.js +46 -0
- package/dist/logs-types.mjs +8 -0
- package/dist/logs.d.ts +85 -0
- package/dist/logs.js +469 -0
- package/dist/logs.mjs +428 -0
- package/dist/token.d.ts +1 -1
- package/dist/types/feature-flags.d.ts +18 -0
- package/dist/types/feature-flags.js +18 -0
- package/dist/types/feature-flags.mjs +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.mjs +1 -0
- package/dist/utils/console-original.d.ts +6 -0
- package/dist/utils/console-original.js +43 -0
- package/dist/utils/console-original.mjs +5 -0
- package/dist/utils/event-utils.d.ts +1 -1
- package/dist/utils/event-utils.js +3 -9
- package/dist/utils/event-utils.mjs +1 -7
- package/dist/utils/logger.js +8 -3
- package/dist/utils/logger.mjs +8 -3
- package/dist/utils/patch.d.ts +3 -0
- package/dist/utils/patch.js +90 -0
- package/dist/utils/patch.mjs +52 -0
- package/dist/utils/request-reachability.d.ts +2 -0
- package/dist/utils/request-reachability.js +61 -0
- package/dist/utils/request-reachability.mjs +20 -0
- package/dist/utils/request-utils.d.ts +1 -2
- package/dist/utils/request-utils.js +3 -14
- package/dist/utils/request-utils.mjs +3 -14
- package/dist/utils/sanitize-url.d.ts +11 -0
- package/dist/utils/sanitize-url.js +55 -0
- package/dist/utils/sanitize-url.mjs +17 -0
- package/dist/utils/timezone.d.ts +1 -0
- package/dist/utils/timezone.js +46 -0
- package/dist/utils/timezone.mjs +8 -0
- package/package.json +59 -3
|
@@ -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 };
|