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.
@@ -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
- })(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
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 global.fetch !== 'undefined' ? global.fetch : undefined;
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.disableGeoip = true;
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
- const url = `${this.host}/decide/?v=3`;
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 featureFlags = await this.getFeatureFlagsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip);
1151
- if (!featureFlags) {
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 undefined;
1534
+ return {
1535
+ response: undefined,
1536
+ requestId: undefined,
1537
+ };
1154
1538
  }
1155
- let response = featureFlags[key];
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 response;
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
- _parsePayload(response) {
1183
- try {
1184
- return JSON.parse(response);
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 getFeatureFlagsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1585
+ async getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1191
1586
  await this._initPromise;
1192
- return (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip)).flags;
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 getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
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?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
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: undefined,
1206
- payloads: undefined,
1623
+ flags: {},
1624
+ featureFlags: {},
1625
+ featureFlagPayloads: {},
1626
+ requestId: decideResponse?.requestId,
1207
1627
  };
1208
1628
  }
1209
- const flags = decideResponse?.featureFlags;
1210
- const payloads = decideResponse?.featureFlagPayloads;
1211
- let parsedPayloads = payloads;
1212
- if (payloads) {
1213
- parsedPayloads = Object.fromEntries(Object.entries(payloads).map(([k, v]) => [k, this._parsePayload(v)]));
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
- return {
1216
- flags,
1217
- payloads: parsedPayloads,
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
- if (!this.flushPromise) {
1280
- this.flushPromise = this._flush().finally(() => {
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
- this.addPendingPromise(this.flushPromise);
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
- const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1832
+ let queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1303
1833
  if (!queue.length) {
1304
1834
  return [];
1305
1835
  }
1306
- const items = queue.slice(0, this.maxBatchSize);
1307
- const messages = items.map((item) => item.message);
1308
- const persistQueueChange = () => {
1309
- const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1310
- this.setPersistedProperty(PostHogPersistedProperty.Queue, refreshedQueue.slice(items.length));
1311
- };
1312
- const data = {
1313
- api_key: this.apiKey,
1314
- batch: messages,
1315
- sent_at: currentISOTime(),
1316
- };
1317
- if (this.historicalMigration) {
1318
- data.historical_migration = true;
1319
- }
1320
- const payload = JSON.stringify(data);
1321
- const url = this.captureMode === 'form'
1322
- ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1323
- : `${this.host}/batch/`;
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
- method: 'POST',
1334
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1335
- body: payload,
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
- try {
1338
- await this.fetchWithRetry(url, fetchOptions);
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
- this._events.emit('error', err);
1347
- throw err;
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
- persistQueueChange();
1350
- this._events.emit('flush', messages);
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 shutdown(shutdownTimeoutMs = 30000) {
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
- this.logMsgIfDebug(() => console.error('Error while shutting down PostHog', e));
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 bootstrapfeatureFlags = bootstrap.featureFlags;
1458
- if (bootstrapfeatureFlags && Object.keys(bootstrapfeatureFlags).length) {
1459
- const bootstrapFlags = Object.keys(bootstrapfeatureFlags)
1460
- .filter((flag) => !!bootstrapfeatureFlags[flag])
1461
- .reduce((res, key) => ((res[key] = bootstrapfeatureFlags[key] || false), res), {});
1462
- if (Object.keys(bootstrapFlags).length) {
1463
- this.setPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlags, bootstrapFlags);
1464
- const currentFlags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags) || {};
1465
- const newFeatureFlags = { ...bootstrapFlags, ...currentFlags };
1466
- this.setKnownFeatureFlags(newFeatureFlags);
1467
- }
1468
- const bootstrapFlagPayloads = bootstrap.featureFlagPayloads;
1469
- if (bootstrapFlagPayloads && Object.keys(bootstrapFlagPayloads).length) {
1470
- this.setPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlagPayloads, bootstrapFlagPayloads);
1471
- const currentFlagPayloads = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads) || {};
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.setKnownFeatureFlags(null);
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 newFeatureFlags = res.featureFlags;
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 currentFlags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags);
1789
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Cached feature flags: ', JSON.stringify(currentFlags)));
1790
- const currentFlagPayloads = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads);
1791
- newFeatureFlags = { ...currentFlags, ...res.featureFlags };
1792
- newFeatureFlagPayloads = { ...currentFlagPayloads, ...res.featureFlagPayloads };
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.setKnownFeatureFlags(newFeatureFlags);
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
- const sessionReplay = res?.sessionRecording;
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
- setKnownFeatureFlags(featureFlags) {
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.FeatureFlags, featureFlags);
1818
- this._events.emit('featureflags', featureFlags);
2419
+ this.setPersistedProperty(PostHogPersistedProperty.FeatureFlagDetails, decideResponse);
2420
+ this._events.emit('featureflags', getFlagValuesFromFlags(decideResponse?.flags ?? {}));
1819
2421
  });
1820
2422
  }
1821
- setKnownFeatureFlagPayloads(featureFlagPayloads) {
1822
- this.wrap(() => {
1823
- this.setPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads, featureFlagPayloads);
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 featureFlags = this.getFeatureFlags();
1828
- if (!featureFlags) {
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
- let response = featureFlags[key];
1833
- // `/decide` v3 returns all flags
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
- $feature_flag_bootstrapped_response: this.getPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlags)?.[key],
1844
- $feature_flag_bootstrapped_payload: this.getPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlagPayloads)?.[key],
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
- const payloads = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads);
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
- let flags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags);
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 flags;
2532
+ return details;
1875
2533
  }
1876
- flags = flags || {};
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
- return flags;
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.esm.js.map
3102
+ //# sourceMappingURL=index.mjs.map