posthog-js-lite 3.4.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +21 -2
- package/lib/index.cjs.js +720 -106
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +315 -22
- package/lib/index.esm.js +720 -106
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/featureFlagUtils.d.ts +34 -0
- package/lib/posthog-core/src/index.d.ts +43 -11
- package/lib/posthog-core/src/patch.d.ts +3 -0
- package/lib/posthog-core/src/types.d.ts +305 -15
- package/lib/posthog-core/src/utils.d.ts +4 -0
- package/lib/posthog-web/src/posthog-web.d.ts +3 -0
- package/lib/posthog-web/src/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/posthog-web.ts +52 -0
- package/src/types.ts +1 -0
- package/test/posthog-web.spec.ts +193 -2
package/lib/index.cjs.js
CHANGED
|
@@ -7,8 +7,10 @@ var PostHogPersistedProperty;
|
|
|
7
7
|
PostHogPersistedProperty["AnonymousId"] = "anonymous_id";
|
|
8
8
|
PostHogPersistedProperty["DistinctId"] = "distinct_id";
|
|
9
9
|
PostHogPersistedProperty["Props"] = "props";
|
|
10
|
+
PostHogPersistedProperty["FeatureFlagDetails"] = "feature_flag_details";
|
|
10
11
|
PostHogPersistedProperty["FeatureFlags"] = "feature_flags";
|
|
11
12
|
PostHogPersistedProperty["FeatureFlagPayloads"] = "feature_flag_payloads";
|
|
13
|
+
PostHogPersistedProperty["BootstrapFeatureFlagDetails"] = "bootstrap_feature_flag_details";
|
|
12
14
|
PostHogPersistedProperty["BootstrapFeatureFlags"] = "bootstrap_feature_flags";
|
|
13
15
|
PostHogPersistedProperty["BootstrapFeatureFlagPayloads"] = "bootstrap_feature_flag_payloads";
|
|
14
16
|
PostHogPersistedProperty["OverrideFeatureFlags"] = "override_feature_flags";
|
|
@@ -22,13 +24,284 @@ var PostHogPersistedProperty;
|
|
|
22
24
|
PostHogPersistedProperty["InstalledAppVersion"] = "installed_app_version";
|
|
23
25
|
PostHogPersistedProperty["SessionReplay"] = "session_replay";
|
|
24
26
|
PostHogPersistedProperty["DecideEndpointWasHit"] = "decide_endpoint_was_hit";
|
|
25
|
-
|
|
27
|
+
PostHogPersistedProperty["SurveyLastSeenDate"] = "survey_last_seen_date";
|
|
28
|
+
PostHogPersistedProperty["SurveysSeen"] = "surveys_seen";
|
|
29
|
+
PostHogPersistedProperty["Surveys"] = "surveys";
|
|
30
|
+
PostHogPersistedProperty["RemoteConfig"] = "remote_config";
|
|
31
|
+
})(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
|
|
32
|
+
var SurveyPosition;
|
|
33
|
+
(function (SurveyPosition) {
|
|
34
|
+
SurveyPosition["Left"] = "left";
|
|
35
|
+
SurveyPosition["Right"] = "right";
|
|
36
|
+
SurveyPosition["Center"] = "center";
|
|
37
|
+
})(SurveyPosition || (SurveyPosition = {}));
|
|
38
|
+
var SurveyWidgetType;
|
|
39
|
+
(function (SurveyWidgetType) {
|
|
40
|
+
SurveyWidgetType["Button"] = "button";
|
|
41
|
+
SurveyWidgetType["Tab"] = "tab";
|
|
42
|
+
SurveyWidgetType["Selector"] = "selector";
|
|
43
|
+
})(SurveyWidgetType || (SurveyWidgetType = {}));
|
|
44
|
+
var SurveyType;
|
|
45
|
+
(function (SurveyType) {
|
|
46
|
+
SurveyType["Popover"] = "popover";
|
|
47
|
+
SurveyType["API"] = "api";
|
|
48
|
+
SurveyType["Widget"] = "widget";
|
|
49
|
+
})(SurveyType || (SurveyType = {}));
|
|
50
|
+
var SurveyQuestionDescriptionContentType;
|
|
51
|
+
(function (SurveyQuestionDescriptionContentType) {
|
|
52
|
+
SurveyQuestionDescriptionContentType["Html"] = "html";
|
|
53
|
+
SurveyQuestionDescriptionContentType["Text"] = "text";
|
|
54
|
+
})(SurveyQuestionDescriptionContentType || (SurveyQuestionDescriptionContentType = {}));
|
|
55
|
+
var SurveyRatingDisplay;
|
|
56
|
+
(function (SurveyRatingDisplay) {
|
|
57
|
+
SurveyRatingDisplay["Number"] = "number";
|
|
58
|
+
SurveyRatingDisplay["Emoji"] = "emoji";
|
|
59
|
+
})(SurveyRatingDisplay || (SurveyRatingDisplay = {}));
|
|
60
|
+
var SurveyQuestionType;
|
|
61
|
+
(function (SurveyQuestionType) {
|
|
62
|
+
SurveyQuestionType["Open"] = "open";
|
|
63
|
+
SurveyQuestionType["MultipleChoice"] = "multiple_choice";
|
|
64
|
+
SurveyQuestionType["SingleChoice"] = "single_choice";
|
|
65
|
+
SurveyQuestionType["Rating"] = "rating";
|
|
66
|
+
SurveyQuestionType["Link"] = "link";
|
|
67
|
+
})(SurveyQuestionType || (SurveyQuestionType = {}));
|
|
68
|
+
var SurveyQuestionBranchingType;
|
|
69
|
+
(function (SurveyQuestionBranchingType) {
|
|
70
|
+
SurveyQuestionBranchingType["NextQuestion"] = "next_question";
|
|
71
|
+
SurveyQuestionBranchingType["End"] = "end";
|
|
72
|
+
SurveyQuestionBranchingType["ResponseBased"] = "response_based";
|
|
73
|
+
SurveyQuestionBranchingType["SpecificQuestion"] = "specific_question";
|
|
74
|
+
})(SurveyQuestionBranchingType || (SurveyQuestionBranchingType = {}));
|
|
75
|
+
var SurveyMatchType;
|
|
76
|
+
(function (SurveyMatchType) {
|
|
77
|
+
SurveyMatchType["Regex"] = "regex";
|
|
78
|
+
SurveyMatchType["NotRegex"] = "not_regex";
|
|
79
|
+
SurveyMatchType["Exact"] = "exact";
|
|
80
|
+
SurveyMatchType["IsNot"] = "is_not";
|
|
81
|
+
SurveyMatchType["Icontains"] = "icontains";
|
|
82
|
+
SurveyMatchType["NotIcontains"] = "not_icontains";
|
|
83
|
+
})(SurveyMatchType || (SurveyMatchType = {}));
|
|
84
|
+
/** Sync with plugin-server/src/types.ts */
|
|
85
|
+
var ActionStepStringMatching;
|
|
86
|
+
(function (ActionStepStringMatching) {
|
|
87
|
+
ActionStepStringMatching["Contains"] = "contains";
|
|
88
|
+
ActionStepStringMatching["Exact"] = "exact";
|
|
89
|
+
ActionStepStringMatching["Regex"] = "regex";
|
|
90
|
+
})(ActionStepStringMatching || (ActionStepStringMatching = {}));
|
|
26
91
|
|
|
92
|
+
const normalizeDecideResponse = (decideResponse) => {
|
|
93
|
+
if ('flags' in decideResponse) {
|
|
94
|
+
// Convert v4 format to v3 format
|
|
95
|
+
const featureFlags = getFlagValuesFromFlags(decideResponse.flags);
|
|
96
|
+
const featureFlagPayloads = getPayloadsFromFlags(decideResponse.flags);
|
|
97
|
+
return {
|
|
98
|
+
...decideResponse,
|
|
99
|
+
featureFlags,
|
|
100
|
+
featureFlagPayloads,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Convert v3 format to v4 format
|
|
105
|
+
const featureFlags = decideResponse.featureFlags ?? {};
|
|
106
|
+
const featureFlagPayloads = Object.fromEntries(Object.entries(decideResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
|
|
107
|
+
const flags = Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [
|
|
108
|
+
key,
|
|
109
|
+
getFlagDetailFromFlagAndPayload(key, value, featureFlagPayloads[key]),
|
|
110
|
+
]));
|
|
111
|
+
return {
|
|
112
|
+
...decideResponse,
|
|
113
|
+
featureFlags,
|
|
114
|
+
featureFlagPayloads,
|
|
115
|
+
flags,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function getFlagDetailFromFlagAndPayload(key, value, payload) {
|
|
120
|
+
return {
|
|
121
|
+
key: key,
|
|
122
|
+
enabled: typeof value === 'string' ? true : value,
|
|
123
|
+
variant: typeof value === 'string' ? value : undefined,
|
|
124
|
+
reason: undefined,
|
|
125
|
+
metadata: {
|
|
126
|
+
id: undefined,
|
|
127
|
+
version: undefined,
|
|
128
|
+
payload: payload ? JSON.stringify(payload) : undefined,
|
|
129
|
+
description: undefined,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get the flag values from the flags v4 response.
|
|
135
|
+
* @param flags - The flags
|
|
136
|
+
* @returns The flag values
|
|
137
|
+
*/
|
|
138
|
+
const getFlagValuesFromFlags = (flags) => {
|
|
139
|
+
return Object.fromEntries(Object.entries(flags ?? {})
|
|
140
|
+
.map(([key, detail]) => [key, getFeatureFlagValue(detail)])
|
|
141
|
+
.filter(([, value]) => value !== undefined));
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Get the payloads from the flags v4 response.
|
|
145
|
+
* @param flags - The flags
|
|
146
|
+
* @returns The payloads
|
|
147
|
+
*/
|
|
148
|
+
const getPayloadsFromFlags = (flags) => {
|
|
149
|
+
const safeFlags = flags ?? {};
|
|
150
|
+
return Object.fromEntries(Object.keys(safeFlags)
|
|
151
|
+
.filter((flag) => {
|
|
152
|
+
const details = safeFlags[flag];
|
|
153
|
+
return details.enabled && details.metadata && details.metadata.payload !== undefined;
|
|
154
|
+
})
|
|
155
|
+
.map((flag) => {
|
|
156
|
+
const payload = safeFlags[flag].metadata?.payload;
|
|
157
|
+
return [flag, payload ? parsePayload(payload) : undefined];
|
|
158
|
+
}));
|
|
159
|
+
};
|
|
160
|
+
const getFeatureFlagValue = (detail) => {
|
|
161
|
+
return detail === undefined ? undefined : detail.variant ?? detail.enabled;
|
|
162
|
+
};
|
|
163
|
+
const parsePayload = (response) => {
|
|
164
|
+
if (typeof response !== 'string') {
|
|
165
|
+
return response;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(response);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return response;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Get the normalized flag details from the flags and payloads.
|
|
176
|
+
* This is used to convert things like boostrap and stored feature flags and payloads to the v4 format.
|
|
177
|
+
* This helps us ensure backwards compatibility.
|
|
178
|
+
* If a key exists in the featureFlagPayloads that is not in the featureFlags, we treat it as a true feature flag.
|
|
179
|
+
*
|
|
180
|
+
* @param featureFlags - The feature flags
|
|
181
|
+
* @param featureFlagPayloads - The feature flag payloads
|
|
182
|
+
* @returns The normalized flag details
|
|
183
|
+
*/
|
|
184
|
+
const createDecideResponseFromFlagsAndPayloads = (featureFlags, featureFlagPayloads) => {
|
|
185
|
+
// If a feature flag payload key is not in the feature flags, we treat it as true feature flag.
|
|
186
|
+
const allKeys = [...new Set([...Object.keys(featureFlags ?? {}), ...Object.keys(featureFlagPayloads ?? {})])];
|
|
187
|
+
const enabledFlags = allKeys
|
|
188
|
+
.filter((flag) => !!featureFlags[flag] || !!featureFlagPayloads[flag])
|
|
189
|
+
.reduce((res, key) => ((res[key] = featureFlags[key] ?? true), res), {});
|
|
190
|
+
const flagDetails = {
|
|
191
|
+
featureFlags: enabledFlags,
|
|
192
|
+
featureFlagPayloads: featureFlagPayloads ?? {},
|
|
193
|
+
};
|
|
194
|
+
return normalizeDecideResponse(flagDetails);
|
|
195
|
+
};
|
|
196
|
+
const updateFlagValue = (flag, value) => {
|
|
197
|
+
return {
|
|
198
|
+
...flag,
|
|
199
|
+
enabled: getEnabledFromValue(value),
|
|
200
|
+
variant: getVariantFromValue(value),
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
function getEnabledFromValue(value) {
|
|
204
|
+
return typeof value === 'string' ? true : value;
|
|
205
|
+
}
|
|
206
|
+
function getVariantFromValue(value) {
|
|
207
|
+
return typeof value === 'string' ? value : undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Rollout constants
|
|
211
|
+
const NEW_FLAGS_ROLLOUT_PERCENTAGE = 1;
|
|
212
|
+
// The fnv1a hashes of the tokens that are explicitly excluded from the rollout
|
|
213
|
+
// see https://github.com/PostHog/posthog-js-lite/blob/main/posthog-core/src/utils.ts#L84
|
|
214
|
+
// are hashed API tokens from our top 10 for each category supported by this SDK.
|
|
215
|
+
const NEW_FLAGS_EXCLUDED_HASHES = new Set([
|
|
216
|
+
// Node
|
|
217
|
+
'61be3dd8',
|
|
218
|
+
'96f6df5f',
|
|
219
|
+
'8cfdba9b',
|
|
220
|
+
'bf027177',
|
|
221
|
+
'e59430a8',
|
|
222
|
+
'7fa5500b',
|
|
223
|
+
'569798e9',
|
|
224
|
+
'04809ff7',
|
|
225
|
+
'0ebc61a5',
|
|
226
|
+
'32de7f98',
|
|
227
|
+
'3beeb69a',
|
|
228
|
+
'12d34ad9',
|
|
229
|
+
'733853ec',
|
|
230
|
+
'0645bb64',
|
|
231
|
+
'5dcbee21',
|
|
232
|
+
'b1f95fa3',
|
|
233
|
+
'2189e408',
|
|
234
|
+
'82b460c2',
|
|
235
|
+
'3a8cc979',
|
|
236
|
+
'29ef8843',
|
|
237
|
+
'2cdbf767',
|
|
238
|
+
'38084b54',
|
|
239
|
+
// React Native
|
|
240
|
+
'50f9f8de',
|
|
241
|
+
'41d0df91',
|
|
242
|
+
'5c236689',
|
|
243
|
+
'c11aedd3',
|
|
244
|
+
'ada46672',
|
|
245
|
+
'f4331ee1',
|
|
246
|
+
'42fed62a',
|
|
247
|
+
'c957462c',
|
|
248
|
+
'd62f705a',
|
|
249
|
+
// Web (lots of teams per org, hence lots of API tokens)
|
|
250
|
+
'e0162666',
|
|
251
|
+
'01b3e5cf',
|
|
252
|
+
'441cef7f',
|
|
253
|
+
'bb9cafee',
|
|
254
|
+
'8f348eb0',
|
|
255
|
+
'b2553f3a',
|
|
256
|
+
'97469d7d',
|
|
257
|
+
'39f21a76',
|
|
258
|
+
'03706dcc',
|
|
259
|
+
'27d50569',
|
|
260
|
+
'307584a7',
|
|
261
|
+
'6433e92e',
|
|
262
|
+
'150c7fbb',
|
|
263
|
+
'49f57f22',
|
|
264
|
+
'3772f65b',
|
|
265
|
+
'01eb8256',
|
|
266
|
+
'3c9e9234',
|
|
267
|
+
'f853c7f7',
|
|
268
|
+
'c0ac4b67',
|
|
269
|
+
'cd609d40',
|
|
270
|
+
'10ca9b1a',
|
|
271
|
+
'8a87f11b',
|
|
272
|
+
'8e8e5216',
|
|
273
|
+
'1f6b63b3',
|
|
274
|
+
'db7943dd',
|
|
275
|
+
'79b7164c',
|
|
276
|
+
'07f78e33',
|
|
277
|
+
'2d21b6fd',
|
|
278
|
+
'952db5ee',
|
|
279
|
+
'a7d3b43f',
|
|
280
|
+
'1924dd9c',
|
|
281
|
+
'84e1b8f6',
|
|
282
|
+
'dff631b6',
|
|
283
|
+
'c5aa8a79',
|
|
284
|
+
'fa133a95',
|
|
285
|
+
'498a4508',
|
|
286
|
+
'24748755',
|
|
287
|
+
'98f3d658',
|
|
288
|
+
'21bbda67',
|
|
289
|
+
'7dbfed69',
|
|
290
|
+
'be3ec24c',
|
|
291
|
+
'fc80b8e2',
|
|
292
|
+
'75cc0998',
|
|
293
|
+
]);
|
|
27
294
|
function assert(truthyValue, message) {
|
|
28
|
-
if (!truthyValue) {
|
|
295
|
+
if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
|
|
29
296
|
throw new Error(message);
|
|
30
297
|
}
|
|
31
298
|
}
|
|
299
|
+
function isEmpty(truthyValue) {
|
|
300
|
+
if (truthyValue.trim().length === 0) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
32
305
|
function removeTrailingSlash(url) {
|
|
33
306
|
return url?.replace(/\/+$/, '');
|
|
34
307
|
}
|
|
@@ -71,6 +344,34 @@ const isError = (x) => {
|
|
|
71
344
|
};
|
|
72
345
|
function getFetch() {
|
|
73
346
|
return typeof fetch !== 'undefined' ? fetch : typeof global.fetch !== 'undefined' ? global.fetch : undefined;
|
|
347
|
+
}
|
|
348
|
+
// copied from: https://github.com/PostHog/posthog-js/blob/main/react/src/utils/type-utils.ts#L4
|
|
349
|
+
const isFunction = function (f) {
|
|
350
|
+
return typeof f === 'function';
|
|
351
|
+
};
|
|
352
|
+
// FNV-1a hash function
|
|
353
|
+
// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
|
|
354
|
+
// I know, I know, I'm rolling my own hash function, but I didn't want to take on
|
|
355
|
+
// a crypto dependency and this is just temporary anyway
|
|
356
|
+
function fnv1a(str) {
|
|
357
|
+
let hash = 0x811c9dc5; // FNV offset basis
|
|
358
|
+
for (let i = 0; i < str.length; i++) {
|
|
359
|
+
hash ^= str.charCodeAt(i);
|
|
360
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
361
|
+
}
|
|
362
|
+
// Convert to hex string, padding to 8 chars
|
|
363
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
364
|
+
}
|
|
365
|
+
function isTokenInRollout(token, percentage = 0, excludedHashes) {
|
|
366
|
+
const tokenHash = fnv1a(token);
|
|
367
|
+
// Check excluded hashes (we're explicitly including these tokens from the rollout)
|
|
368
|
+
if (excludedHashes?.has(tokenHash)) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
// Convert hash to int and divide by max value to get number between 0-1
|
|
372
|
+
const hashInt = parseInt(tokenHash, 16);
|
|
373
|
+
const hashFloat = hashInt / 0xffffffff;
|
|
374
|
+
return hashFloat < percentage;
|
|
74
375
|
}
|
|
75
376
|
|
|
76
377
|
// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
|
|
@@ -954,13 +1255,14 @@ class PostHogFetchNetworkError extends Error {
|
|
|
954
1255
|
function isPostHogFetchError(err) {
|
|
955
1256
|
return typeof err === 'object' && (err instanceof PostHogFetchHttpError || err instanceof PostHogFetchNetworkError);
|
|
956
1257
|
}
|
|
1258
|
+
var QuotaLimitedFeature;
|
|
1259
|
+
(function (QuotaLimitedFeature) {
|
|
1260
|
+
QuotaLimitedFeature["FeatureFlags"] = "feature_flags";
|
|
1261
|
+
QuotaLimitedFeature["Recordings"] = "recordings";
|
|
1262
|
+
})(QuotaLimitedFeature || (QuotaLimitedFeature = {}));
|
|
957
1263
|
class PostHogCoreStateless {
|
|
958
1264
|
constructor(apiKey, options) {
|
|
959
1265
|
this.flushPromise = null;
|
|
960
|
-
this.disableGeoip = true;
|
|
961
|
-
this.historicalMigration = false;
|
|
962
|
-
this.disabled = false;
|
|
963
|
-
this.defaultOptIn = true;
|
|
964
1266
|
this.pendingPromises = {};
|
|
965
1267
|
// internal
|
|
966
1268
|
this._events = new SimpleEventEmitter();
|
|
@@ -973,8 +1275,10 @@ class PostHogCoreStateless {
|
|
|
973
1275
|
this.maxQueueSize = Math.max(this.flushAt, options?.maxQueueSize ?? 1000);
|
|
974
1276
|
this.flushInterval = options?.flushInterval ?? 10000;
|
|
975
1277
|
this.captureMode = options?.captureMode || 'json';
|
|
1278
|
+
this.preloadFeatureFlags = options?.preloadFeatureFlags ?? true;
|
|
976
1279
|
// If enable is explicitly set to false we override the optout
|
|
977
1280
|
this.defaultOptIn = options?.defaultOptIn ?? true;
|
|
1281
|
+
this.disableSurveys = options?.disableSurveys ?? false;
|
|
978
1282
|
this._retryOptions = {
|
|
979
1283
|
retryCount: options?.fetchRetryCount ?? 3,
|
|
980
1284
|
retryDelay: options?.fetchRetryDelay ?? 3000,
|
|
@@ -982,6 +1286,7 @@ class PostHogCoreStateless {
|
|
|
982
1286
|
};
|
|
983
1287
|
this.requestTimeout = options?.requestTimeout ?? 10000; // 10 seconds
|
|
984
1288
|
this.featureFlagsRequestTimeoutMs = options?.featureFlagsRequestTimeoutMs ?? 3000; // 3 seconds
|
|
1289
|
+
this.remoteConfigRequestTimeoutMs = options?.remoteConfigRequestTimeoutMs ?? 3000; // 3 seconds
|
|
985
1290
|
this.disableGeoip = options?.disableGeoip ?? true;
|
|
986
1291
|
this.disabled = options?.disabled ?? false;
|
|
987
1292
|
this.historicalMigration = options?.historicalMigration ?? false;
|
|
@@ -1118,12 +1423,39 @@ class PostHogCoreStateless {
|
|
|
1118
1423
|
this.enqueue('capture', payload, options);
|
|
1119
1424
|
});
|
|
1120
1425
|
}
|
|
1426
|
+
async getRemoteConfig() {
|
|
1427
|
+
await this._initPromise;
|
|
1428
|
+
let host = this.host;
|
|
1429
|
+
if (host === 'https://us.i.posthog.com') {
|
|
1430
|
+
host = 'https://us-assets.i.posthog.com';
|
|
1431
|
+
}
|
|
1432
|
+
else if (host === 'https://eu.i.posthog.com') {
|
|
1433
|
+
host = 'https://eu-assets.i.posthog.com';
|
|
1434
|
+
}
|
|
1435
|
+
const url = `${host}/array/${this.apiKey}/config`;
|
|
1436
|
+
const fetchOptions = {
|
|
1437
|
+
method: 'GET',
|
|
1438
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
1439
|
+
};
|
|
1440
|
+
// Don't retry remote config API calls
|
|
1441
|
+
return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.remoteConfigRequestTimeoutMs)
|
|
1442
|
+
.then((response) => response.json())
|
|
1443
|
+
.catch((error) => {
|
|
1444
|
+
this.logMsgIfDebug(() => console.error('Remote config could not be loaded', error));
|
|
1445
|
+
this._events.emit('error', error);
|
|
1446
|
+
return undefined;
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1121
1449
|
/***
|
|
1122
1450
|
*** FEATURE FLAGS
|
|
1123
1451
|
***/
|
|
1124
1452
|
async getDecide(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
|
|
1125
1453
|
await this._initPromise;
|
|
1126
|
-
|
|
1454
|
+
// Check if the API token is in the new flags rollout
|
|
1455
|
+
// This is a temporary measure to ensure that we can still use the old flags API
|
|
1456
|
+
// while we migrate to the new flags API
|
|
1457
|
+
const useFlags = isTokenInRollout(this.apiKey, NEW_FLAGS_ROLLOUT_PERCENTAGE, NEW_FLAGS_EXCLUDED_HASHES);
|
|
1458
|
+
const url = useFlags ? `${this.host}/flags/?v=2` : `${this.host}/decide/?v=4`;
|
|
1127
1459
|
const fetchOptions = {
|
|
1128
1460
|
method: 'POST',
|
|
1129
1461
|
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
@@ -1139,6 +1471,7 @@ class PostHogCoreStateless {
|
|
|
1139
1471
|
// Don't retry /decide API calls
|
|
1140
1472
|
return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.featureFlagsRequestTimeoutMs)
|
|
1141
1473
|
.then((response) => response.json())
|
|
1474
|
+
.then((response) => normalizeDecideResponse(response))
|
|
1142
1475
|
.catch((error) => {
|
|
1143
1476
|
this._events.emit('error', error);
|
|
1144
1477
|
return undefined;
|
|
@@ -1146,23 +1479,41 @@ class PostHogCoreStateless {
|
|
|
1146
1479
|
}
|
|
1147
1480
|
async getFeatureFlagStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1148
1481
|
await this._initPromise;
|
|
1149
|
-
const
|
|
1150
|
-
if (
|
|
1482
|
+
const flagDetailResponse = await this.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
1483
|
+
if (flagDetailResponse === undefined) {
|
|
1151
1484
|
// If we haven't loaded flags yet, or errored out, we respond with undefined
|
|
1152
|
-
return
|
|
1485
|
+
return {
|
|
1486
|
+
response: undefined,
|
|
1487
|
+
requestId: undefined,
|
|
1488
|
+
};
|
|
1153
1489
|
}
|
|
1154
|
-
let response =
|
|
1155
|
-
// `/decide` v3 returns all flags
|
|
1490
|
+
let response = getFeatureFlagValue(flagDetailResponse.response);
|
|
1156
1491
|
if (response === undefined) {
|
|
1157
1492
|
// For cases where the flag is unknown, return false
|
|
1158
1493
|
response = false;
|
|
1159
1494
|
}
|
|
1160
1495
|
// If we have flags we either return the value (true or string) or false
|
|
1161
|
-
return
|
|
1496
|
+
return {
|
|
1497
|
+
response,
|
|
1498
|
+
requestId: flagDetailResponse.requestId,
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
async getFeatureFlagDetailStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1502
|
+
await this._initPromise;
|
|
1503
|
+
const decideResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
|
|
1504
|
+
if (decideResponse === undefined) {
|
|
1505
|
+
return undefined;
|
|
1506
|
+
}
|
|
1507
|
+
const featureFlags = decideResponse.flags;
|
|
1508
|
+
const flagDetail = featureFlags[key];
|
|
1509
|
+
return {
|
|
1510
|
+
response: flagDetail,
|
|
1511
|
+
requestId: decideResponse.requestId,
|
|
1512
|
+
};
|
|
1162
1513
|
}
|
|
1163
1514
|
async getFeatureFlagPayloadStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1164
1515
|
await this._initPromise;
|
|
1165
|
-
const payloads = await this.getFeatureFlagPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
1516
|
+
const payloads = await this.getFeatureFlagPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
|
|
1166
1517
|
if (!payloads) {
|
|
1167
1518
|
return undefined;
|
|
1168
1519
|
}
|
|
@@ -1173,40 +1524,96 @@ class PostHogCoreStateless {
|
|
|
1173
1524
|
}
|
|
1174
1525
|
return response;
|
|
1175
1526
|
}
|
|
1176
|
-
async getFeatureFlagPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1527
|
+
async getFeatureFlagPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1177
1528
|
await this._initPromise;
|
|
1178
|
-
const payloads = (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip)).payloads;
|
|
1529
|
+
const payloads = (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate)).payloads;
|
|
1179
1530
|
return payloads;
|
|
1180
1531
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
1185
|
-
catch {
|
|
1186
|
-
return response;
|
|
1187
|
-
}
|
|
1532
|
+
async getFeatureFlagsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1533
|
+
await this._initPromise;
|
|
1534
|
+
return await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
|
|
1188
1535
|
}
|
|
1189
|
-
async
|
|
1536
|
+
async getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1190
1537
|
await this._initPromise;
|
|
1191
|
-
|
|
1538
|
+
const featureFlagDetails = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
|
|
1539
|
+
if (!featureFlagDetails) {
|
|
1540
|
+
return {
|
|
1541
|
+
flags: undefined,
|
|
1542
|
+
payloads: undefined,
|
|
1543
|
+
requestId: undefined,
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
return {
|
|
1547
|
+
flags: featureFlagDetails.featureFlags,
|
|
1548
|
+
payloads: featureFlagDetails.featureFlagPayloads,
|
|
1549
|
+
requestId: featureFlagDetails.requestId,
|
|
1550
|
+
};
|
|
1192
1551
|
}
|
|
1193
|
-
async
|
|
1552
|
+
async getFeatureFlagDetailsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1194
1553
|
await this._initPromise;
|
|
1195
1554
|
const extraPayload = {};
|
|
1196
1555
|
if (disableGeoip ?? this.disableGeoip) {
|
|
1197
1556
|
extraPayload['geoip_disable'] = true;
|
|
1198
1557
|
}
|
|
1558
|
+
if (flagKeysToEvaluate) {
|
|
1559
|
+
extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate;
|
|
1560
|
+
}
|
|
1199
1561
|
const decideResponse = await this.getDecide(distinctId, groups, personProperties, groupProperties, extraPayload);
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
if (payloads) {
|
|
1204
|
-
parsedPayloads = Object.fromEntries(Object.entries(payloads).map(([k, v]) => [k, this._parsePayload(v)]));
|
|
1562
|
+
if (decideResponse === undefined) {
|
|
1563
|
+
// We probably errored out, so return undefined
|
|
1564
|
+
return undefined;
|
|
1205
1565
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1566
|
+
// if there's an error on the decideResponse, log a console error, but don't throw an error
|
|
1567
|
+
if (decideResponse.errorsWhileComputingFlags) {
|
|
1568
|
+
console.error('[FEATURE FLAGS] Error while computing feature flags, some flags may be missing or incorrect. Learn more at https://posthog.com/docs/feature-flags/best-practices');
|
|
1569
|
+
}
|
|
1570
|
+
// Add check for quota limitation on feature flags
|
|
1571
|
+
if (decideResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
|
|
1572
|
+
console.warn('[FEATURE FLAGS] Feature flags quota limit exceeded - feature flags unavailable. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts');
|
|
1573
|
+
return {
|
|
1574
|
+
flags: {},
|
|
1575
|
+
featureFlags: {},
|
|
1576
|
+
featureFlagPayloads: {},
|
|
1577
|
+
requestId: decideResponse?.requestId,
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
return decideResponse;
|
|
1581
|
+
}
|
|
1582
|
+
/***
|
|
1583
|
+
*** SURVEYS
|
|
1584
|
+
***/
|
|
1585
|
+
async getSurveysStateless() {
|
|
1586
|
+
await this._initPromise;
|
|
1587
|
+
if (this.disableSurveys === true) {
|
|
1588
|
+
this.logMsgIfDebug(() => console.log('Loading surveys is disabled.'));
|
|
1589
|
+
return [];
|
|
1590
|
+
}
|
|
1591
|
+
const url = `${this.host}/api/surveys/?token=${this.apiKey}`;
|
|
1592
|
+
const fetchOptions = {
|
|
1593
|
+
method: 'GET',
|
|
1594
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
1209
1595
|
};
|
|
1596
|
+
const response = await this.fetchWithRetry(url, fetchOptions)
|
|
1597
|
+
.then((response) => {
|
|
1598
|
+
if (response.status !== 200 || !response.json) {
|
|
1599
|
+
const msg = `Surveys API could not be loaded: ${response.status}`;
|
|
1600
|
+
const error = new Error(msg);
|
|
1601
|
+
this.logMsgIfDebug(() => console.error(error));
|
|
1602
|
+
this._events.emit('error', new Error(msg));
|
|
1603
|
+
return undefined;
|
|
1604
|
+
}
|
|
1605
|
+
return response.json();
|
|
1606
|
+
})
|
|
1607
|
+
.catch((error) => {
|
|
1608
|
+
this.logMsgIfDebug(() => console.error('Surveys API could not be loaded', error));
|
|
1609
|
+
this._events.emit('error', error);
|
|
1610
|
+
return undefined;
|
|
1611
|
+
});
|
|
1612
|
+
const newSurveys = response?.surveys;
|
|
1613
|
+
if (newSurveys) {
|
|
1614
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Surveys fetched from API: ', JSON.stringify(newSurveys)));
|
|
1615
|
+
}
|
|
1616
|
+
return newSurveys ?? [];
|
|
1210
1617
|
}
|
|
1211
1618
|
/***
|
|
1212
1619
|
*** QUEUEING AND FLUSHING
|
|
@@ -1445,23 +1852,21 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1445
1852
|
}
|
|
1446
1853
|
}
|
|
1447
1854
|
}
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
this.
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
const newFeatureFlagPayloads = { ...bootstrapFlagPayloads, ...currentFlagPayloads };
|
|
1464
|
-
this.setKnownFeatureFlagPayloads(newFeatureFlagPayloads);
|
|
1855
|
+
const bootstrapFeatureFlags = bootstrap.featureFlags;
|
|
1856
|
+
const bootstrapFeatureFlagPayloads = bootstrap.featureFlagPayloads ?? {};
|
|
1857
|
+
if (bootstrapFeatureFlags && Object.keys(bootstrapFeatureFlags).length) {
|
|
1858
|
+
const normalizedBootstrapFeatureFlagDetails = createDecideResponseFromFlagsAndPayloads(bootstrapFeatureFlags, bootstrapFeatureFlagPayloads);
|
|
1859
|
+
if (Object.keys(normalizedBootstrapFeatureFlagDetails.flags).length > 0) {
|
|
1860
|
+
this.setBootstrappedFeatureFlagDetails(normalizedBootstrapFeatureFlagDetails);
|
|
1861
|
+
const currentFeatureFlagDetails = this.getKnownFeatureFlagDetails() || { flags: {}, requestId: undefined };
|
|
1862
|
+
const newFeatureFlagDetails = {
|
|
1863
|
+
flags: {
|
|
1864
|
+
...normalizedBootstrapFeatureFlagDetails.flags,
|
|
1865
|
+
...currentFeatureFlagDetails.flags,
|
|
1866
|
+
},
|
|
1867
|
+
requestId: normalizedBootstrapFeatureFlagDetails.requestId,
|
|
1868
|
+
};
|
|
1869
|
+
this.setKnownFeatureFlagDetails(newFeatureFlagDetails);
|
|
1465
1870
|
}
|
|
1466
1871
|
}
|
|
1467
1872
|
}
|
|
@@ -1699,7 +2104,7 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1699
2104
|
}
|
|
1700
2105
|
resetPersonPropertiesForFlags() {
|
|
1701
2106
|
this.wrap(() => {
|
|
1702
|
-
this.setPersistedProperty(PostHogPersistedProperty.PersonProperties,
|
|
2107
|
+
this.setPersistedProperty(PostHogPersistedProperty.PersonProperties, null);
|
|
1703
2108
|
});
|
|
1704
2109
|
}
|
|
1705
2110
|
/** @deprecated - Renamed to setPersonPropertiesForFlags */
|
|
@@ -1728,7 +2133,7 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1728
2133
|
}
|
|
1729
2134
|
resetGroupPropertiesForFlags() {
|
|
1730
2135
|
this.wrap(() => {
|
|
1731
|
-
this.setPersistedProperty(PostHogPersistedProperty.GroupProperties,
|
|
2136
|
+
this.setPersistedProperty(PostHogPersistedProperty.GroupProperties, null);
|
|
1732
2137
|
});
|
|
1733
2138
|
}
|
|
1734
2139
|
/** @deprecated - Renamed to setGroupPropertiesForFlags */
|
|
@@ -1737,6 +2142,13 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1737
2142
|
this.setGroupPropertiesForFlags(properties);
|
|
1738
2143
|
});
|
|
1739
2144
|
}
|
|
2145
|
+
async remoteConfigAsync() {
|
|
2146
|
+
await this._initPromise;
|
|
2147
|
+
if (this._remoteConfigResponsePromise) {
|
|
2148
|
+
return this._remoteConfigResponsePromise;
|
|
2149
|
+
}
|
|
2150
|
+
return this._remoteConfigAsync();
|
|
2151
|
+
}
|
|
1740
2152
|
/***
|
|
1741
2153
|
*** FEATURE FLAGS
|
|
1742
2154
|
***/
|
|
@@ -1747,9 +2159,68 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1747
2159
|
}
|
|
1748
2160
|
return this._decideAsync(sendAnonDistinctId);
|
|
1749
2161
|
}
|
|
2162
|
+
cacheSessionReplay(response) {
|
|
2163
|
+
const sessionReplay = response?.sessionRecording;
|
|
2164
|
+
if (sessionReplay) {
|
|
2165
|
+
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, sessionReplay);
|
|
2166
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Session replay config: ', JSON.stringify(sessionReplay)));
|
|
2167
|
+
}
|
|
2168
|
+
else {
|
|
2169
|
+
this.logMsgIfDebug(() => console.info('PostHog Debug', 'Session replay config disabled.'));
|
|
2170
|
+
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, null);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
async _remoteConfigAsync() {
|
|
2174
|
+
this._remoteConfigResponsePromise = this._initPromise
|
|
2175
|
+
.then(() => {
|
|
2176
|
+
let remoteConfig = this.getPersistedProperty(PostHogPersistedProperty.RemoteConfig);
|
|
2177
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Cached remote config: ', JSON.stringify(remoteConfig)));
|
|
2178
|
+
return super.getRemoteConfig().then((response) => {
|
|
2179
|
+
if (response) {
|
|
2180
|
+
const remoteConfigWithoutSurveys = { ...response };
|
|
2181
|
+
delete remoteConfigWithoutSurveys.surveys;
|
|
2182
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Fetched remote config: ', JSON.stringify(remoteConfigWithoutSurveys)));
|
|
2183
|
+
const surveys = response.surveys;
|
|
2184
|
+
let hasSurveys = true;
|
|
2185
|
+
if (!Array.isArray(surveys)) {
|
|
2186
|
+
// If surveys is not an array, it means there are no surveys (its a boolean instead)
|
|
2187
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'There are no surveys.'));
|
|
2188
|
+
hasSurveys = false;
|
|
2189
|
+
}
|
|
2190
|
+
else {
|
|
2191
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Surveys fetched from remote config: ', JSON.stringify(surveys)));
|
|
2192
|
+
}
|
|
2193
|
+
if (this.disableSurveys === false && hasSurveys) {
|
|
2194
|
+
this.setPersistedProperty(PostHogPersistedProperty.Surveys, surveys);
|
|
2195
|
+
}
|
|
2196
|
+
else {
|
|
2197
|
+
this.setPersistedProperty(PostHogPersistedProperty.Surveys, null);
|
|
2198
|
+
}
|
|
2199
|
+
// we cache the surveys in its own storage key
|
|
2200
|
+
this.setPersistedProperty(PostHogPersistedProperty.RemoteConfig, remoteConfigWithoutSurveys);
|
|
2201
|
+
this.cacheSessionReplay(response);
|
|
2202
|
+
// we only dont load flags if the remote config has no feature flags
|
|
2203
|
+
if (response.hasFeatureFlags === false) {
|
|
2204
|
+
// resetting flags to empty object
|
|
2205
|
+
this.setKnownFeatureFlagDetails({ flags: {} });
|
|
2206
|
+
this.logMsgIfDebug(() => console.warn('Remote config has no feature flags, will not load feature flags.'));
|
|
2207
|
+
}
|
|
2208
|
+
else if (this.preloadFeatureFlags !== false) {
|
|
2209
|
+
this.reloadFeatureFlags();
|
|
2210
|
+
}
|
|
2211
|
+
remoteConfig = response;
|
|
2212
|
+
}
|
|
2213
|
+
return remoteConfig;
|
|
2214
|
+
});
|
|
2215
|
+
})
|
|
2216
|
+
.finally(() => {
|
|
2217
|
+
this._remoteConfigResponsePromise = undefined;
|
|
2218
|
+
});
|
|
2219
|
+
return this._remoteConfigResponsePromise;
|
|
2220
|
+
}
|
|
1750
2221
|
async _decideAsync(sendAnonDistinctId = true) {
|
|
1751
2222
|
this._decideResponsePromise = this._initPromise
|
|
1752
|
-
.then(() => {
|
|
2223
|
+
.then(async () => {
|
|
1753
2224
|
const distinctId = this.getDistinctId();
|
|
1754
2225
|
const groups = this.props.$groups || {};
|
|
1755
2226
|
const personProperties = this.getPersistedProperty(PostHogPersistedProperty.PersonProperties) || {};
|
|
@@ -1758,76 +2229,126 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1758
2229
|
const extraProperties = {
|
|
1759
2230
|
$anon_distinct_id: sendAnonDistinctId ? this.getAnonymousId() : undefined,
|
|
1760
2231
|
};
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
let newFeatureFlags = res.featureFlags;
|
|
1768
|
-
let newFeatureFlagPayloads = res.featureFlagPayloads;
|
|
1769
|
-
if (res.errorsWhileComputingFlags) {
|
|
1770
|
-
// if not all flags were computed, we upsert flags instead of replacing them
|
|
1771
|
-
const currentFlags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags);
|
|
1772
|
-
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Cached feature flags: ', JSON.stringify(currentFlags)));
|
|
1773
|
-
const currentFlagPayloads = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads);
|
|
1774
|
-
newFeatureFlags = { ...currentFlags, ...res.featureFlags };
|
|
1775
|
-
newFeatureFlagPayloads = { ...currentFlagPayloads, ...res.featureFlagPayloads };
|
|
1776
|
-
}
|
|
1777
|
-
this.setKnownFeatureFlags(newFeatureFlags);
|
|
1778
|
-
this.setKnownFeatureFlagPayloads(Object.fromEntries(Object.entries(newFeatureFlagPayloads || {}).map(([k, v]) => [k, this._parsePayload(v)])));
|
|
1779
|
-
// Mark that we hit the /decide endpoint so we can capture this in the $feature_flag_called event
|
|
1780
|
-
this.setPersistedProperty(PostHogPersistedProperty.DecideEndpointWasHit, true);
|
|
1781
|
-
const sessionReplay = res?.sessionRecording;
|
|
1782
|
-
if (sessionReplay) {
|
|
1783
|
-
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, sessionReplay);
|
|
1784
|
-
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Session replay config: ', JSON.stringify(sessionReplay)));
|
|
1785
|
-
}
|
|
1786
|
-
else {
|
|
1787
|
-
this.logMsgIfDebug(() => console.info('PostHog Debug', 'Session replay config disabled.'));
|
|
1788
|
-
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, null);
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
2232
|
+
const res = await super.getDecide(distinctId, groups, personProperties, groupProperties, extraProperties);
|
|
2233
|
+
// Add check for quota limitation on feature flags
|
|
2234
|
+
if (res?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
|
|
2235
|
+
// Unset all feature flags by setting to null
|
|
2236
|
+
this.setKnownFeatureFlagDetails(null);
|
|
2237
|
+
console.warn('[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts');
|
|
1791
2238
|
return res;
|
|
1792
|
-
}
|
|
2239
|
+
}
|
|
2240
|
+
if (res?.featureFlags) {
|
|
2241
|
+
// clear flag call reported if we have new flags since they might have changed
|
|
2242
|
+
if (this.sendFeatureFlagEvent) {
|
|
2243
|
+
this.flagCallReported = {};
|
|
2244
|
+
}
|
|
2245
|
+
let newFeatureFlagDetails = res;
|
|
2246
|
+
if (res.errorsWhileComputingFlags) {
|
|
2247
|
+
// if not all flags were computed, we upsert flags instead of replacing them
|
|
2248
|
+
const currentFlagDetails = this.getKnownFeatureFlagDetails();
|
|
2249
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Cached feature flags: ', JSON.stringify(currentFlagDetails)));
|
|
2250
|
+
newFeatureFlagDetails = {
|
|
2251
|
+
...res,
|
|
2252
|
+
flags: { ...currentFlagDetails?.flags, ...res.flags },
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
this.setKnownFeatureFlagDetails(newFeatureFlagDetails);
|
|
2256
|
+
// Mark that we hit the /decide endpoint so we can capture this in the $feature_flag_called event
|
|
2257
|
+
this.setPersistedProperty(PostHogPersistedProperty.DecideEndpointWasHit, true);
|
|
2258
|
+
this.cacheSessionReplay(res);
|
|
2259
|
+
}
|
|
2260
|
+
return res;
|
|
1793
2261
|
})
|
|
1794
2262
|
.finally(() => {
|
|
1795
2263
|
this._decideResponsePromise = undefined;
|
|
1796
2264
|
});
|
|
1797
2265
|
return this._decideResponsePromise;
|
|
1798
2266
|
}
|
|
1799
|
-
|
|
2267
|
+
// We only store the flags and request id in the feature flag details storage key
|
|
2268
|
+
setKnownFeatureFlagDetails(decideResponse) {
|
|
1800
2269
|
this.wrap(() => {
|
|
1801
|
-
this.setPersistedProperty(PostHogPersistedProperty.
|
|
1802
|
-
this._events.emit('featureflags',
|
|
2270
|
+
this.setPersistedProperty(PostHogPersistedProperty.FeatureFlagDetails, decideResponse);
|
|
2271
|
+
this._events.emit('featureflags', getFlagValuesFromFlags(decideResponse?.flags ?? {}));
|
|
1803
2272
|
});
|
|
1804
2273
|
}
|
|
1805
|
-
|
|
1806
|
-
this.
|
|
1807
|
-
|
|
1808
|
-
|
|
2274
|
+
getKnownFeatureFlagDetails() {
|
|
2275
|
+
const storedDetails = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagDetails);
|
|
2276
|
+
if (!storedDetails) {
|
|
2277
|
+
// Rebuild from the stored feature flags and feature flag payloads
|
|
2278
|
+
const featureFlags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags);
|
|
2279
|
+
const featureFlagPayloads = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads);
|
|
2280
|
+
if (featureFlags === undefined && featureFlagPayloads === undefined) {
|
|
2281
|
+
return undefined;
|
|
2282
|
+
}
|
|
2283
|
+
return createDecideResponseFromFlagsAndPayloads(featureFlags ?? {}, featureFlagPayloads ?? {});
|
|
2284
|
+
}
|
|
2285
|
+
return normalizeDecideResponse(storedDetails);
|
|
2286
|
+
}
|
|
2287
|
+
getKnownFeatureFlags() {
|
|
2288
|
+
const featureFlagDetails = this.getKnownFeatureFlagDetails();
|
|
2289
|
+
if (!featureFlagDetails) {
|
|
2290
|
+
return undefined;
|
|
2291
|
+
}
|
|
2292
|
+
return getFlagValuesFromFlags(featureFlagDetails.flags);
|
|
2293
|
+
}
|
|
2294
|
+
getKnownFeatureFlagPayloads() {
|
|
2295
|
+
const featureFlagDetails = this.getKnownFeatureFlagDetails();
|
|
2296
|
+
if (!featureFlagDetails) {
|
|
2297
|
+
return undefined;
|
|
2298
|
+
}
|
|
2299
|
+
return getPayloadsFromFlags(featureFlagDetails.flags);
|
|
2300
|
+
}
|
|
2301
|
+
getBootstrappedFeatureFlagDetails() {
|
|
2302
|
+
const details = this.getPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlagDetails);
|
|
2303
|
+
if (!details) {
|
|
2304
|
+
return undefined;
|
|
2305
|
+
}
|
|
2306
|
+
return details;
|
|
2307
|
+
}
|
|
2308
|
+
setBootstrappedFeatureFlagDetails(details) {
|
|
2309
|
+
this.setPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlagDetails, details);
|
|
2310
|
+
}
|
|
2311
|
+
getBootstrappedFeatureFlags() {
|
|
2312
|
+
const details = this.getBootstrappedFeatureFlagDetails();
|
|
2313
|
+
if (!details) {
|
|
2314
|
+
return undefined;
|
|
2315
|
+
}
|
|
2316
|
+
return getFlagValuesFromFlags(details.flags);
|
|
2317
|
+
}
|
|
2318
|
+
getBootstrappedFeatureFlagPayloads() {
|
|
2319
|
+
const details = this.getBootstrappedFeatureFlagDetails();
|
|
2320
|
+
if (!details) {
|
|
2321
|
+
return undefined;
|
|
2322
|
+
}
|
|
2323
|
+
return getPayloadsFromFlags(details.flags);
|
|
1809
2324
|
}
|
|
1810
2325
|
getFeatureFlag(key) {
|
|
1811
|
-
const
|
|
1812
|
-
if (!
|
|
2326
|
+
const details = this.getFeatureFlagDetails();
|
|
2327
|
+
if (!details) {
|
|
1813
2328
|
// If we haven't loaded flags yet, or errored out, we respond with undefined
|
|
1814
2329
|
return undefined;
|
|
1815
2330
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
2331
|
+
const featureFlag = details.flags[key];
|
|
2332
|
+
let response = getFeatureFlagValue(featureFlag);
|
|
1818
2333
|
if (response === undefined) {
|
|
1819
2334
|
// For cases where the flag is unknown, return false
|
|
1820
2335
|
response = false;
|
|
1821
2336
|
}
|
|
1822
2337
|
if (this.sendFeatureFlagEvent && !this.flagCallReported[key]) {
|
|
2338
|
+
const bootstrappedResponse = this.getBootstrappedFeatureFlags()?.[key];
|
|
2339
|
+
const bootstrappedPayload = this.getBootstrappedFeatureFlagPayloads()?.[key];
|
|
1823
2340
|
this.flagCallReported[key] = true;
|
|
1824
2341
|
this.capture('$feature_flag_called', {
|
|
1825
2342
|
$feature_flag: key,
|
|
1826
2343
|
$feature_flag_response: response,
|
|
1827
|
-
$
|
|
1828
|
-
$
|
|
2344
|
+
$feature_flag_id: featureFlag?.metadata?.id,
|
|
2345
|
+
$feature_flag_version: featureFlag?.metadata?.version,
|
|
2346
|
+
$feature_flag_reason: featureFlag?.reason?.description ?? featureFlag?.reason?.code,
|
|
2347
|
+
$feature_flag_bootstrapped_response: bootstrappedResponse,
|
|
2348
|
+
$feature_flag_bootstrapped_payload: bootstrappedPayload,
|
|
1829
2349
|
// If we haven't yet received a response from the /decide endpoint, we must have used the bootstrapped value
|
|
1830
2350
|
$used_bootstrap_value: !this.getPersistedProperty(PostHogPersistedProperty.DecideEndpointWasHit),
|
|
2351
|
+
$feature_flag_request_id: details.requestId,
|
|
1831
2352
|
});
|
|
1832
2353
|
}
|
|
1833
2354
|
// If we have flags we either return the value (true or string) or false
|
|
@@ -1846,27 +2367,36 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1846
2367
|
return response;
|
|
1847
2368
|
}
|
|
1848
2369
|
getFeatureFlagPayloads() {
|
|
1849
|
-
|
|
1850
|
-
return payloads;
|
|
2370
|
+
return this.getFeatureFlagDetails()?.featureFlagPayloads;
|
|
1851
2371
|
}
|
|
1852
2372
|
getFeatureFlags() {
|
|
1853
2373
|
// NOTE: We don't check for _initPromise here as the function is designed to be
|
|
1854
2374
|
// callable before the state being loaded anyways
|
|
1855
|
-
|
|
2375
|
+
return this.getFeatureFlagDetails()?.featureFlags;
|
|
2376
|
+
}
|
|
2377
|
+
getFeatureFlagDetails() {
|
|
2378
|
+
// NOTE: We don't check for _initPromise here as the function is designed to be
|
|
2379
|
+
// callable before the state being loaded anyways
|
|
2380
|
+
let details = this.getKnownFeatureFlagDetails();
|
|
1856
2381
|
const overriddenFlags = this.getPersistedProperty(PostHogPersistedProperty.OverrideFeatureFlags);
|
|
1857
2382
|
if (!overriddenFlags) {
|
|
1858
|
-
return
|
|
2383
|
+
return details;
|
|
1859
2384
|
}
|
|
1860
|
-
|
|
2385
|
+
details = details ?? { featureFlags: {}, featureFlagPayloads: {}, flags: {} };
|
|
2386
|
+
const flags = details.flags ?? {};
|
|
1861
2387
|
for (const key in overriddenFlags) {
|
|
1862
2388
|
if (!overriddenFlags[key]) {
|
|
1863
2389
|
delete flags[key];
|
|
1864
2390
|
}
|
|
1865
2391
|
else {
|
|
1866
|
-
flags[key] = overriddenFlags[key];
|
|
2392
|
+
flags[key] = updateFlagValue(flags[key], overriddenFlags[key]);
|
|
1867
2393
|
}
|
|
1868
2394
|
}
|
|
1869
|
-
|
|
2395
|
+
const result = {
|
|
2396
|
+
...details,
|
|
2397
|
+
flags,
|
|
2398
|
+
};
|
|
2399
|
+
return normalizeDecideResponse(result);
|
|
1870
2400
|
}
|
|
1871
2401
|
getFeatureFlagsAndPayloads() {
|
|
1872
2402
|
const flags = this.getFeatureFlags();
|
|
@@ -1896,6 +2426,9 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1896
2426
|
}
|
|
1897
2427
|
});
|
|
1898
2428
|
}
|
|
2429
|
+
async reloadRemoteConfigAsync() {
|
|
2430
|
+
return await this.remoteConfigAsync();
|
|
2431
|
+
}
|
|
1899
2432
|
async reloadFeatureFlagsAsync(sendAnonDistinctId = true) {
|
|
1900
2433
|
return (await this.decideAsync(sendAnonDistinctId))?.featureFlags;
|
|
1901
2434
|
}
|
|
@@ -1970,7 +2503,7 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1970
2503
|
}
|
|
1971
2504
|
}
|
|
1972
2505
|
|
|
1973
|
-
var version = "3.
|
|
2506
|
+
var version = "3.5.0";
|
|
1974
2507
|
|
|
1975
2508
|
function getContext(window) {
|
|
1976
2509
|
let context = {};
|
|
@@ -2275,9 +2808,45 @@ const getStorage = (type, window) => {
|
|
|
2275
2808
|
}
|
|
2276
2809
|
};
|
|
2277
2810
|
|
|
2811
|
+
// import { patch } from 'rrweb/typings/utils'
|
|
2812
|
+
function patch(source, name, replacement) {
|
|
2813
|
+
try {
|
|
2814
|
+
if (!(name in source)) {
|
|
2815
|
+
return () => {
|
|
2816
|
+
//
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
const original = source[name];
|
|
2820
|
+
const wrapped = replacement(original);
|
|
2821
|
+
// Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work
|
|
2822
|
+
// otherwise it'll throw "TypeError: Object.defineProperties called on non-object"
|
|
2823
|
+
if (isFunction(wrapped)) {
|
|
2824
|
+
wrapped.prototype = wrapped.prototype || {};
|
|
2825
|
+
Object.defineProperties(wrapped, {
|
|
2826
|
+
__posthog_wrapped__: {
|
|
2827
|
+
enumerable: false,
|
|
2828
|
+
value: true,
|
|
2829
|
+
},
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
source[name] = wrapped;
|
|
2833
|
+
return () => {
|
|
2834
|
+
source[name] = original;
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
catch {
|
|
2838
|
+
return () => {
|
|
2839
|
+
//
|
|
2840
|
+
};
|
|
2841
|
+
// This can throw if multiple fill happens on a global object like XMLHttpRequest
|
|
2842
|
+
// Fixes https://github.com/getsentry/sentry-javascript/issues/2043
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2278
2846
|
class PostHog extends PostHogCore {
|
|
2279
2847
|
constructor(apiKey, options) {
|
|
2280
2848
|
super(apiKey, options);
|
|
2849
|
+
this._lastPathname = '';
|
|
2281
2850
|
// posthog-js stores options in one object on
|
|
2282
2851
|
this._storageKey = options?.persistence_name ? `ph_${options.persistence_name}` : `ph_${apiKey}_posthog`;
|
|
2283
2852
|
this._storage = getStorage(options?.persistence || 'localStorage', this.getWindow());
|
|
@@ -2285,6 +2854,10 @@ class PostHog extends PostHogCore {
|
|
|
2285
2854
|
if (options?.preloadFeatureFlags !== false) {
|
|
2286
2855
|
this.reloadFeatureFlags();
|
|
2287
2856
|
}
|
|
2857
|
+
if (options?.captureHistoryEvents && typeof window !== 'undefined') {
|
|
2858
|
+
this._lastPathname = window?.location?.pathname || '';
|
|
2859
|
+
this.setupHistoryEventTracking();
|
|
2860
|
+
}
|
|
2288
2861
|
}
|
|
2289
2862
|
getWindow() {
|
|
2290
2863
|
return typeof window !== 'undefined' ? window : undefined;
|
|
@@ -2329,6 +2902,47 @@ class PostHog extends PostHogCore {
|
|
|
2329
2902
|
...getContext(this.getWindow())
|
|
2330
2903
|
};
|
|
2331
2904
|
}
|
|
2905
|
+
setupHistoryEventTracking() {
|
|
2906
|
+
const window = this.getWindow();
|
|
2907
|
+
if (!window) {
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
// Old fashioned, we could also use arrow functions but I think the closure for a patch is more reliable
|
|
2911
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
2912
|
+
const self = this;
|
|
2913
|
+
patch(window.history, 'pushState', originalPushState => {
|
|
2914
|
+
return function patchedPushState(state, title, url) {
|
|
2915
|
+
;
|
|
2916
|
+
originalPushState.call(this, state, title, url);
|
|
2917
|
+
self.captureNavigationEvent('pushState');
|
|
2918
|
+
};
|
|
2919
|
+
});
|
|
2920
|
+
patch(window.history, 'replaceState', originalReplaceState => {
|
|
2921
|
+
return function patchedReplaceState(state, title, url) {
|
|
2922
|
+
;
|
|
2923
|
+
originalReplaceState.call(this, state, title, url);
|
|
2924
|
+
self.captureNavigationEvent('replaceState');
|
|
2925
|
+
};
|
|
2926
|
+
});
|
|
2927
|
+
// For popstate we need to listen to the event instead of overriding a method
|
|
2928
|
+
window.addEventListener('popstate', () => {
|
|
2929
|
+
this.captureNavigationEvent('popstate');
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
captureNavigationEvent(navigationType) {
|
|
2933
|
+
const window = this.getWindow();
|
|
2934
|
+
if (!window) {
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
const currentPathname = window.location.pathname;
|
|
2938
|
+
// Only capture pageview if the pathname has changed
|
|
2939
|
+
if (currentPathname !== this._lastPathname) {
|
|
2940
|
+
this.capture('$pageview', {
|
|
2941
|
+
navigation_type: navigationType
|
|
2942
|
+
});
|
|
2943
|
+
this._lastPathname = currentPathname;
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2332
2946
|
}
|
|
2333
2947
|
|
|
2334
2948
|
exports.PostHog = PostHog;
|