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