posthog-node 4.0.0-beta.1 → 4.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,19 @@
1
+ # 4.0.0-beta.3 - 2024-03-13
2
+
3
+ 1. Sets `User-Agent` headers with SDK name and version for RN
4
+
5
+ # 4.0.0-beta.2 - 2024-03-12
6
+
7
+ 1. `flushAsync` and `shutdownAsync` are removed with `flush` and `shutdown` now being the async methods.
8
+ 2. Fixed an issue where `shutdown` would potentially exit early if a flush was already in progress
9
+ 3. Flushes will now try to flush up to `maxBatchSize` (default 100) in one go
10
+
1
11
  # 4.0.0-beta.1 - 2024-03-04
2
12
 
3
- - Adds a `disabled` option and the ability to change it later via `posthog.disabled = true`. Useful for disabling PostHog tracking for example in a testing environment without having complex conditional checking
4
- - Fixes some typos in types
5
- - `shutdown` and `shutdownAsync` takes a `shutdownTimeoutMs` param with a default of 30000 (30s). This is the time to wait for flushing events before shutting down the client. If the timeout is reached, the client will be shut down regardless of pending events.
6
- - Adds a new `featureFlagsRequestTimeoutMs` timeout parameter for feature flags which defaults to 3 seconds, updated from the default 10s for all other API calls.
13
+ 1. Adds a `disabled` option and the ability to change it later via `posthog.disabled = true`. Useful for disabling PostHog tracking for example in a testing environment without having complex conditional checking
14
+ 2. Fixes some typos in types
15
+ 3. `shutdown` and `shutdownAsync` takes a `shutdownTimeoutMs` param with a default of 30000 (30s). This is the time to wait for flushing events before shutting down the client. If the timeout is reached, the client will be shut down regardless of pending events.
16
+ 4. Adds a new `featureFlagsRequestTimeoutMs` timeout parameter for feature flags which defaults to 3 seconds, updated from the default 10s for all other API calls.
7
17
 
8
18
  # 3.6.3 - 2024-02-15
9
19
 
package/lib/index.cjs.js CHANGED
@@ -4,7 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var rusha = require('rusha');
6
6
 
7
- var version = "4.0.0-beta.1";
7
+ var version = "4.0.0-beta.3";
8
8
 
9
9
  var PostHogPersistedProperty;
10
10
  (function (PostHogPersistedProperty) {
@@ -950,6 +950,7 @@ function isPostHogFetchError(err) {
950
950
  }
951
951
  class PostHogCoreStateless {
952
952
  constructor(apiKey, options) {
953
+ this.flushPromise = null;
953
954
  this.disableGeoip = true;
954
955
  this.disabled = false;
955
956
  this.defaultOptIn = true;
@@ -961,6 +962,7 @@ class PostHogCoreStateless {
961
962
  this.apiKey = apiKey;
962
963
  this.host = removeTrailingSlash(options?.host || 'https://app.posthog.com');
963
964
  this.flushAt = options?.flushAt ? Math.max(options?.flushAt, 1) : 20;
965
+ this.maxBatchSize = Math.max(this.flushAt, options?.maxBatchSize ?? 100);
964
966
  this.flushInterval = options?.flushInterval ?? 10000;
965
967
  this.captureMode = options?.captureMode || 'form';
966
968
  // If enable is explicitly set to false we override the optout
@@ -1039,9 +1041,12 @@ class PostHogCoreStateless {
1039
1041
  addPendingPromise(promise) {
1040
1042
  const promiseUUID = uuidv7();
1041
1043
  this.pendingPromises[promiseUUID] = promise;
1042
- promise.finally(() => {
1044
+ promise
1045
+ .catch(() => { })
1046
+ .finally(() => {
1043
1047
  delete this.pendingPromises[promiseUUID];
1044
1048
  });
1049
+ return promise;
1045
1050
  }
1046
1051
  /***
1047
1052
  *** TRACKING
@@ -1106,7 +1111,7 @@ class PostHogCoreStateless {
1106
1111
  const url = `${this.host}/decide/?v=3`;
1107
1112
  const fetchOptions = {
1108
1113
  method: 'POST',
1109
- headers: { 'Content-Type': 'application/json' },
1114
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1110
1115
  body: JSON.stringify({
1111
1116
  token: this.apiKey,
1112
1117
  distinct_id: distinctId,
@@ -1221,75 +1226,97 @@ class PostHogCoreStateless {
1221
1226
  this._events.emit(type, message);
1222
1227
  // Flush queued events if we meet the flushAt length
1223
1228
  if (queue.length >= this.flushAt) {
1224
- this.flush();
1229
+ this.flushBackground();
1225
1230
  }
1226
1231
  if (this.flushInterval && !this._flushTimer) {
1227
- this._flushTimer = safeSetTimeout(() => this.flush(), this.flushInterval);
1232
+ this._flushTimer = safeSetTimeout(() => this.flushBackground(), this.flushInterval);
1228
1233
  }
1229
1234
  });
1230
1235
  }
1231
- async flushAsync() {
1232
- await this._initPromise;
1233
- return new Promise((resolve, reject) => {
1234
- this.flush((err, data) => {
1235
- return err ? reject(err) : resolve(data);
1236
+ clearFlushTimer() {
1237
+ if (this._flushTimer) {
1238
+ clearTimeout(this._flushTimer);
1239
+ this._flushTimer = undefined;
1240
+ }
1241
+ }
1242
+ /**
1243
+ * Helper for flushing the queue in the background
1244
+ * Avoids unnecessary promise errors
1245
+ */
1246
+ flushBackground() {
1247
+ void this.flush().catch(() => { });
1248
+ }
1249
+ async flush() {
1250
+ if (!this.flushPromise) {
1251
+ this.flushPromise = this._flush().finally(() => {
1252
+ this.flushPromise = null;
1236
1253
  });
1237
- });
1254
+ this.addPendingPromise(this.flushPromise);
1255
+ }
1256
+ return this.flushPromise;
1238
1257
  }
1239
- flush(callback) {
1240
- this.wrap(() => {
1241
- if (this._flushTimer) {
1242
- clearTimeout(this._flushTimer);
1243
- this._flushTimer = null;
1244
- }
1245
- const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1246
- if (!queue.length) {
1247
- return callback?.();
1258
+ getCustomHeaders() {
1259
+ // Don't set the user agent if we're not on a browser. The latest spec allows
1260
+ // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers
1261
+ // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader),
1262
+ // but browsers such as Chrome and Safari have not caught up.
1263
+ const customUserAgent = this.getCustomUserAgent();
1264
+ const headers = {};
1265
+ if (customUserAgent && customUserAgent !== '') {
1266
+ headers['User-Agent'] = customUserAgent;
1267
+ }
1268
+ return headers;
1269
+ }
1270
+ async _flush() {
1271
+ this.clearFlushTimer();
1272
+ await this._initPromise;
1273
+ const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1274
+ if (!queue.length) {
1275
+ return [];
1276
+ }
1277
+ const items = queue.slice(0, this.maxBatchSize);
1278
+ const messages = items.map((item) => item.message);
1279
+ const persistQueueChange = () => {
1280
+ const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1281
+ this.setPersistedProperty(PostHogPersistedProperty.Queue, refreshedQueue.slice(items.length));
1282
+ };
1283
+ const data = {
1284
+ api_key: this.apiKey,
1285
+ batch: messages,
1286
+ sent_at: currentISOTime(),
1287
+ };
1288
+ const payload = JSON.stringify(data);
1289
+ const url = this.captureMode === 'form'
1290
+ ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1291
+ : `${this.host}/batch/`;
1292
+ const fetchOptions = this.captureMode === 'form'
1293
+ ? {
1294
+ method: 'POST',
1295
+ mode: 'no-cors',
1296
+ credentials: 'omit',
1297
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
1298
+ body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
1248
1299
  }
1249
- const items = queue.splice(0, this.flushAt);
1250
- this.setPersistedProperty(PostHogPersistedProperty.Queue, queue);
1251
- const messages = items.map((item) => item.message);
1252
- const data = {
1253
- api_key: this.apiKey,
1254
- batch: messages,
1255
- sent_at: currentISOTime(),
1256
- };
1257
- const done = (err) => {
1258
- if (err) {
1259
- this._events.emit('error', err);
1260
- }
1261
- callback?.(err, messages);
1262
- this._events.emit('flush', messages);
1300
+ : {
1301
+ method: 'POST',
1302
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1303
+ body: payload,
1263
1304
  };
1264
- // Don't set the user agent if we're not on a browser. The latest spec allows
1265
- // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers
1266
- // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader),
1267
- // but browsers such as Chrome and Safari have not caught up.
1268
- this.getCustomUserAgent();
1269
- const payload = JSON.stringify(data);
1270
- const url = this.captureMode === 'form'
1271
- ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1272
- : `${this.host}/batch/`;
1273
- const fetchOptions = this.captureMode === 'form'
1274
- ? {
1275
- method: 'POST',
1276
- mode: 'no-cors',
1277
- credentials: 'omit',
1278
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1279
- body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
1280
- }
1281
- : {
1282
- method: 'POST',
1283
- headers: { 'Content-Type': 'application/json' },
1284
- body: payload,
1285
- };
1286
- const requestPromise = this.fetchWithRetry(url, fetchOptions);
1287
- this.addPendingPromise(requestPromise
1288
- .then(() => done())
1289
- .catch((err) => {
1290
- done(err);
1291
- }));
1292
- });
1305
+ try {
1306
+ await this.fetchWithRetry(url, fetchOptions);
1307
+ }
1308
+ catch (err) {
1309
+ // depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
1310
+ // 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
1311
+ if (!(err instanceof PostHogFetchNetworkError)) {
1312
+ persistQueueChange();
1313
+ }
1314
+ this._events.emit('error', err);
1315
+ throw err;
1316
+ }
1317
+ persistQueueChange();
1318
+ this._events.emit('flush', messages);
1319
+ return messages;
1293
1320
  }
1294
1321
  async fetchWithRetry(url, options, retryOptions, requestTimeout) {
1295
1322
  var _a;
@@ -1320,15 +1347,12 @@ class PostHogCoreStateless {
1320
1347
  return res;
1321
1348
  }, { ...this._retryOptions, ...retryOptions });
1322
1349
  }
1323
- async shutdownAsync(shutdownTimeoutMs) {
1350
+ async shutdown(shutdownTimeoutMs = 30000) {
1324
1351
  await this._initPromise;
1325
- clearTimeout(this._flushTimer);
1352
+ this.clearFlushTimer();
1326
1353
  try {
1327
- await Promise.all(Object.values(this.pendingPromises).map((x) => x.catch(() => {
1328
- // ignore errors as we are shutting down and can't deal with them anyways.
1329
- })));
1330
- const timeout = shutdownTimeoutMs ?? 30000;
1331
- const startTimeWithDelay = Date.now() + timeout;
1354
+ await Promise.all(Object.values(this.pendingPromises));
1355
+ const startTimeWithDelay = Date.now() + shutdownTimeoutMs;
1332
1356
  while (true) {
1333
1357
  const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1334
1358
  if (queue.length === 0) {
@@ -1337,7 +1361,7 @@ class PostHogCoreStateless {
1337
1361
  // flush again to make sure we send all events, some of which might've been added
1338
1362
  // while we were waiting for the pending promises to resolve
1339
1363
  // For example, see sendFeatureFlags in posthog-node/src/posthog-node.ts::capture
1340
- await this.flushAsync();
1364
+ await this.flush();
1341
1365
  // If we've been waiting for more than the shutdownTimeoutMs, stop it
1342
1366
  const now = Date.now();
1343
1367
  if (startTimeWithDelay < now) {
@@ -1352,9 +1376,6 @@ class PostHogCoreStateless {
1352
1376
  console.error('Error while shutting down PostHog', e);
1353
1377
  }
1354
1378
  }
1355
- shutdown(shutdownTimeoutMs) {
1356
- void this.shutdownAsync(shutdownTimeoutMs);
1357
- }
1358
1379
  }
1359
1380
 
1360
1381
  class PostHogMemoryStorage {
@@ -1439,6 +1460,7 @@ class FeatureFlagsPoller {
1439
1460
  projectApiKey,
1440
1461
  timeout,
1441
1462
  host,
1463
+ customHeaders,
1442
1464
  ...options
1443
1465
  }) {
1444
1466
  this.debugMode = false;
@@ -1456,6 +1478,7 @@ class FeatureFlagsPoller {
1456
1478
 
1457
1479
  this.fetch = options.fetch || fetch$1;
1458
1480
  this.onError = options.onError;
1481
+ this.customHeaders = customHeaders;
1459
1482
  void this.loadFeatureFlags();
1460
1483
  }
1461
1484
 
@@ -1758,10 +1781,9 @@ class FeatureFlagsPoller {
1758
1781
  const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`;
1759
1782
  const options = {
1760
1783
  method: 'GET',
1761
- headers: {
1784
+ headers: { ...this.customHeaders,
1762
1785
  'Content-Type': 'application/json',
1763
- Authorization: `Bearer ${this.personalApiKey}`,
1764
- 'user-agent': `posthog-node/${version}`
1786
+ Authorization: `Bearer ${this.personalApiKey}`
1765
1787
  }
1766
1788
  };
1767
1789
  let abortTimeout = null;
@@ -2103,7 +2125,8 @@ class PostHog extends PostHogCoreStateless {
2103
2125
  fetch: options.fetch,
2104
2126
  onError: err => {
2105
2127
  this._events.emit('error', err);
2106
- }
2128
+ },
2129
+ customHeaders: this.getCustomHeaders()
2107
2130
  });
2108
2131
  }
2109
2132
 
@@ -2132,7 +2155,7 @@ class PostHog extends PostHogCoreStateless {
2132
2155
  }
2133
2156
 
2134
2157
  getCustomUserAgent() {
2135
- return `posthog-node/${version}`;
2158
+ return `${this.getLibraryId()}/${this.getLibraryVersion()}`;
2136
2159
  }
2137
2160
 
2138
2161
  enable() {
@@ -2433,13 +2456,9 @@ class PostHog extends PostHogCoreStateless {
2433
2456
  await this.featureFlagsPoller?.loadFeatureFlags(true);
2434
2457
  }
2435
2458
 
2436
- shutdown(shutdownTimeoutMs) {
2437
- void this.shutdownAsync(shutdownTimeoutMs);
2438
- }
2439
-
2440
- async shutdownAsync(shutdownTimeoutMs) {
2459
+ async shutdown(shutdownTimeoutMs) {
2441
2460
  this.featureFlagsPoller?.stopPoller();
2442
- return super.shutdownAsync(shutdownTimeoutMs);
2461
+ return super.shutdown(shutdownTimeoutMs);
2443
2462
  }
2444
2463
 
2445
2464
  addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties) {