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.
@@ -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
- })(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
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 global.fetch !== 'undefined' ? global.fetch : undefined;
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.disableGeoip = true;
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
- const url = `${this.host}/decide/?v=3`;
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 featureFlags = await this.getFeatureFlagsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip);
1155
- if (!featureFlags) {
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 undefined;
1538
+ return {
1539
+ response: undefined,
1540
+ requestId: undefined,
1541
+ };
1158
1542
  }
1159
- let response = featureFlags[key];
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 response;
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
- _parsePayload(response) {
1187
- try {
1188
- return JSON.parse(response);
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 getFeatureFlagsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1589
+ async getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1195
1590
  await this._initPromise;
1196
- return (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip)).flags;
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 getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
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?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
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: undefined,
1210
- payloads: undefined,
1627
+ flags: {},
1628
+ featureFlags: {},
1629
+ featureFlagPayloads: {},
1630
+ requestId: decideResponse?.requestId,
1211
1631
  };
1212
1632
  }
1213
- const flags = decideResponse?.featureFlags;
1214
- const payloads = decideResponse?.featureFlagPayloads;
1215
- let parsedPayloads = payloads;
1216
- if (payloads) {
1217
- parsedPayloads = Object.fromEntries(Object.entries(payloads).map(([k, v]) => [k, this._parsePayload(v)]));
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
- return {
1220
- flags,
1221
- payloads: parsedPayloads,
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
- if (!this.flushPromise) {
1284
- this.flushPromise = this._flush().finally(() => {
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
- this.addPendingPromise(this.flushPromise);
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
- const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1836
+ let queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1307
1837
  if (!queue.length) {
1308
1838
  return [];
1309
1839
  }
1310
- const items = queue.slice(0, this.maxBatchSize);
1311
- const messages = items.map((item) => item.message);
1312
- const persistQueueChange = () => {
1313
- const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1314
- this.setPersistedProperty(PostHogPersistedProperty.Queue, refreshedQueue.slice(items.length));
1315
- };
1316
- const data = {
1317
- api_key: this.apiKey,
1318
- batch: messages,
1319
- sent_at: currentISOTime(),
1320
- };
1321
- if (this.historicalMigration) {
1322
- data.historical_migration = true;
1323
- }
1324
- const payload = JSON.stringify(data);
1325
- const url = this.captureMode === 'form'
1326
- ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1327
- : `${this.host}/batch/`;
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
- method: 'POST',
1338
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1339
- body: payload,
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
- try {
1342
- await this.fetchWithRetry(url, fetchOptions);
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
- this._events.emit('error', err);
1351
- throw err;
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
- persistQueueChange();
1354
- this._events.emit('flush', messages);
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 shutdown(shutdownTimeoutMs = 30000) {
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
- this.logMsgIfDebug(() => console.error('Error while shutting down PostHog', e));
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 bootstrapfeatureFlags = bootstrap.featureFlags;
1462
- if (bootstrapfeatureFlags && Object.keys(bootstrapfeatureFlags).length) {
1463
- const bootstrapFlags = Object.keys(bootstrapfeatureFlags)
1464
- .filter((flag) => !!bootstrapfeatureFlags[flag])
1465
- .reduce((res, key) => ((res[key] = bootstrapfeatureFlags[key] || false), res), {});
1466
- if (Object.keys(bootstrapFlags).length) {
1467
- this.setPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlags, bootstrapFlags);
1468
- const currentFlags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags) || {};
1469
- const newFeatureFlags = { ...bootstrapFlags, ...currentFlags };
1470
- this.setKnownFeatureFlags(newFeatureFlags);
1471
- }
1472
- const bootstrapFlagPayloads = bootstrap.featureFlagPayloads;
1473
- if (bootstrapFlagPayloads && Object.keys(bootstrapFlagPayloads).length) {
1474
- this.setPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlagPayloads, bootstrapFlagPayloads);
1475
- const currentFlagPayloads = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads) || {};
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.setKnownFeatureFlags(null);
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 newFeatureFlags = res.featureFlags;
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 currentFlags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags);
1793
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Cached feature flags: ', JSON.stringify(currentFlags)));
1794
- const currentFlagPayloads = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads);
1795
- newFeatureFlags = { ...currentFlags, ...res.featureFlags };
1796
- newFeatureFlagPayloads = { ...currentFlagPayloads, ...res.featureFlagPayloads };
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.setKnownFeatureFlags(newFeatureFlags);
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
- const sessionReplay = res?.sessionRecording;
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
- setKnownFeatureFlags(featureFlags) {
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.FeatureFlags, featureFlags);
1822
- this._events.emit('featureflags', featureFlags);
2423
+ this.setPersistedProperty(PostHogPersistedProperty.FeatureFlagDetails, decideResponse);
2424
+ this._events.emit('featureflags', getFlagValuesFromFlags(decideResponse?.flags ?? {}));
1823
2425
  });
1824
2426
  }
1825
- setKnownFeatureFlagPayloads(featureFlagPayloads) {
1826
- this.wrap(() => {
1827
- this.setPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads, featureFlagPayloads);
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 featureFlags = this.getFeatureFlags();
1832
- if (!featureFlags) {
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
- let response = featureFlags[key];
1837
- // `/decide` v3 returns all flags
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
- $feature_flag_bootstrapped_response: this.getPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlags)?.[key],
1848
- $feature_flag_bootstrapped_payload: this.getPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlagPayloads)?.[key],
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
- const payloads = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlagPayloads);
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
- let flags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags);
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 flags;
2536
+ return details;
1879
2537
  }
1880
- flags = flags || {};
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
- return flags;
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.js.map
3107
+ //# sourceMappingURL=index.cjs.map