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