posthog-js-lite 3.4.2 → 3.5.1
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 +19 -0
- package/lib/{index.cjs.js → index.cjs} +950 -199
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.ts +661 -350
- package/lib/{index.esm.js → index.mjs} +950 -199
- package/lib/index.mjs.map +1 -0
- package/package.json +3 -3
- package/src/context.ts +1 -1
- package/src/patch.ts +50 -0
- package/src/posthog-web.ts +57 -8
- package/src/types.ts +2 -1
- package/test/posthog-web.spec.ts +193 -2
- package/tsconfig.json +1 -0
- package/lib/index.cjs.js.map +0 -1
- package/lib/index.esm.js.map +0 -1
- package/lib/posthog-core/src/eventemitter.d.ts +0 -8
- package/lib/posthog-core/src/index.d.ts +0 -207
- package/lib/posthog-core/src/lz-string.d.ts +0 -8
- package/lib/posthog-core/src/types.d.ts +0 -133
- package/lib/posthog-core/src/utils.d.ts +0 -15
- package/lib/posthog-core/src/vendor/uuidv7.d.ts +0 -179
- package/lib/posthog-web/index.d.ts +0 -3
- package/lib/posthog-web/src/context.d.ts +0 -1
- package/lib/posthog-web/src/posthog-web.d.ts +0 -16
- package/lib/posthog-web/src/storage.d.ts +0 -10
- package/lib/posthog-web/src/types.d.ts +0 -6
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
var version = "3.5.1";
|
|
2
|
+
|
|
1
3
|
var PostHogPersistedProperty;
|
|
2
4
|
(function (PostHogPersistedProperty) {
|
|
3
5
|
PostHogPersistedProperty["AnonymousId"] = "anonymous_id";
|
|
4
6
|
PostHogPersistedProperty["DistinctId"] = "distinct_id";
|
|
5
7
|
PostHogPersistedProperty["Props"] = "props";
|
|
8
|
+
PostHogPersistedProperty["FeatureFlagDetails"] = "feature_flag_details";
|
|
6
9
|
PostHogPersistedProperty["FeatureFlags"] = "feature_flags";
|
|
7
10
|
PostHogPersistedProperty["FeatureFlagPayloads"] = "feature_flag_payloads";
|
|
11
|
+
PostHogPersistedProperty["BootstrapFeatureFlagDetails"] = "bootstrap_feature_flag_details";
|
|
8
12
|
PostHogPersistedProperty["BootstrapFeatureFlags"] = "bootstrap_feature_flags";
|
|
9
13
|
PostHogPersistedProperty["BootstrapFeatureFlagPayloads"] = "bootstrap_feature_flag_payloads";
|
|
10
14
|
PostHogPersistedProperty["OverrideFeatureFlags"] = "override_feature_flags";
|
|
@@ -18,13 +22,285 @@ var PostHogPersistedProperty;
|
|
|
18
22
|
PostHogPersistedProperty["InstalledAppVersion"] = "installed_app_version";
|
|
19
23
|
PostHogPersistedProperty["SessionReplay"] = "session_replay";
|
|
20
24
|
PostHogPersistedProperty["DecideEndpointWasHit"] = "decide_endpoint_was_hit";
|
|
21
|
-
|
|
25
|
+
PostHogPersistedProperty["SurveyLastSeenDate"] = "survey_last_seen_date";
|
|
26
|
+
PostHogPersistedProperty["SurveysSeen"] = "surveys_seen";
|
|
27
|
+
PostHogPersistedProperty["Surveys"] = "surveys";
|
|
28
|
+
PostHogPersistedProperty["RemoteConfig"] = "remote_config";
|
|
29
|
+
})(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
|
|
30
|
+
var SurveyPosition;
|
|
31
|
+
(function (SurveyPosition) {
|
|
32
|
+
SurveyPosition["Left"] = "left";
|
|
33
|
+
SurveyPosition["Right"] = "right";
|
|
34
|
+
SurveyPosition["Center"] = "center";
|
|
35
|
+
})(SurveyPosition || (SurveyPosition = {}));
|
|
36
|
+
var SurveyWidgetType;
|
|
37
|
+
(function (SurveyWidgetType) {
|
|
38
|
+
SurveyWidgetType["Button"] = "button";
|
|
39
|
+
SurveyWidgetType["Tab"] = "tab";
|
|
40
|
+
SurveyWidgetType["Selector"] = "selector";
|
|
41
|
+
})(SurveyWidgetType || (SurveyWidgetType = {}));
|
|
42
|
+
var SurveyType;
|
|
43
|
+
(function (SurveyType) {
|
|
44
|
+
SurveyType["Popover"] = "popover";
|
|
45
|
+
SurveyType["API"] = "api";
|
|
46
|
+
SurveyType["Widget"] = "widget";
|
|
47
|
+
})(SurveyType || (SurveyType = {}));
|
|
48
|
+
var SurveyQuestionDescriptionContentType;
|
|
49
|
+
(function (SurveyQuestionDescriptionContentType) {
|
|
50
|
+
SurveyQuestionDescriptionContentType["Html"] = "html";
|
|
51
|
+
SurveyQuestionDescriptionContentType["Text"] = "text";
|
|
52
|
+
})(SurveyQuestionDescriptionContentType || (SurveyQuestionDescriptionContentType = {}));
|
|
53
|
+
var SurveyRatingDisplay;
|
|
54
|
+
(function (SurveyRatingDisplay) {
|
|
55
|
+
SurveyRatingDisplay["Number"] = "number";
|
|
56
|
+
SurveyRatingDisplay["Emoji"] = "emoji";
|
|
57
|
+
})(SurveyRatingDisplay || (SurveyRatingDisplay = {}));
|
|
58
|
+
var SurveyQuestionType;
|
|
59
|
+
(function (SurveyQuestionType) {
|
|
60
|
+
SurveyQuestionType["Open"] = "open";
|
|
61
|
+
SurveyQuestionType["MultipleChoice"] = "multiple_choice";
|
|
62
|
+
SurveyQuestionType["SingleChoice"] = "single_choice";
|
|
63
|
+
SurveyQuestionType["Rating"] = "rating";
|
|
64
|
+
SurveyQuestionType["Link"] = "link";
|
|
65
|
+
})(SurveyQuestionType || (SurveyQuestionType = {}));
|
|
66
|
+
var SurveyQuestionBranchingType;
|
|
67
|
+
(function (SurveyQuestionBranchingType) {
|
|
68
|
+
SurveyQuestionBranchingType["NextQuestion"] = "next_question";
|
|
69
|
+
SurveyQuestionBranchingType["End"] = "end";
|
|
70
|
+
SurveyQuestionBranchingType["ResponseBased"] = "response_based";
|
|
71
|
+
SurveyQuestionBranchingType["SpecificQuestion"] = "specific_question";
|
|
72
|
+
})(SurveyQuestionBranchingType || (SurveyQuestionBranchingType = {}));
|
|
73
|
+
var SurveyMatchType;
|
|
74
|
+
(function (SurveyMatchType) {
|
|
75
|
+
SurveyMatchType["Regex"] = "regex";
|
|
76
|
+
SurveyMatchType["NotRegex"] = "not_regex";
|
|
77
|
+
SurveyMatchType["Exact"] = "exact";
|
|
78
|
+
SurveyMatchType["IsNot"] = "is_not";
|
|
79
|
+
SurveyMatchType["Icontains"] = "icontains";
|
|
80
|
+
SurveyMatchType["NotIcontains"] = "not_icontains";
|
|
81
|
+
})(SurveyMatchType || (SurveyMatchType = {}));
|
|
82
|
+
/** Sync with plugin-server/src/types.ts */
|
|
83
|
+
var ActionStepStringMatching;
|
|
84
|
+
(function (ActionStepStringMatching) {
|
|
85
|
+
ActionStepStringMatching["Contains"] = "contains";
|
|
86
|
+
ActionStepStringMatching["Exact"] = "exact";
|
|
87
|
+
ActionStepStringMatching["Regex"] = "regex";
|
|
88
|
+
})(ActionStepStringMatching || (ActionStepStringMatching = {}));
|
|
22
89
|
|
|
90
|
+
const normalizeDecideResponse = (decideResponse) => {
|
|
91
|
+
if ('flags' in decideResponse) {
|
|
92
|
+
// Convert v4 format to v3 format
|
|
93
|
+
const featureFlags = getFlagValuesFromFlags(decideResponse.flags);
|
|
94
|
+
const featureFlagPayloads = getPayloadsFromFlags(decideResponse.flags);
|
|
95
|
+
return {
|
|
96
|
+
...decideResponse,
|
|
97
|
+
featureFlags,
|
|
98
|
+
featureFlagPayloads,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Convert v3 format to v4 format
|
|
103
|
+
const featureFlags = decideResponse.featureFlags ?? {};
|
|
104
|
+
const featureFlagPayloads = Object.fromEntries(Object.entries(decideResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
|
|
105
|
+
const flags = Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [
|
|
106
|
+
key,
|
|
107
|
+
getFlagDetailFromFlagAndPayload(key, value, featureFlagPayloads[key]),
|
|
108
|
+
]));
|
|
109
|
+
return {
|
|
110
|
+
...decideResponse,
|
|
111
|
+
featureFlags,
|
|
112
|
+
featureFlagPayloads,
|
|
113
|
+
flags,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function getFlagDetailFromFlagAndPayload(key, value, payload) {
|
|
118
|
+
return {
|
|
119
|
+
key: key,
|
|
120
|
+
enabled: typeof value === 'string' ? true : value,
|
|
121
|
+
variant: typeof value === 'string' ? value : undefined,
|
|
122
|
+
reason: undefined,
|
|
123
|
+
metadata: {
|
|
124
|
+
id: undefined,
|
|
125
|
+
version: undefined,
|
|
126
|
+
payload: payload ? JSON.stringify(payload) : undefined,
|
|
127
|
+
description: undefined,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get the flag values from the flags v4 response.
|
|
133
|
+
* @param flags - The flags
|
|
134
|
+
* @returns The flag values
|
|
135
|
+
*/
|
|
136
|
+
const getFlagValuesFromFlags = (flags) => {
|
|
137
|
+
return Object.fromEntries(Object.entries(flags ?? {})
|
|
138
|
+
.map(([key, detail]) => [key, getFeatureFlagValue(detail)])
|
|
139
|
+
.filter(([, value]) => value !== undefined));
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Get the payloads from the flags v4 response.
|
|
143
|
+
* @param flags - The flags
|
|
144
|
+
* @returns The payloads
|
|
145
|
+
*/
|
|
146
|
+
const getPayloadsFromFlags = (flags) => {
|
|
147
|
+
const safeFlags = flags ?? {};
|
|
148
|
+
return Object.fromEntries(Object.keys(safeFlags)
|
|
149
|
+
.filter((flag) => {
|
|
150
|
+
const details = safeFlags[flag];
|
|
151
|
+
return details.enabled && details.metadata && details.metadata.payload !== undefined;
|
|
152
|
+
})
|
|
153
|
+
.map((flag) => {
|
|
154
|
+
const payload = safeFlags[flag].metadata?.payload;
|
|
155
|
+
return [flag, payload ? parsePayload(payload) : undefined];
|
|
156
|
+
}));
|
|
157
|
+
};
|
|
158
|
+
const getFeatureFlagValue = (detail) => {
|
|
159
|
+
return detail === undefined ? undefined : detail.variant ?? detail.enabled;
|
|
160
|
+
};
|
|
161
|
+
const parsePayload = (response) => {
|
|
162
|
+
if (typeof response !== 'string') {
|
|
163
|
+
return response;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(response);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return response;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Get the normalized flag details from the flags and payloads.
|
|
174
|
+
* This is used to convert things like boostrap and stored feature flags and payloads to the v4 format.
|
|
175
|
+
* This helps us ensure backwards compatibility.
|
|
176
|
+
* If a key exists in the featureFlagPayloads that is not in the featureFlags, we treat it as a true feature flag.
|
|
177
|
+
*
|
|
178
|
+
* @param featureFlags - The feature flags
|
|
179
|
+
* @param featureFlagPayloads - The feature flag payloads
|
|
180
|
+
* @returns The normalized flag details
|
|
181
|
+
*/
|
|
182
|
+
const createDecideResponseFromFlagsAndPayloads = (featureFlags, featureFlagPayloads) => {
|
|
183
|
+
// If a feature flag payload key is not in the feature flags, we treat it as true feature flag.
|
|
184
|
+
const allKeys = [...new Set([...Object.keys(featureFlags ?? {}), ...Object.keys(featureFlagPayloads ?? {})])];
|
|
185
|
+
const enabledFlags = allKeys
|
|
186
|
+
.filter((flag) => !!featureFlags[flag] || !!featureFlagPayloads[flag])
|
|
187
|
+
.reduce((res, key) => ((res[key] = featureFlags[key] ?? true), res), {});
|
|
188
|
+
const flagDetails = {
|
|
189
|
+
featureFlags: enabledFlags,
|
|
190
|
+
featureFlagPayloads: featureFlagPayloads ?? {},
|
|
191
|
+
};
|
|
192
|
+
return normalizeDecideResponse(flagDetails);
|
|
193
|
+
};
|
|
194
|
+
const updateFlagValue = (flag, value) => {
|
|
195
|
+
return {
|
|
196
|
+
...flag,
|
|
197
|
+
enabled: getEnabledFromValue(value),
|
|
198
|
+
variant: getVariantFromValue(value),
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
function getEnabledFromValue(value) {
|
|
202
|
+
return typeof value === 'string' ? true : value;
|
|
203
|
+
}
|
|
204
|
+
function getVariantFromValue(value) {
|
|
205
|
+
return typeof value === 'string' ? value : undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Rollout constants
|
|
209
|
+
const NEW_FLAGS_ROLLOUT_PERCENTAGE = 1;
|
|
210
|
+
// The fnv1a hashes of the tokens that are explicitly excluded from the rollout
|
|
211
|
+
// see https://github.com/PostHog/posthog-js-lite/blob/main/posthog-core/src/utils.ts#L84
|
|
212
|
+
// are hashed API tokens from our top 10 for each category supported by this SDK.
|
|
213
|
+
const NEW_FLAGS_EXCLUDED_HASHES = new Set([
|
|
214
|
+
// Node
|
|
215
|
+
'61be3dd8',
|
|
216
|
+
'96f6df5f',
|
|
217
|
+
'8cfdba9b',
|
|
218
|
+
'bf027177',
|
|
219
|
+
'e59430a8',
|
|
220
|
+
'7fa5500b',
|
|
221
|
+
'569798e9',
|
|
222
|
+
'04809ff7',
|
|
223
|
+
'0ebc61a5',
|
|
224
|
+
'32de7f98',
|
|
225
|
+
'3beeb69a',
|
|
226
|
+
'12d34ad9',
|
|
227
|
+
'733853ec',
|
|
228
|
+
'0645bb64',
|
|
229
|
+
'5dcbee21',
|
|
230
|
+
'b1f95fa3',
|
|
231
|
+
'2189e408',
|
|
232
|
+
'82b460c2',
|
|
233
|
+
'3a8cc979',
|
|
234
|
+
'29ef8843',
|
|
235
|
+
'2cdbf767',
|
|
236
|
+
'38084b54',
|
|
237
|
+
// React Native
|
|
238
|
+
'50f9f8de',
|
|
239
|
+
'41d0df91',
|
|
240
|
+
'5c236689',
|
|
241
|
+
'c11aedd3',
|
|
242
|
+
'ada46672',
|
|
243
|
+
'f4331ee1',
|
|
244
|
+
'42fed62a',
|
|
245
|
+
'c957462c',
|
|
246
|
+
'd62f705a',
|
|
247
|
+
// Web (lots of teams per org, hence lots of API tokens)
|
|
248
|
+
'e0162666',
|
|
249
|
+
'01b3e5cf',
|
|
250
|
+
'441cef7f',
|
|
251
|
+
'bb9cafee',
|
|
252
|
+
'8f348eb0',
|
|
253
|
+
'b2553f3a',
|
|
254
|
+
'97469d7d',
|
|
255
|
+
'39f21a76',
|
|
256
|
+
'03706dcc',
|
|
257
|
+
'27d50569',
|
|
258
|
+
'307584a7',
|
|
259
|
+
'6433e92e',
|
|
260
|
+
'150c7fbb',
|
|
261
|
+
'49f57f22',
|
|
262
|
+
'3772f65b',
|
|
263
|
+
'01eb8256',
|
|
264
|
+
'3c9e9234',
|
|
265
|
+
'f853c7f7',
|
|
266
|
+
'c0ac4b67',
|
|
267
|
+
'cd609d40',
|
|
268
|
+
'10ca9b1a',
|
|
269
|
+
'8a87f11b',
|
|
270
|
+
'8e8e5216',
|
|
271
|
+
'1f6b63b3',
|
|
272
|
+
'db7943dd',
|
|
273
|
+
'79b7164c',
|
|
274
|
+
'07f78e33',
|
|
275
|
+
'2d21b6fd',
|
|
276
|
+
'952db5ee',
|
|
277
|
+
'a7d3b43f',
|
|
278
|
+
'1924dd9c',
|
|
279
|
+
'84e1b8f6',
|
|
280
|
+
'dff631b6',
|
|
281
|
+
'c5aa8a79',
|
|
282
|
+
'fa133a95',
|
|
283
|
+
'498a4508',
|
|
284
|
+
'24748755',
|
|
285
|
+
'98f3d658',
|
|
286
|
+
'21bbda67',
|
|
287
|
+
'7dbfed69',
|
|
288
|
+
'be3ec24c',
|
|
289
|
+
'fc80b8e2',
|
|
290
|
+
'75cc0998',
|
|
291
|
+
]);
|
|
292
|
+
const STRING_FORMAT = 'utf8';
|
|
23
293
|
function assert(truthyValue, message) {
|
|
24
|
-
if (!truthyValue) {
|
|
294
|
+
if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
|
|
25
295
|
throw new Error(message);
|
|
26
296
|
}
|
|
27
297
|
}
|
|
298
|
+
function isEmpty(truthyValue) {
|
|
299
|
+
if (truthyValue.trim().length === 0) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
28
304
|
function removeTrailingSlash(url) {
|
|
29
305
|
return url?.replace(/\/+$/, '');
|
|
30
306
|
}
|
|
@@ -66,7 +342,31 @@ const isError = (x) => {
|
|
|
66
342
|
return x instanceof Error;
|
|
67
343
|
};
|
|
68
344
|
function getFetch() {
|
|
69
|
-
return typeof fetch !== 'undefined' ? fetch : typeof
|
|
345
|
+
return typeof fetch !== 'undefined' ? fetch : typeof globalThis.fetch !== 'undefined' ? globalThis.fetch : undefined;
|
|
346
|
+
}
|
|
347
|
+
// FNV-1a hash function
|
|
348
|
+
// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
|
|
349
|
+
// I know, I know, I'm rolling my own hash function, but I didn't want to take on
|
|
350
|
+
// a crypto dependency and this is just temporary anyway
|
|
351
|
+
function fnv1a(str) {
|
|
352
|
+
let hash = 0x811c9dc5; // FNV offset basis
|
|
353
|
+
for (let i = 0; i < str.length; i++) {
|
|
354
|
+
hash ^= str.charCodeAt(i);
|
|
355
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
356
|
+
}
|
|
357
|
+
// Convert to hex string, padding to 8 chars
|
|
358
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
359
|
+
}
|
|
360
|
+
function isTokenInRollout(token, percentage = 0, excludedHashes) {
|
|
361
|
+
const tokenHash = fnv1a(token);
|
|
362
|
+
// Check excluded hashes (we're explicitly including these tokens from the rollout)
|
|
363
|
+
if (excludedHashes?.has(tokenHash)) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
// Convert hash to int and divide by max value to get number between 0-1
|
|
367
|
+
const hashInt = parseInt(tokenHash, 16);
|
|
368
|
+
const hashFloat = hashInt / 0xffffffff;
|
|
369
|
+
return hashFloat < percentage;
|
|
70
370
|
}
|
|
71
371
|
|
|
72
372
|
// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
|
|
@@ -931,11 +1231,21 @@ const uuidv7 = () => uuidv7obj().toString();
|
|
|
931
1231
|
const uuidv7obj = () => (defaultGenerator || (defaultGenerator = new V7Generator())).generate();
|
|
932
1232
|
|
|
933
1233
|
class PostHogFetchHttpError extends Error {
|
|
934
|
-
constructor(response) {
|
|
935
|
-
super('HTTP error while fetching PostHog: ' + response.status);
|
|
1234
|
+
constructor(response, reqByteLength) {
|
|
1235
|
+
super('HTTP error while fetching PostHog: status=' + response.status + ', reqByteLength=' + reqByteLength);
|
|
936
1236
|
this.response = response;
|
|
1237
|
+
this.reqByteLength = reqByteLength;
|
|
937
1238
|
this.name = 'PostHogFetchHttpError';
|
|
938
1239
|
}
|
|
1240
|
+
get status() {
|
|
1241
|
+
return this.response.status;
|
|
1242
|
+
}
|
|
1243
|
+
get text() {
|
|
1244
|
+
return this.response.text();
|
|
1245
|
+
}
|
|
1246
|
+
get json() {
|
|
1247
|
+
return this.response.json();
|
|
1248
|
+
}
|
|
939
1249
|
}
|
|
940
1250
|
class PostHogFetchNetworkError extends Error {
|
|
941
1251
|
constructor(error) {
|
|
@@ -947,9 +1257,26 @@ class PostHogFetchNetworkError extends Error {
|
|
|
947
1257
|
this.name = 'PostHogFetchNetworkError';
|
|
948
1258
|
}
|
|
949
1259
|
}
|
|
1260
|
+
async function logFlushError(err) {
|
|
1261
|
+
if (err instanceof PostHogFetchHttpError) {
|
|
1262
|
+
let text = '';
|
|
1263
|
+
try {
|
|
1264
|
+
text = await err.text;
|
|
1265
|
+
}
|
|
1266
|
+
catch { }
|
|
1267
|
+
console.error(`Error while flushing PostHog: message=${err.message}, response body=${text}`, err);
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
console.error('Error while flushing PostHog', err);
|
|
1271
|
+
}
|
|
1272
|
+
return Promise.resolve();
|
|
1273
|
+
}
|
|
950
1274
|
function isPostHogFetchError(err) {
|
|
951
1275
|
return typeof err === 'object' && (err instanceof PostHogFetchHttpError || err instanceof PostHogFetchNetworkError);
|
|
952
1276
|
}
|
|
1277
|
+
function isPostHogFetchContentTooLargeError(err) {
|
|
1278
|
+
return typeof err === 'object' && err instanceof PostHogFetchHttpError && err.status === 413;
|
|
1279
|
+
}
|
|
953
1280
|
var QuotaLimitedFeature;
|
|
954
1281
|
(function (QuotaLimitedFeature) {
|
|
955
1282
|
QuotaLimitedFeature["FeatureFlags"] = "feature_flags";
|
|
@@ -958,10 +1285,7 @@ var QuotaLimitedFeature;
|
|
|
958
1285
|
class PostHogCoreStateless {
|
|
959
1286
|
constructor(apiKey, options) {
|
|
960
1287
|
this.flushPromise = null;
|
|
961
|
-
this.
|
|
962
|
-
this.historicalMigration = false;
|
|
963
|
-
this.disabled = false;
|
|
964
|
-
this.defaultOptIn = true;
|
|
1288
|
+
this.shutdownPromise = null;
|
|
965
1289
|
this.pendingPromises = {};
|
|
966
1290
|
// internal
|
|
967
1291
|
this._events = new SimpleEventEmitter();
|
|
@@ -974,8 +1298,10 @@ class PostHogCoreStateless {
|
|
|
974
1298
|
this.maxQueueSize = Math.max(this.flushAt, options?.maxQueueSize ?? 1000);
|
|
975
1299
|
this.flushInterval = options?.flushInterval ?? 10000;
|
|
976
1300
|
this.captureMode = options?.captureMode || 'json';
|
|
1301
|
+
this.preloadFeatureFlags = options?.preloadFeatureFlags ?? true;
|
|
977
1302
|
// If enable is explicitly set to false we override the optout
|
|
978
1303
|
this.defaultOptIn = options?.defaultOptIn ?? true;
|
|
1304
|
+
this.disableSurveys = options?.disableSurveys ?? false;
|
|
979
1305
|
this._retryOptions = {
|
|
980
1306
|
retryCount: options?.fetchRetryCount ?? 3,
|
|
981
1307
|
retryDelay: options?.fetchRetryDelay ?? 3000,
|
|
@@ -983,6 +1309,7 @@ class PostHogCoreStateless {
|
|
|
983
1309
|
};
|
|
984
1310
|
this.requestTimeout = options?.requestTimeout ?? 10000; // 10 seconds
|
|
985
1311
|
this.featureFlagsRequestTimeoutMs = options?.featureFlagsRequestTimeoutMs ?? 3000; // 3 seconds
|
|
1312
|
+
this.remoteConfigRequestTimeoutMs = options?.remoteConfigRequestTimeoutMs ?? 3000; // 3 seconds
|
|
986
1313
|
this.disableGeoip = options?.disableGeoip ?? true;
|
|
987
1314
|
this.disabled = options?.disabled ?? false;
|
|
988
1315
|
this.historicalMigration = options?.historicalMigration ?? false;
|
|
@@ -1081,12 +1408,26 @@ class PostHogCoreStateless {
|
|
|
1081
1408
|
this.enqueue('identify', payload, options);
|
|
1082
1409
|
});
|
|
1083
1410
|
}
|
|
1411
|
+
async identifyStatelessImmediate(distinctId, properties, options) {
|
|
1412
|
+
const payload = {
|
|
1413
|
+
...this.buildPayload({
|
|
1414
|
+
distinct_id: distinctId,
|
|
1415
|
+
event: '$identify',
|
|
1416
|
+
properties,
|
|
1417
|
+
}),
|
|
1418
|
+
};
|
|
1419
|
+
await this.sendImmediate('identify', payload, options);
|
|
1420
|
+
}
|
|
1084
1421
|
captureStateless(distinctId, event, properties, options) {
|
|
1085
1422
|
this.wrap(() => {
|
|
1086
1423
|
const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
|
|
1087
1424
|
this.enqueue('capture', payload, options);
|
|
1088
1425
|
});
|
|
1089
1426
|
}
|
|
1427
|
+
async captureStatelessImmediate(distinctId, event, properties, options) {
|
|
1428
|
+
const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
|
|
1429
|
+
await this.sendImmediate('capture', payload, options);
|
|
1430
|
+
}
|
|
1090
1431
|
aliasStateless(alias, distinctId, properties, options) {
|
|
1091
1432
|
this.wrap(() => {
|
|
1092
1433
|
const payload = this.buildPayload({
|
|
@@ -1101,6 +1442,18 @@ class PostHogCoreStateless {
|
|
|
1101
1442
|
this.enqueue('alias', payload, options);
|
|
1102
1443
|
});
|
|
1103
1444
|
}
|
|
1445
|
+
async aliasStatelessImmediate(alias, distinctId, properties, options) {
|
|
1446
|
+
const payload = this.buildPayload({
|
|
1447
|
+
event: '$create_alias',
|
|
1448
|
+
distinct_id: distinctId,
|
|
1449
|
+
properties: {
|
|
1450
|
+
...(properties || {}),
|
|
1451
|
+
distinct_id: distinctId,
|
|
1452
|
+
alias,
|
|
1453
|
+
},
|
|
1454
|
+
});
|
|
1455
|
+
await this.sendImmediate('alias', payload, options);
|
|
1456
|
+
}
|
|
1104
1457
|
/***
|
|
1105
1458
|
*** GROUPS
|
|
1106
1459
|
***/
|
|
@@ -1119,12 +1472,39 @@ class PostHogCoreStateless {
|
|
|
1119
1472
|
this.enqueue('capture', payload, options);
|
|
1120
1473
|
});
|
|
1121
1474
|
}
|
|
1475
|
+
async getRemoteConfig() {
|
|
1476
|
+
await this._initPromise;
|
|
1477
|
+
let host = this.host;
|
|
1478
|
+
if (host === 'https://us.i.posthog.com') {
|
|
1479
|
+
host = 'https://us-assets.i.posthog.com';
|
|
1480
|
+
}
|
|
1481
|
+
else if (host === 'https://eu.i.posthog.com') {
|
|
1482
|
+
host = 'https://eu-assets.i.posthog.com';
|
|
1483
|
+
}
|
|
1484
|
+
const url = `${host}/array/${this.apiKey}/config`;
|
|
1485
|
+
const fetchOptions = {
|
|
1486
|
+
method: 'GET',
|
|
1487
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
1488
|
+
};
|
|
1489
|
+
// Don't retry remote config API calls
|
|
1490
|
+
return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.remoteConfigRequestTimeoutMs)
|
|
1491
|
+
.then((response) => response.json())
|
|
1492
|
+
.catch((error) => {
|
|
1493
|
+
this.logMsgIfDebug(() => console.error('Remote config could not be loaded', error));
|
|
1494
|
+
this._events.emit('error', error);
|
|
1495
|
+
return undefined;
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1122
1498
|
/***
|
|
1123
1499
|
*** FEATURE FLAGS
|
|
1124
1500
|
***/
|
|
1125
1501
|
async getDecide(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
|
|
1126
1502
|
await this._initPromise;
|
|
1127
|
-
|
|
1503
|
+
// Check if the API token is in the new flags rollout
|
|
1504
|
+
// This is a temporary measure to ensure that we can still use the old flags API
|
|
1505
|
+
// while we migrate to the new flags API
|
|
1506
|
+
const useFlags = isTokenInRollout(this.apiKey, NEW_FLAGS_ROLLOUT_PERCENTAGE, NEW_FLAGS_EXCLUDED_HASHES);
|
|
1507
|
+
const url = useFlags ? `${this.host}/flags/?v=2` : `${this.host}/decide/?v=4`;
|
|
1128
1508
|
const fetchOptions = {
|
|
1129
1509
|
method: 'POST',
|
|
1130
1510
|
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
@@ -1140,6 +1520,7 @@ class PostHogCoreStateless {
|
|
|
1140
1520
|
// Don't retry /decide API calls
|
|
1141
1521
|
return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.featureFlagsRequestTimeoutMs)
|
|
1142
1522
|
.then((response) => response.json())
|
|
1523
|
+
.then((response) => normalizeDecideResponse(response))
|
|
1143
1524
|
.catch((error) => {
|
|
1144
1525
|
this._events.emit('error', error);
|
|
1145
1526
|
return undefined;
|
|
@@ -1147,23 +1528,41 @@ class PostHogCoreStateless {
|
|
|
1147
1528
|
}
|
|
1148
1529
|
async getFeatureFlagStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1149
1530
|
await this._initPromise;
|
|
1150
|
-
const
|
|
1151
|
-
if (
|
|
1531
|
+
const flagDetailResponse = await this.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
1532
|
+
if (flagDetailResponse === undefined) {
|
|
1152
1533
|
// If we haven't loaded flags yet, or errored out, we respond with undefined
|
|
1153
|
-
return
|
|
1534
|
+
return {
|
|
1535
|
+
response: undefined,
|
|
1536
|
+
requestId: undefined,
|
|
1537
|
+
};
|
|
1154
1538
|
}
|
|
1155
|
-
let response =
|
|
1156
|
-
// `/decide` v3 returns all flags
|
|
1539
|
+
let response = getFeatureFlagValue(flagDetailResponse.response);
|
|
1157
1540
|
if (response === undefined) {
|
|
1158
1541
|
// For cases where the flag is unknown, return false
|
|
1159
1542
|
response = false;
|
|
1160
1543
|
}
|
|
1161
1544
|
// If we have flags we either return the value (true or string) or false
|
|
1162
|
-
return
|
|
1545
|
+
return {
|
|
1546
|
+
response,
|
|
1547
|
+
requestId: flagDetailResponse.requestId,
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
async getFeatureFlagDetailStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1551
|
+
await this._initPromise;
|
|
1552
|
+
const decideResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
|
|
1553
|
+
if (decideResponse === undefined) {
|
|
1554
|
+
return undefined;
|
|
1555
|
+
}
|
|
1556
|
+
const featureFlags = decideResponse.flags;
|
|
1557
|
+
const flagDetail = featureFlags[key];
|
|
1558
|
+
return {
|
|
1559
|
+
response: flagDetail,
|
|
1560
|
+
requestId: decideResponse.requestId,
|
|
1561
|
+
};
|
|
1163
1562
|
}
|
|
1164
1563
|
async getFeatureFlagPayloadStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1165
1564
|
await this._initPromise;
|
|
1166
|
-
const payloads = await this.getFeatureFlagPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
1565
|
+
const payloads = await this.getFeatureFlagPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
|
|
1167
1566
|
if (!payloads) {
|
|
1168
1567
|
return undefined;
|
|
1169
1568
|
}
|
|
@@ -1174,48 +1573,120 @@ class PostHogCoreStateless {
|
|
|
1174
1573
|
}
|
|
1175
1574
|
return response;
|
|
1176
1575
|
}
|
|
1177
|
-
async getFeatureFlagPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1576
|
+
async getFeatureFlagPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1178
1577
|
await this._initPromise;
|
|
1179
|
-
const payloads = (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip)).payloads;
|
|
1578
|
+
const payloads = (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate)).payloads;
|
|
1180
1579
|
return payloads;
|
|
1181
1580
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
catch {
|
|
1187
|
-
return response;
|
|
1188
|
-
}
|
|
1581
|
+
async getFeatureFlagsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1582
|
+
await this._initPromise;
|
|
1583
|
+
return await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
|
|
1189
1584
|
}
|
|
1190
|
-
async
|
|
1585
|
+
async getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1191
1586
|
await this._initPromise;
|
|
1192
|
-
|
|
1587
|
+
const featureFlagDetails = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
|
|
1588
|
+
if (!featureFlagDetails) {
|
|
1589
|
+
return {
|
|
1590
|
+
flags: undefined,
|
|
1591
|
+
payloads: undefined,
|
|
1592
|
+
requestId: undefined,
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
return {
|
|
1596
|
+
flags: featureFlagDetails.featureFlags,
|
|
1597
|
+
payloads: featureFlagDetails.featureFlagPayloads,
|
|
1598
|
+
requestId: featureFlagDetails.requestId,
|
|
1599
|
+
};
|
|
1193
1600
|
}
|
|
1194
|
-
async
|
|
1601
|
+
async getFeatureFlagDetailsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1195
1602
|
await this._initPromise;
|
|
1196
1603
|
const extraPayload = {};
|
|
1197
1604
|
if (disableGeoip ?? this.disableGeoip) {
|
|
1198
1605
|
extraPayload['geoip_disable'] = true;
|
|
1199
1606
|
}
|
|
1607
|
+
if (flagKeysToEvaluate) {
|
|
1608
|
+
extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate;
|
|
1609
|
+
}
|
|
1200
1610
|
const decideResponse = await this.getDecide(distinctId, groups, personProperties, groupProperties, extraPayload);
|
|
1611
|
+
if (decideResponse === undefined) {
|
|
1612
|
+
// We probably errored out, so return undefined
|
|
1613
|
+
return undefined;
|
|
1614
|
+
}
|
|
1615
|
+
// if there's an error on the decideResponse, log a console error, but don't throw an error
|
|
1616
|
+
if (decideResponse.errorsWhileComputingFlags) {
|
|
1617
|
+
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');
|
|
1618
|
+
}
|
|
1201
1619
|
// Add check for quota limitation on feature flags
|
|
1202
|
-
if (decideResponse
|
|
1620
|
+
if (decideResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
|
|
1203
1621
|
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');
|
|
1204
1622
|
return {
|
|
1205
|
-
flags:
|
|
1206
|
-
|
|
1623
|
+
flags: {},
|
|
1624
|
+
featureFlags: {},
|
|
1625
|
+
featureFlagPayloads: {},
|
|
1626
|
+
requestId: decideResponse?.requestId,
|
|
1207
1627
|
};
|
|
1208
1628
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1629
|
+
return decideResponse;
|
|
1630
|
+
}
|
|
1631
|
+
/***
|
|
1632
|
+
*** SURVEYS
|
|
1633
|
+
***/
|
|
1634
|
+
async getSurveysStateless() {
|
|
1635
|
+
await this._initPromise;
|
|
1636
|
+
if (this.disableSurveys === true) {
|
|
1637
|
+
this.logMsgIfDebug(() => console.log('Loading surveys is disabled.'));
|
|
1638
|
+
return [];
|
|
1214
1639
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1640
|
+
const url = `${this.host}/api/surveys/?token=${this.apiKey}`;
|
|
1641
|
+
const fetchOptions = {
|
|
1642
|
+
method: 'GET',
|
|
1643
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
1218
1644
|
};
|
|
1645
|
+
const response = await this.fetchWithRetry(url, fetchOptions)
|
|
1646
|
+
.then((response) => {
|
|
1647
|
+
if (response.status !== 200 || !response.json) {
|
|
1648
|
+
const msg = `Surveys API could not be loaded: ${response.status}`;
|
|
1649
|
+
const error = new Error(msg);
|
|
1650
|
+
this.logMsgIfDebug(() => console.error(error));
|
|
1651
|
+
this._events.emit('error', new Error(msg));
|
|
1652
|
+
return undefined;
|
|
1653
|
+
}
|
|
1654
|
+
return response.json();
|
|
1655
|
+
})
|
|
1656
|
+
.catch((error) => {
|
|
1657
|
+
this.logMsgIfDebug(() => console.error('Surveys API could not be loaded', error));
|
|
1658
|
+
this._events.emit('error', error);
|
|
1659
|
+
return undefined;
|
|
1660
|
+
});
|
|
1661
|
+
const newSurveys = response?.surveys;
|
|
1662
|
+
if (newSurveys) {
|
|
1663
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Surveys fetched from API: ', JSON.stringify(newSurveys)));
|
|
1664
|
+
}
|
|
1665
|
+
return newSurveys ?? [];
|
|
1666
|
+
}
|
|
1667
|
+
get props() {
|
|
1668
|
+
if (!this._props) {
|
|
1669
|
+
this._props = this.getPersistedProperty(PostHogPersistedProperty.Props);
|
|
1670
|
+
}
|
|
1671
|
+
return this._props || {};
|
|
1672
|
+
}
|
|
1673
|
+
set props(val) {
|
|
1674
|
+
this._props = val;
|
|
1675
|
+
}
|
|
1676
|
+
async register(properties) {
|
|
1677
|
+
this.wrap(() => {
|
|
1678
|
+
this.props = {
|
|
1679
|
+
...this.props,
|
|
1680
|
+
...properties,
|
|
1681
|
+
};
|
|
1682
|
+
this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
async unregister(property) {
|
|
1686
|
+
this.wrap(() => {
|
|
1687
|
+
delete this.props[property];
|
|
1688
|
+
this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
|
|
1689
|
+
});
|
|
1219
1690
|
}
|
|
1220
1691
|
/***
|
|
1221
1692
|
*** QUEUEING AND FLUSHING
|
|
@@ -1226,25 +1697,7 @@ class PostHogCoreStateless {
|
|
|
1226
1697
|
this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
|
|
1227
1698
|
return;
|
|
1228
1699
|
}
|
|
1229
|
-
const message =
|
|
1230
|
-
..._message,
|
|
1231
|
-
type: type,
|
|
1232
|
-
library: this.getLibraryId(),
|
|
1233
|
-
library_version: this.getLibraryVersion(),
|
|
1234
|
-
timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
|
|
1235
|
-
uuid: options?.uuid ? options.uuid : uuidv7(),
|
|
1236
|
-
};
|
|
1237
|
-
const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
|
|
1238
|
-
if (addGeoipDisableProperty) {
|
|
1239
|
-
if (!message.properties) {
|
|
1240
|
-
message.properties = {};
|
|
1241
|
-
}
|
|
1242
|
-
message['properties']['$geoip_disable'] = true;
|
|
1243
|
-
}
|
|
1244
|
-
if (message.distinctId) {
|
|
1245
|
-
message.distinct_id = message.distinctId;
|
|
1246
|
-
delete message.distinctId;
|
|
1247
|
-
}
|
|
1700
|
+
const message = this.prepareMessage(type, _message, options);
|
|
1248
1701
|
const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
|
|
1249
1702
|
if (queue.length >= this.maxQueueSize) {
|
|
1250
1703
|
queue.shift();
|
|
@@ -1262,6 +1715,73 @@ class PostHogCoreStateless {
|
|
|
1262
1715
|
}
|
|
1263
1716
|
});
|
|
1264
1717
|
}
|
|
1718
|
+
async sendImmediate(type, _message, options) {
|
|
1719
|
+
if (this.disabled) {
|
|
1720
|
+
this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'));
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (!this._isInitialized) {
|
|
1724
|
+
await this._initPromise;
|
|
1725
|
+
}
|
|
1726
|
+
if (this.optedOut) {
|
|
1727
|
+
this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const data = {
|
|
1731
|
+
api_key: this.apiKey,
|
|
1732
|
+
batch: [this.prepareMessage(type, _message, options)],
|
|
1733
|
+
sent_at: currentISOTime(),
|
|
1734
|
+
};
|
|
1735
|
+
if (this.historicalMigration) {
|
|
1736
|
+
data.historical_migration = true;
|
|
1737
|
+
}
|
|
1738
|
+
const payload = JSON.stringify(data);
|
|
1739
|
+
const url = this.captureMode === 'form'
|
|
1740
|
+
? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
|
|
1741
|
+
: `${this.host}/batch/`;
|
|
1742
|
+
const fetchOptions = this.captureMode === 'form'
|
|
1743
|
+
? {
|
|
1744
|
+
method: 'POST',
|
|
1745
|
+
mode: 'no-cors',
|
|
1746
|
+
credentials: 'omit',
|
|
1747
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1748
|
+
body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
|
|
1749
|
+
}
|
|
1750
|
+
: {
|
|
1751
|
+
method: 'POST',
|
|
1752
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
1753
|
+
body: payload,
|
|
1754
|
+
};
|
|
1755
|
+
try {
|
|
1756
|
+
await this.fetchWithRetry(url, fetchOptions);
|
|
1757
|
+
}
|
|
1758
|
+
catch (err) {
|
|
1759
|
+
this._events.emit('error', err);
|
|
1760
|
+
throw err;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
prepareMessage(type, _message, options) {
|
|
1764
|
+
const message = {
|
|
1765
|
+
..._message,
|
|
1766
|
+
type: type,
|
|
1767
|
+
library: this.getLibraryId(),
|
|
1768
|
+
library_version: this.getLibraryVersion(),
|
|
1769
|
+
timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
|
|
1770
|
+
uuid: options?.uuid ? options.uuid : uuidv7(),
|
|
1771
|
+
};
|
|
1772
|
+
const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
|
|
1773
|
+
if (addGeoipDisableProperty) {
|
|
1774
|
+
if (!message.properties) {
|
|
1775
|
+
message.properties = {};
|
|
1776
|
+
}
|
|
1777
|
+
message['properties']['$geoip_disable'] = true;
|
|
1778
|
+
}
|
|
1779
|
+
if (message.distinctId) {
|
|
1780
|
+
message.distinct_id = message.distinctId;
|
|
1781
|
+
delete message.distinctId;
|
|
1782
|
+
}
|
|
1783
|
+
return message;
|
|
1784
|
+
}
|
|
1265
1785
|
clearFlushTimer() {
|
|
1266
1786
|
if (this._flushTimer) {
|
|
1267
1787
|
clearTimeout(this._flushTimer);
|
|
@@ -1273,16 +1793,26 @@ class PostHogCoreStateless {
|
|
|
1273
1793
|
* Avoids unnecessary promise errors
|
|
1274
1794
|
*/
|
|
1275
1795
|
flushBackground() {
|
|
1276
|
-
void this.flush().catch(() => {
|
|
1796
|
+
void this.flush().catch(async (err) => {
|
|
1797
|
+
await logFlushError(err);
|
|
1798
|
+
});
|
|
1277
1799
|
}
|
|
1278
1800
|
async flush() {
|
|
1279
|
-
|
|
1280
|
-
|
|
1801
|
+
// Wait for the current flush operation to finish (regardless of success or failure), then try to flush again.
|
|
1802
|
+
// Use allSettled instead of finally to be defensive around flush throwing errors immediately rather than rejecting.
|
|
1803
|
+
const nextFlushPromise = Promise.allSettled([this.flushPromise]).then(() => {
|
|
1804
|
+
return this._flush();
|
|
1805
|
+
});
|
|
1806
|
+
this.flushPromise = nextFlushPromise;
|
|
1807
|
+
void this.addPendingPromise(nextFlushPromise);
|
|
1808
|
+
Promise.allSettled([nextFlushPromise]).then(() => {
|
|
1809
|
+
// If there are no others waiting to flush, clear the promise.
|
|
1810
|
+
// We don't strictly need to do this, but it could make debugging easier
|
|
1811
|
+
if (this.flushPromise === nextFlushPromise) {
|
|
1281
1812
|
this.flushPromise = null;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
return this.flushPromise;
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
return nextFlushPromise;
|
|
1286
1816
|
}
|
|
1287
1817
|
getCustomHeaders() {
|
|
1288
1818
|
// Don't set the user agent if we're not on a browser. The latest spec allows
|
|
@@ -1299,56 +1829,80 @@ class PostHogCoreStateless {
|
|
|
1299
1829
|
async _flush() {
|
|
1300
1830
|
this.clearFlushTimer();
|
|
1301
1831
|
await this._initPromise;
|
|
1302
|
-
|
|
1832
|
+
let queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
|
|
1303
1833
|
if (!queue.length) {
|
|
1304
1834
|
return [];
|
|
1305
1835
|
}
|
|
1306
|
-
const
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
const fetchOptions = this.captureMode === 'form'
|
|
1325
|
-
? {
|
|
1326
|
-
method: 'POST',
|
|
1327
|
-
mode: 'no-cors',
|
|
1328
|
-
credentials: 'omit',
|
|
1329
|
-
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1330
|
-
body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
|
|
1836
|
+
const sentMessages = [];
|
|
1837
|
+
const originalQueueLength = queue.length;
|
|
1838
|
+
while (queue.length > 0 && sentMessages.length < originalQueueLength) {
|
|
1839
|
+
const batchItems = queue.slice(0, this.maxBatchSize);
|
|
1840
|
+
const batchMessages = batchItems.map((item) => item.message);
|
|
1841
|
+
const persistQueueChange = () => {
|
|
1842
|
+
const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
|
|
1843
|
+
const newQueue = refreshedQueue.slice(batchItems.length);
|
|
1844
|
+
this.setPersistedProperty(PostHogPersistedProperty.Queue, newQueue);
|
|
1845
|
+
queue = newQueue;
|
|
1846
|
+
};
|
|
1847
|
+
const data = {
|
|
1848
|
+
api_key: this.apiKey,
|
|
1849
|
+
batch: batchMessages,
|
|
1850
|
+
sent_at: currentISOTime(),
|
|
1851
|
+
};
|
|
1852
|
+
if (this.historicalMigration) {
|
|
1853
|
+
data.historical_migration = true;
|
|
1331
1854
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1855
|
+
const payload = JSON.stringify(data);
|
|
1856
|
+
const url = this.captureMode === 'form'
|
|
1857
|
+
? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
|
|
1858
|
+
: `${this.host}/batch/`;
|
|
1859
|
+
const fetchOptions = this.captureMode === 'form'
|
|
1860
|
+
? {
|
|
1861
|
+
method: 'POST',
|
|
1862
|
+
mode: 'no-cors',
|
|
1863
|
+
credentials: 'omit',
|
|
1864
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1865
|
+
body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
|
|
1866
|
+
}
|
|
1867
|
+
: {
|
|
1868
|
+
method: 'POST',
|
|
1869
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
1870
|
+
body: payload,
|
|
1871
|
+
};
|
|
1872
|
+
const retryOptions = {
|
|
1873
|
+
retryCheck: (err) => {
|
|
1874
|
+
// don't automatically retry on 413 errors, we want to reduce the batch size first
|
|
1875
|
+
if (isPostHogFetchContentTooLargeError(err)) {
|
|
1876
|
+
return false;
|
|
1877
|
+
}
|
|
1878
|
+
// otherwise, retry on network errors
|
|
1879
|
+
return isPostHogFetchError(err);
|
|
1880
|
+
},
|
|
1336
1881
|
};
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
}
|
|
1340
|
-
catch (err) {
|
|
1341
|
-
// depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
|
|
1342
|
-
// and this will be an endless loop, in this case, if the error isn't a network issue, we always remove the items from the queue
|
|
1343
|
-
if (!(err instanceof PostHogFetchNetworkError)) {
|
|
1344
|
-
persistQueueChange();
|
|
1882
|
+
try {
|
|
1883
|
+
await this.fetchWithRetry(url, fetchOptions, retryOptions);
|
|
1345
1884
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1885
|
+
catch (err) {
|
|
1886
|
+
if (isPostHogFetchContentTooLargeError(err) && batchMessages.length > 1) {
|
|
1887
|
+
// if we get a 413 error, we want to reduce the batch size and try again
|
|
1888
|
+
this.maxBatchSize = Math.max(1, Math.floor(batchMessages.length / 2));
|
|
1889
|
+
this.logMsgIfDebug(() => console.warn(`Received 413 when sending batch of size ${batchMessages.length}, reducing batch size to ${this.maxBatchSize}`));
|
|
1890
|
+
// do not persist the queue change, we want to retry the same batch
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
// depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
|
|
1894
|
+
// and this will be an endless loop, in this case, if the error isn't a network issue, we always remove the items from the queue
|
|
1895
|
+
if (!(err instanceof PostHogFetchNetworkError)) {
|
|
1896
|
+
persistQueueChange();
|
|
1897
|
+
}
|
|
1898
|
+
this._events.emit('error', err);
|
|
1899
|
+
throw err;
|
|
1900
|
+
}
|
|
1901
|
+
persistQueueChange();
|
|
1902
|
+
sentMessages.push(...batchMessages);
|
|
1348
1903
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
return messages;
|
|
1904
|
+
this._events.emit('flush', sentMessages);
|
|
1905
|
+
return sentMessages;
|
|
1352
1906
|
}
|
|
1353
1907
|
async fetchWithRetry(url, options, retryOptions, requestTimeout) {
|
|
1354
1908
|
var _a;
|
|
@@ -1357,6 +1911,8 @@ class PostHogCoreStateless {
|
|
|
1357
1911
|
setTimeout(() => ctrl.abort(), ms);
|
|
1358
1912
|
return ctrl.signal;
|
|
1359
1913
|
});
|
|
1914
|
+
const body = options.body ? options.body : '';
|
|
1915
|
+
const reqByteLength = Buffer.byteLength(body, STRING_FORMAT);
|
|
1360
1916
|
return await retriable(async () => {
|
|
1361
1917
|
let res = null;
|
|
1362
1918
|
try {
|
|
@@ -1374,12 +1930,12 @@ class PostHogCoreStateless {
|
|
|
1374
1930
|
// https://developer.mozilla.org/en-US/docs/Web/API/Request/mode#no-cors
|
|
1375
1931
|
const isNoCors = options.mode === 'no-cors';
|
|
1376
1932
|
if (!isNoCors && (res.status < 200 || res.status >= 400)) {
|
|
1377
|
-
throw new PostHogFetchHttpError(res);
|
|
1933
|
+
throw new PostHogFetchHttpError(res, reqByteLength);
|
|
1378
1934
|
}
|
|
1379
1935
|
return res;
|
|
1380
1936
|
}, { ...this._retryOptions, ...retryOptions });
|
|
1381
1937
|
}
|
|
1382
|
-
async
|
|
1938
|
+
async _shutdown(shutdownTimeoutMs = 30000) {
|
|
1383
1939
|
// A little tricky - we want to have a max shutdown time and enforce it, even if that means we have some
|
|
1384
1940
|
// dangling promises. We'll keep track of the timeout and resolve/reject based on that.
|
|
1385
1941
|
await this._initPromise;
|
|
@@ -1406,7 +1962,7 @@ class PostHogCoreStateless {
|
|
|
1406
1962
|
if (!isPostHogFetchError(e)) {
|
|
1407
1963
|
throw e;
|
|
1408
1964
|
}
|
|
1409
|
-
|
|
1965
|
+
await logFlushError(e);
|
|
1410
1966
|
}
|
|
1411
1967
|
};
|
|
1412
1968
|
return Promise.race([
|
|
@@ -1420,6 +1976,22 @@ class PostHogCoreStateless {
|
|
|
1420
1976
|
doShutdown(),
|
|
1421
1977
|
]);
|
|
1422
1978
|
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Call shutdown() once before the node process exits, so ensure that all events have been sent and all promises
|
|
1981
|
+
* have resolved. Do not use this function if you intend to keep using this PostHog instance after calling it.
|
|
1982
|
+
* @param shutdownTimeoutMs
|
|
1983
|
+
*/
|
|
1984
|
+
async shutdown(shutdownTimeoutMs = 30000) {
|
|
1985
|
+
if (this.shutdownPromise) {
|
|
1986
|
+
this.logMsgIfDebug(() => console.warn('shutdown() called while already shutting down. shutdown() is meant to be called once before process exit - use flush() for per-request cleanup'));
|
|
1987
|
+
}
|
|
1988
|
+
else {
|
|
1989
|
+
this.shutdownPromise = this._shutdown(shutdownTimeoutMs).finally(() => {
|
|
1990
|
+
this.shutdownPromise = null;
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
return this.shutdownPromise;
|
|
1994
|
+
}
|
|
1423
1995
|
}
|
|
1424
1996
|
class PostHogCore extends PostHogCoreStateless {
|
|
1425
1997
|
constructor(apiKey, options) {
|
|
@@ -1454,36 +2026,24 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1454
2026
|
}
|
|
1455
2027
|
}
|
|
1456
2028
|
}
|
|
1457
|
-
const
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
this.
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
const newFeatureFlagPayloads = { ...bootstrapFlagPayloads, ...currentFlagPayloads };
|
|
1473
|
-
this.setKnownFeatureFlagPayloads(newFeatureFlagPayloads);
|
|
2029
|
+
const bootstrapFeatureFlags = bootstrap.featureFlags;
|
|
2030
|
+
const bootstrapFeatureFlagPayloads = bootstrap.featureFlagPayloads ?? {};
|
|
2031
|
+
if (bootstrapFeatureFlags && Object.keys(bootstrapFeatureFlags).length) {
|
|
2032
|
+
const normalizedBootstrapFeatureFlagDetails = createDecideResponseFromFlagsAndPayloads(bootstrapFeatureFlags, bootstrapFeatureFlagPayloads);
|
|
2033
|
+
if (Object.keys(normalizedBootstrapFeatureFlagDetails.flags).length > 0) {
|
|
2034
|
+
this.setBootstrappedFeatureFlagDetails(normalizedBootstrapFeatureFlagDetails);
|
|
2035
|
+
const currentFeatureFlagDetails = this.getKnownFeatureFlagDetails() || { flags: {}, requestId: undefined };
|
|
2036
|
+
const newFeatureFlagDetails = {
|
|
2037
|
+
flags: {
|
|
2038
|
+
...normalizedBootstrapFeatureFlagDetails.flags,
|
|
2039
|
+
...currentFeatureFlagDetails.flags,
|
|
2040
|
+
},
|
|
2041
|
+
requestId: normalizedBootstrapFeatureFlagDetails.requestId,
|
|
2042
|
+
};
|
|
2043
|
+
this.setKnownFeatureFlagDetails(newFeatureFlagDetails);
|
|
1474
2044
|
}
|
|
1475
2045
|
}
|
|
1476
2046
|
}
|
|
1477
|
-
// NOTE: Props are lazy loaded from localstorage hence the complex getter setter logic
|
|
1478
|
-
get props() {
|
|
1479
|
-
if (!this._props) {
|
|
1480
|
-
this._props = this.getPersistedProperty(PostHogPersistedProperty.Props);
|
|
1481
|
-
}
|
|
1482
|
-
return this._props || {};
|
|
1483
|
-
}
|
|
1484
|
-
set props(val) {
|
|
1485
|
-
this._props = val;
|
|
1486
|
-
}
|
|
1487
2047
|
clearProps() {
|
|
1488
2048
|
this.props = undefined;
|
|
1489
2049
|
this.sessionProps = {};
|
|
@@ -1573,21 +2133,6 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1573
2133
|
}
|
|
1574
2134
|
return this.getPersistedProperty(PostHogPersistedProperty.DistinctId) || this.getAnonymousId();
|
|
1575
2135
|
}
|
|
1576
|
-
async unregister(property) {
|
|
1577
|
-
this.wrap(() => {
|
|
1578
|
-
delete this.props[property];
|
|
1579
|
-
this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
|
|
1580
|
-
});
|
|
1581
|
-
}
|
|
1582
|
-
async register(properties) {
|
|
1583
|
-
this.wrap(() => {
|
|
1584
|
-
this.props = {
|
|
1585
|
-
...this.props,
|
|
1586
|
-
...properties,
|
|
1587
|
-
};
|
|
1588
|
-
this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
|
|
1589
|
-
});
|
|
1590
|
-
}
|
|
1591
2136
|
registerForSession(properties) {
|
|
1592
2137
|
this.sessionProps = {
|
|
1593
2138
|
...this.sessionProps,
|
|
@@ -1708,7 +2253,7 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1708
2253
|
}
|
|
1709
2254
|
resetPersonPropertiesForFlags() {
|
|
1710
2255
|
this.wrap(() => {
|
|
1711
|
-
this.setPersistedProperty(PostHogPersistedProperty.PersonProperties,
|
|
2256
|
+
this.setPersistedProperty(PostHogPersistedProperty.PersonProperties, null);
|
|
1712
2257
|
});
|
|
1713
2258
|
}
|
|
1714
2259
|
/** @deprecated - Renamed to setPersonPropertiesForFlags */
|
|
@@ -1737,7 +2282,7 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1737
2282
|
}
|
|
1738
2283
|
resetGroupPropertiesForFlags() {
|
|
1739
2284
|
this.wrap(() => {
|
|
1740
|
-
this.setPersistedProperty(PostHogPersistedProperty.GroupProperties,
|
|
2285
|
+
this.setPersistedProperty(PostHogPersistedProperty.GroupProperties, null);
|
|
1741
2286
|
});
|
|
1742
2287
|
}
|
|
1743
2288
|
/** @deprecated - Renamed to setGroupPropertiesForFlags */
|
|
@@ -1746,6 +2291,13 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1746
2291
|
this.setGroupPropertiesForFlags(properties);
|
|
1747
2292
|
});
|
|
1748
2293
|
}
|
|
2294
|
+
async remoteConfigAsync() {
|
|
2295
|
+
await this._initPromise;
|
|
2296
|
+
if (this._remoteConfigResponsePromise) {
|
|
2297
|
+
return this._remoteConfigResponsePromise;
|
|
2298
|
+
}
|
|
2299
|
+
return this._remoteConfigAsync();
|
|
2300
|
+
}
|
|
1749
2301
|
/***
|
|
1750
2302
|
*** FEATURE FLAGS
|
|
1751
2303
|
***/
|
|
@@ -1756,6 +2308,65 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1756
2308
|
}
|
|
1757
2309
|
return this._decideAsync(sendAnonDistinctId);
|
|
1758
2310
|
}
|
|
2311
|
+
cacheSessionReplay(response) {
|
|
2312
|
+
const sessionReplay = response?.sessionRecording;
|
|
2313
|
+
if (sessionReplay) {
|
|
2314
|
+
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, sessionReplay);
|
|
2315
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Session replay config: ', JSON.stringify(sessionReplay)));
|
|
2316
|
+
}
|
|
2317
|
+
else {
|
|
2318
|
+
this.logMsgIfDebug(() => console.info('PostHog Debug', 'Session replay config disabled.'));
|
|
2319
|
+
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, null);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
async _remoteConfigAsync() {
|
|
2323
|
+
this._remoteConfigResponsePromise = this._initPromise
|
|
2324
|
+
.then(() => {
|
|
2325
|
+
let remoteConfig = this.getPersistedProperty(PostHogPersistedProperty.RemoteConfig);
|
|
2326
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Cached remote config: ', JSON.stringify(remoteConfig)));
|
|
2327
|
+
return super.getRemoteConfig().then((response) => {
|
|
2328
|
+
if (response) {
|
|
2329
|
+
const remoteConfigWithoutSurveys = { ...response };
|
|
2330
|
+
delete remoteConfigWithoutSurveys.surveys;
|
|
2331
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Fetched remote config: ', JSON.stringify(remoteConfigWithoutSurveys)));
|
|
2332
|
+
const surveys = response.surveys;
|
|
2333
|
+
let hasSurveys = true;
|
|
2334
|
+
if (!Array.isArray(surveys)) {
|
|
2335
|
+
// If surveys is not an array, it means there are no surveys (its a boolean instead)
|
|
2336
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'There are no surveys.'));
|
|
2337
|
+
hasSurveys = false;
|
|
2338
|
+
}
|
|
2339
|
+
else {
|
|
2340
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Surveys fetched from remote config: ', JSON.stringify(surveys)));
|
|
2341
|
+
}
|
|
2342
|
+
if (this.disableSurveys === false && hasSurveys) {
|
|
2343
|
+
this.setPersistedProperty(PostHogPersistedProperty.Surveys, surveys);
|
|
2344
|
+
}
|
|
2345
|
+
else {
|
|
2346
|
+
this.setPersistedProperty(PostHogPersistedProperty.Surveys, null);
|
|
2347
|
+
}
|
|
2348
|
+
// we cache the surveys in its own storage key
|
|
2349
|
+
this.setPersistedProperty(PostHogPersistedProperty.RemoteConfig, remoteConfigWithoutSurveys);
|
|
2350
|
+
this.cacheSessionReplay(response);
|
|
2351
|
+
// we only dont load flags if the remote config has no feature flags
|
|
2352
|
+
if (response.hasFeatureFlags === false) {
|
|
2353
|
+
// resetting flags to empty object
|
|
2354
|
+
this.setKnownFeatureFlagDetails({ flags: {} });
|
|
2355
|
+
this.logMsgIfDebug(() => console.warn('Remote config has no feature flags, will not load feature flags.'));
|
|
2356
|
+
}
|
|
2357
|
+
else if (this.preloadFeatureFlags !== false) {
|
|
2358
|
+
this.reloadFeatureFlags();
|
|
2359
|
+
}
|
|
2360
|
+
remoteConfig = response;
|
|
2361
|
+
}
|
|
2362
|
+
return remoteConfig;
|
|
2363
|
+
});
|
|
2364
|
+
})
|
|
2365
|
+
.finally(() => {
|
|
2366
|
+
this._remoteConfigResponsePromise = undefined;
|
|
2367
|
+
});
|
|
2368
|
+
return this._remoteConfigResponsePromise;
|
|
2369
|
+
}
|
|
1759
2370
|
async _decideAsync(sendAnonDistinctId = true) {
|
|
1760
2371
|
this._decideResponsePromise = this._initPromise
|
|
1761
2372
|
.then(async () => {
|
|
@@ -1771,8 +2382,7 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1771
2382
|
// Add check for quota limitation on feature flags
|
|
1772
2383
|
if (res?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
|
|
1773
2384
|
// Unset all feature flags by setting to null
|
|
1774
|
-
this.
|
|
1775
|
-
this.setKnownFeatureFlagPayloads(null);
|
|
2385
|
+
this.setKnownFeatureFlagDetails(null);
|
|
1776
2386
|
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');
|
|
1777
2387
|
return res;
|
|
1778
2388
|
}
|
|
@@ -1781,29 +2391,20 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1781
2391
|
if (this.sendFeatureFlagEvent) {
|
|
1782
2392
|
this.flagCallReported = {};
|
|
1783
2393
|
}
|
|
1784
|
-
let
|
|
1785
|
-
let newFeatureFlagPayloads = res.featureFlagPayloads;
|
|
2394
|
+
let newFeatureFlagDetails = res;
|
|
1786
2395
|
if (res.errorsWhileComputingFlags) {
|
|
1787
2396
|
// if not all flags were computed, we upsert flags instead of replacing them
|
|
1788
|
-
const
|
|
1789
|
-
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Cached feature flags: ', JSON.stringify(
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
2397
|
+
const currentFlagDetails = this.getKnownFeatureFlagDetails();
|
|
2398
|
+
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Cached feature flags: ', JSON.stringify(currentFlagDetails)));
|
|
2399
|
+
newFeatureFlagDetails = {
|
|
2400
|
+
...res,
|
|
2401
|
+
flags: { ...currentFlagDetails?.flags, ...res.flags },
|
|
2402
|
+
};
|
|
1793
2403
|
}
|
|
1794
|
-
this.
|
|
1795
|
-
this.setKnownFeatureFlagPayloads(Object.fromEntries(Object.entries(newFeatureFlagPayloads || {}).map(([k, v]) => [k, this._parsePayload(v)])));
|
|
2404
|
+
this.setKnownFeatureFlagDetails(newFeatureFlagDetails);
|
|
1796
2405
|
// Mark that we hit the /decide endpoint so we can capture this in the $feature_flag_called event
|
|
1797
2406
|
this.setPersistedProperty(PostHogPersistedProperty.DecideEndpointWasHit, true);
|
|
1798
|
-
|
|
1799
|
-
if (sessionReplay) {
|
|
1800
|
-
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, sessionReplay);
|
|
1801
|
-
this.logMsgIfDebug(() => console.log('PostHog Debug', 'Session replay config: ', JSON.stringify(sessionReplay)));
|
|
1802
|
-
}
|
|
1803
|
-
else {
|
|
1804
|
-
this.logMsgIfDebug(() => console.info('PostHog Debug', 'Session replay config disabled.'));
|
|
1805
|
-
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, null);
|
|
1806
|
-
}
|
|
2407
|
+
this.cacheSessionReplay(res);
|
|
1807
2408
|
}
|
|
1808
2409
|
return res;
|
|
1809
2410
|
})
|
|
@@ -1812,38 +2413,91 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1812
2413
|
});
|
|
1813
2414
|
return this._decideResponsePromise;
|
|
1814
2415
|
}
|
|
1815
|
-
|
|
2416
|
+
// We only store the flags and request id in the feature flag details storage key
|
|
2417
|
+
setKnownFeatureFlagDetails(decideResponse) {
|
|
1816
2418
|
this.wrap(() => {
|
|
1817
|
-
this.setPersistedProperty(PostHogPersistedProperty.
|
|
1818
|
-
this._events.emit('featureflags',
|
|
2419
|
+
this.setPersistedProperty(PostHogPersistedProperty.FeatureFlagDetails, decideResponse);
|
|
2420
|
+
this._events.emit('featureflags', getFlagValuesFromFlags(decideResponse?.flags ?? {}));
|
|
1819
2421
|
});
|
|
1820
2422
|
}
|
|
1821
|
-
|
|
1822
|
-
this.
|
|
1823
|
-
|
|
1824
|
-
|
|
2423
|
+
getKnownFeatureFlagDetails() {
|
|
2424
|
+
const storedDetails = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagDetails);
|
|
2425
|
+
if (!storedDetails) {
|
|
2426
|
+
// Rebuild from the stored feature flags and feature flag payloads
|
|
2427
|
+
const featureFlags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags);
|
|
2428
|
+
const featureFlagPayloads = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads);
|
|
2429
|
+
if (featureFlags === undefined && featureFlagPayloads === undefined) {
|
|
2430
|
+
return undefined;
|
|
2431
|
+
}
|
|
2432
|
+
return createDecideResponseFromFlagsAndPayloads(featureFlags ?? {}, featureFlagPayloads ?? {});
|
|
2433
|
+
}
|
|
2434
|
+
return normalizeDecideResponse(storedDetails);
|
|
2435
|
+
}
|
|
2436
|
+
getKnownFeatureFlags() {
|
|
2437
|
+
const featureFlagDetails = this.getKnownFeatureFlagDetails();
|
|
2438
|
+
if (!featureFlagDetails) {
|
|
2439
|
+
return undefined;
|
|
2440
|
+
}
|
|
2441
|
+
return getFlagValuesFromFlags(featureFlagDetails.flags);
|
|
2442
|
+
}
|
|
2443
|
+
getKnownFeatureFlagPayloads() {
|
|
2444
|
+
const featureFlagDetails = this.getKnownFeatureFlagDetails();
|
|
2445
|
+
if (!featureFlagDetails) {
|
|
2446
|
+
return undefined;
|
|
2447
|
+
}
|
|
2448
|
+
return getPayloadsFromFlags(featureFlagDetails.flags);
|
|
2449
|
+
}
|
|
2450
|
+
getBootstrappedFeatureFlagDetails() {
|
|
2451
|
+
const details = this.getPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlagDetails);
|
|
2452
|
+
if (!details) {
|
|
2453
|
+
return undefined;
|
|
2454
|
+
}
|
|
2455
|
+
return details;
|
|
2456
|
+
}
|
|
2457
|
+
setBootstrappedFeatureFlagDetails(details) {
|
|
2458
|
+
this.setPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlagDetails, details);
|
|
2459
|
+
}
|
|
2460
|
+
getBootstrappedFeatureFlags() {
|
|
2461
|
+
const details = this.getBootstrappedFeatureFlagDetails();
|
|
2462
|
+
if (!details) {
|
|
2463
|
+
return undefined;
|
|
2464
|
+
}
|
|
2465
|
+
return getFlagValuesFromFlags(details.flags);
|
|
2466
|
+
}
|
|
2467
|
+
getBootstrappedFeatureFlagPayloads() {
|
|
2468
|
+
const details = this.getBootstrappedFeatureFlagDetails();
|
|
2469
|
+
if (!details) {
|
|
2470
|
+
return undefined;
|
|
2471
|
+
}
|
|
2472
|
+
return getPayloadsFromFlags(details.flags);
|
|
1825
2473
|
}
|
|
1826
2474
|
getFeatureFlag(key) {
|
|
1827
|
-
const
|
|
1828
|
-
if (!
|
|
2475
|
+
const details = this.getFeatureFlagDetails();
|
|
2476
|
+
if (!details) {
|
|
1829
2477
|
// If we haven't loaded flags yet, or errored out, we respond with undefined
|
|
1830
2478
|
return undefined;
|
|
1831
2479
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
2480
|
+
const featureFlag = details.flags[key];
|
|
2481
|
+
let response = getFeatureFlagValue(featureFlag);
|
|
1834
2482
|
if (response === undefined) {
|
|
1835
2483
|
// For cases where the flag is unknown, return false
|
|
1836
2484
|
response = false;
|
|
1837
2485
|
}
|
|
1838
2486
|
if (this.sendFeatureFlagEvent && !this.flagCallReported[key]) {
|
|
2487
|
+
const bootstrappedResponse = this.getBootstrappedFeatureFlags()?.[key];
|
|
2488
|
+
const bootstrappedPayload = this.getBootstrappedFeatureFlagPayloads()?.[key];
|
|
1839
2489
|
this.flagCallReported[key] = true;
|
|
1840
2490
|
this.capture('$feature_flag_called', {
|
|
1841
2491
|
$feature_flag: key,
|
|
1842
2492
|
$feature_flag_response: response,
|
|
1843
|
-
$
|
|
1844
|
-
$
|
|
2493
|
+
$feature_flag_id: featureFlag?.metadata?.id,
|
|
2494
|
+
$feature_flag_version: featureFlag?.metadata?.version,
|
|
2495
|
+
$feature_flag_reason: featureFlag?.reason?.description ?? featureFlag?.reason?.code,
|
|
2496
|
+
$feature_flag_bootstrapped_response: bootstrappedResponse,
|
|
2497
|
+
$feature_flag_bootstrapped_payload: bootstrappedPayload,
|
|
1845
2498
|
// If we haven't yet received a response from the /decide endpoint, we must have used the bootstrapped value
|
|
1846
2499
|
$used_bootstrap_value: !this.getPersistedProperty(PostHogPersistedProperty.DecideEndpointWasHit),
|
|
2500
|
+
$feature_flag_request_id: details.requestId,
|
|
1847
2501
|
});
|
|
1848
2502
|
}
|
|
1849
2503
|
// If we have flags we either return the value (true or string) or false
|
|
@@ -1862,27 +2516,36 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1862
2516
|
return response;
|
|
1863
2517
|
}
|
|
1864
2518
|
getFeatureFlagPayloads() {
|
|
1865
|
-
|
|
1866
|
-
return payloads;
|
|
2519
|
+
return this.getFeatureFlagDetails()?.featureFlagPayloads;
|
|
1867
2520
|
}
|
|
1868
2521
|
getFeatureFlags() {
|
|
1869
2522
|
// NOTE: We don't check for _initPromise here as the function is designed to be
|
|
1870
2523
|
// callable before the state being loaded anyways
|
|
1871
|
-
|
|
2524
|
+
return this.getFeatureFlagDetails()?.featureFlags;
|
|
2525
|
+
}
|
|
2526
|
+
getFeatureFlagDetails() {
|
|
2527
|
+
// NOTE: We don't check for _initPromise here as the function is designed to be
|
|
2528
|
+
// callable before the state being loaded anyways
|
|
2529
|
+
let details = this.getKnownFeatureFlagDetails();
|
|
1872
2530
|
const overriddenFlags = this.getPersistedProperty(PostHogPersistedProperty.OverrideFeatureFlags);
|
|
1873
2531
|
if (!overriddenFlags) {
|
|
1874
|
-
return
|
|
2532
|
+
return details;
|
|
1875
2533
|
}
|
|
1876
|
-
|
|
2534
|
+
details = details ?? { featureFlags: {}, featureFlagPayloads: {}, flags: {} };
|
|
2535
|
+
const flags = details.flags ?? {};
|
|
1877
2536
|
for (const key in overriddenFlags) {
|
|
1878
2537
|
if (!overriddenFlags[key]) {
|
|
1879
2538
|
delete flags[key];
|
|
1880
2539
|
}
|
|
1881
2540
|
else {
|
|
1882
|
-
flags[key] = overriddenFlags[key];
|
|
2541
|
+
flags[key] = updateFlagValue(flags[key], overriddenFlags[key]);
|
|
1883
2542
|
}
|
|
1884
2543
|
}
|
|
1885
|
-
|
|
2544
|
+
const result = {
|
|
2545
|
+
...details,
|
|
2546
|
+
flags,
|
|
2547
|
+
};
|
|
2548
|
+
return normalizeDecideResponse(result);
|
|
1886
2549
|
}
|
|
1887
2550
|
getFeatureFlagsAndPayloads() {
|
|
1888
2551
|
const flags = this.getFeatureFlags();
|
|
@@ -1912,6 +2575,9 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1912
2575
|
}
|
|
1913
2576
|
});
|
|
1914
2577
|
}
|
|
2578
|
+
async reloadRemoteConfigAsync() {
|
|
2579
|
+
return await this.remoteConfigAsync();
|
|
2580
|
+
}
|
|
1915
2581
|
async reloadFeatureFlagsAsync(sendAnonDistinctId = true) {
|
|
1916
2582
|
return (await this.decideAsync(sendAnonDistinctId))?.featureFlags;
|
|
1917
2583
|
}
|
|
@@ -1986,8 +2652,6 @@ class PostHogCore extends PostHogCoreStateless {
|
|
|
1986
2652
|
}
|
|
1987
2653
|
}
|
|
1988
2654
|
|
|
1989
|
-
var version = "3.4.2";
|
|
1990
|
-
|
|
1991
2655
|
function getContext(window) {
|
|
1992
2656
|
let context = {};
|
|
1993
2657
|
if (window?.navigator) {
|
|
@@ -2291,9 +2955,51 @@ const getStorage = (type, window) => {
|
|
|
2291
2955
|
}
|
|
2292
2956
|
};
|
|
2293
2957
|
|
|
2958
|
+
// import { patch } from 'rrweb/typings/utils'
|
|
2959
|
+
// copied from: https://github.com/PostHog/posthog-js/blob/main/src/extensions/replay/rrweb-plugins/patch.ts
|
|
2960
|
+
// which was copied from https://github.com/rrweb-io/rrweb/blob/8aea5b00a4dfe5a6f59bd2ae72bb624f45e51e81/packages/rrweb/src/utils.ts#L129
|
|
2961
|
+
// which was copied from https://github.com/getsentry/sentry-javascript/blob/b2109071975af8bf0316d3b5b38f519bdaf5dc15/packages/utils/src/object.ts
|
|
2962
|
+
// copied from: https://github.com/PostHog/posthog-js/blob/main/react/src/utils/type-utils.ts#L4
|
|
2963
|
+
const isFunction = function (f) {
|
|
2964
|
+
return typeof f === 'function';
|
|
2965
|
+
};
|
|
2966
|
+
function patch(source, name, replacement) {
|
|
2967
|
+
try {
|
|
2968
|
+
if (!(name in source)) {
|
|
2969
|
+
return () => {
|
|
2970
|
+
//
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
const original = source[name];
|
|
2974
|
+
const wrapped = replacement(original);
|
|
2975
|
+
// Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work
|
|
2976
|
+
// otherwise it'll throw "TypeError: Object.defineProperties called on non-object"
|
|
2977
|
+
if (isFunction(wrapped)) {
|
|
2978
|
+
wrapped.prototype = wrapped.prototype || {};
|
|
2979
|
+
Object.defineProperties(wrapped, {
|
|
2980
|
+
__posthog_wrapped__: {
|
|
2981
|
+
enumerable: false,
|
|
2982
|
+
value: true
|
|
2983
|
+
}
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
source[name] = wrapped;
|
|
2987
|
+
return () => {
|
|
2988
|
+
source[name] = original;
|
|
2989
|
+
};
|
|
2990
|
+
} catch {
|
|
2991
|
+
return () => {
|
|
2992
|
+
//
|
|
2993
|
+
};
|
|
2994
|
+
// This can throw if multiple fill happens on a global object like XMLHttpRequest
|
|
2995
|
+
// Fixes https://github.com/getsentry/sentry-javascript/issues/2043
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2294
2999
|
class PostHog extends PostHogCore {
|
|
2295
3000
|
constructor(apiKey, options) {
|
|
2296
3001
|
super(apiKey, options);
|
|
3002
|
+
this._lastPathname = '';
|
|
2297
3003
|
// posthog-js stores options in one object on
|
|
2298
3004
|
this._storageKey = options?.persistence_name ? `ph_${options.persistence_name}` : `ph_${apiKey}_posthog`;
|
|
2299
3005
|
this._storage = getStorage(options?.persistence || 'localStorage', this.getWindow());
|
|
@@ -2301,6 +3007,10 @@ class PostHog extends PostHogCore {
|
|
|
2301
3007
|
if (options?.preloadFeatureFlags !== false) {
|
|
2302
3008
|
this.reloadFeatureFlags();
|
|
2303
3009
|
}
|
|
3010
|
+
if (options?.captureHistoryEvents && typeof window !== 'undefined') {
|
|
3011
|
+
this._lastPathname = window?.location?.pathname || '';
|
|
3012
|
+
this.setupHistoryEventTracking();
|
|
3013
|
+
}
|
|
2304
3014
|
}
|
|
2305
3015
|
getWindow() {
|
|
2306
3016
|
return typeof window !== 'undefined' ? window : undefined;
|
|
@@ -2345,7 +3055,48 @@ class PostHog extends PostHogCore {
|
|
|
2345
3055
|
...getContext(this.getWindow())
|
|
2346
3056
|
};
|
|
2347
3057
|
}
|
|
3058
|
+
setupHistoryEventTracking() {
|
|
3059
|
+
const window = this.getWindow();
|
|
3060
|
+
if (!window) {
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
// Old fashioned, we could also use arrow functions but I think the closure for a patch is more reliable
|
|
3064
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
3065
|
+
const self = this;
|
|
3066
|
+
patch(window.history, 'pushState', originalPushState => {
|
|
3067
|
+
return function patchedPushState(state, title, url) {
|
|
3068
|
+
;
|
|
3069
|
+
originalPushState.call(this, state, title, url);
|
|
3070
|
+
self.captureNavigationEvent('pushState');
|
|
3071
|
+
};
|
|
3072
|
+
});
|
|
3073
|
+
patch(window.history, 'replaceState', originalReplaceState => {
|
|
3074
|
+
return function patchedReplaceState(state, title, url) {
|
|
3075
|
+
;
|
|
3076
|
+
originalReplaceState.call(this, state, title, url);
|
|
3077
|
+
self.captureNavigationEvent('replaceState');
|
|
3078
|
+
};
|
|
3079
|
+
});
|
|
3080
|
+
// For popstate we need to listen to the event instead of overriding a method
|
|
3081
|
+
window.addEventListener('popstate', () => {
|
|
3082
|
+
this.captureNavigationEvent('popstate');
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
captureNavigationEvent(navigationType) {
|
|
3086
|
+
const window = this.getWindow();
|
|
3087
|
+
if (!window) {
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
const currentPathname = window.location.pathname;
|
|
3091
|
+
// Only capture pageview if the pathname has changed
|
|
3092
|
+
if (currentPathname !== this._lastPathname) {
|
|
3093
|
+
this.capture('$pageview', {
|
|
3094
|
+
navigation_type: navigationType
|
|
3095
|
+
});
|
|
3096
|
+
this._lastPathname = currentPathname;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
2348
3099
|
}
|
|
2349
3100
|
|
|
2350
3101
|
export { PostHog, PostHog as default };
|
|
2351
|
-
//# sourceMappingURL=index.
|
|
3102
|
+
//# sourceMappingURL=index.mjs.map
|