posthog-node 4.15.0 → 4.16.0

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,3 +1,7 @@
1
+ # 4.16.0 - 2025-05-01
2
+
3
+ 1. chore: improve flush event
4
+
1
5
  # 4.15.0 - 2025-04-30
2
6
 
3
7
  1. chore: add immediate-mode
package/lib/index.cjs.js CHANGED
@@ -22,7 +22,7 @@ function _interopNamespace(e) {
22
22
  return Object.freeze(n);
23
23
  }
24
24
 
25
- var version = "4.15.0";
25
+ var version = "4.16.0";
26
26
 
27
27
  var PostHogPersistedProperty;
28
28
  (function (PostHogPersistedProperty) {
@@ -1260,6 +1260,9 @@ async function logFlushError(err) {
1260
1260
  function isPostHogFetchError(err) {
1261
1261
  return typeof err === 'object' && (err instanceof PostHogFetchHttpError || err instanceof PostHogFetchNetworkError);
1262
1262
  }
1263
+ function isPostHogFetchContentTooLargeError(err) {
1264
+ return typeof err === 'object' && err instanceof PostHogFetchHttpError && err.status === 413;
1265
+ }
1263
1266
  var QuotaLimitedFeature;
1264
1267
  (function (QuotaLimitedFeature) {
1265
1268
  QuotaLimitedFeature["FeatureFlags"] = "feature_flags";
@@ -1268,6 +1271,7 @@ var QuotaLimitedFeature;
1268
1271
  class PostHogCoreStateless {
1269
1272
  constructor(apiKey, options) {
1270
1273
  this.flushPromise = null;
1274
+ this.shutdownPromise = null;
1271
1275
  this.pendingPromises = {};
1272
1276
  // internal
1273
1277
  this._events = new SimpleEventEmitter();
@@ -1780,13 +1784,21 @@ class PostHogCoreStateless {
1780
1784
  });
1781
1785
  }
1782
1786
  async flush() {
1783
- if (!this.flushPromise) {
1784
- this.flushPromise = this._flush().finally(() => {
1787
+ // Wait for the current flush operation to finish (regardless of success or failure), then try to flush again.
1788
+ // Use allSettled instead of finally to be defensive around flush throwing errors immediately rather than rejecting.
1789
+ const nextFlushPromise = Promise.allSettled([this.flushPromise]).then(() => {
1790
+ return this._flush();
1791
+ });
1792
+ this.flushPromise = nextFlushPromise;
1793
+ void this.addPendingPromise(nextFlushPromise);
1794
+ Promise.allSettled([nextFlushPromise]).then(() => {
1795
+ // If there are no others waiting to flush, clear the promise.
1796
+ // We don't strictly need to do this, but it could make debugging easier
1797
+ if (this.flushPromise === nextFlushPromise) {
1785
1798
  this.flushPromise = null;
1786
- });
1787
- this.addPendingPromise(this.flushPromise);
1788
- }
1789
- return this.flushPromise;
1799
+ }
1800
+ });
1801
+ return nextFlushPromise;
1790
1802
  }
1791
1803
  getCustomHeaders() {
1792
1804
  // Don't set the user agent if we're not on a browser. The latest spec allows
@@ -1803,56 +1815,80 @@ class PostHogCoreStateless {
1803
1815
  async _flush() {
1804
1816
  this.clearFlushTimer();
1805
1817
  await this._initPromise;
1806
- const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1818
+ let queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1807
1819
  if (!queue.length) {
1808
1820
  return [];
1809
1821
  }
1810
- const items = queue.slice(0, this.maxBatchSize);
1811
- const messages = items.map((item) => item.message);
1812
- const persistQueueChange = () => {
1813
- const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1814
- this.setPersistedProperty(PostHogPersistedProperty.Queue, refreshedQueue.slice(items.length));
1815
- };
1816
- const data = {
1817
- api_key: this.apiKey,
1818
- batch: messages,
1819
- sent_at: currentISOTime(),
1820
- };
1821
- if (this.historicalMigration) {
1822
- data.historical_migration = true;
1823
- }
1824
- const payload = JSON.stringify(data);
1825
- const url = this.captureMode === 'form'
1826
- ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1827
- : `${this.host}/batch/`;
1828
- const fetchOptions = this.captureMode === 'form'
1829
- ? {
1830
- method: 'POST',
1831
- mode: 'no-cors',
1832
- credentials: 'omit',
1833
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
1834
- body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
1822
+ const sentMessages = [];
1823
+ const originalQueueLength = queue.length;
1824
+ while (queue.length > 0 && sentMessages.length < originalQueueLength) {
1825
+ const batchItems = queue.slice(0, this.maxBatchSize);
1826
+ const batchMessages = batchItems.map((item) => item.message);
1827
+ const persistQueueChange = () => {
1828
+ const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1829
+ const newQueue = refreshedQueue.slice(batchItems.length);
1830
+ this.setPersistedProperty(PostHogPersistedProperty.Queue, newQueue);
1831
+ queue = newQueue;
1832
+ };
1833
+ const data = {
1834
+ api_key: this.apiKey,
1835
+ batch: batchMessages,
1836
+ sent_at: currentISOTime(),
1837
+ };
1838
+ if (this.historicalMigration) {
1839
+ data.historical_migration = true;
1835
1840
  }
1836
- : {
1837
- method: 'POST',
1838
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1839
- body: payload,
1841
+ const payload = JSON.stringify(data);
1842
+ const url = this.captureMode === 'form'
1843
+ ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1844
+ : `${this.host}/batch/`;
1845
+ const fetchOptions = this.captureMode === 'form'
1846
+ ? {
1847
+ method: 'POST',
1848
+ mode: 'no-cors',
1849
+ credentials: 'omit',
1850
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
1851
+ body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
1852
+ }
1853
+ : {
1854
+ method: 'POST',
1855
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1856
+ body: payload,
1857
+ };
1858
+ const retryOptions = {
1859
+ retryCheck: (err) => {
1860
+ // don't automatically retry on 413 errors, we want to reduce the batch size first
1861
+ if (isPostHogFetchContentTooLargeError(err)) {
1862
+ return false;
1863
+ }
1864
+ // otherwise, retry on network errors
1865
+ return isPostHogFetchError(err);
1866
+ },
1840
1867
  };
1841
- try {
1842
- await this.fetchWithRetry(url, fetchOptions);
1843
- }
1844
- catch (err) {
1845
- // depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
1846
- // 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
1847
- if (!(err instanceof PostHogFetchNetworkError)) {
1848
- persistQueueChange();
1868
+ try {
1869
+ await this.fetchWithRetry(url, fetchOptions, retryOptions);
1849
1870
  }
1850
- this._events.emit('error', err);
1851
- throw err;
1871
+ catch (err) {
1872
+ if (isPostHogFetchContentTooLargeError(err) && batchMessages.length > 1) {
1873
+ // if we get a 413 error, we want to reduce the batch size and try again
1874
+ this.maxBatchSize = Math.max(1, Math.floor(batchMessages.length / 2));
1875
+ this.logMsgIfDebug(() => console.warn(`Received 413 when sending batch of size ${batchMessages.length}, reducing batch size to ${this.maxBatchSize}`));
1876
+ // do not persist the queue change, we want to retry the same batch
1877
+ continue;
1878
+ }
1879
+ // depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
1880
+ // 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
1881
+ if (!(err instanceof PostHogFetchNetworkError)) {
1882
+ persistQueueChange();
1883
+ }
1884
+ this._events.emit('error', err);
1885
+ throw err;
1886
+ }
1887
+ persistQueueChange();
1888
+ sentMessages.push(...batchMessages);
1852
1889
  }
1853
- persistQueueChange();
1854
- this._events.emit('flush', messages);
1855
- return messages;
1890
+ this._events.emit('flush', sentMessages);
1891
+ return sentMessages;
1856
1892
  }
1857
1893
  async fetchWithRetry(url, options, retryOptions, requestTimeout) {
1858
1894
  var _a;
@@ -1885,7 +1921,7 @@ class PostHogCoreStateless {
1885
1921
  return res;
1886
1922
  }, { ...this._retryOptions, ...retryOptions });
1887
1923
  }
1888
- async shutdown(shutdownTimeoutMs = 30000) {
1924
+ async _shutdown(shutdownTimeoutMs = 30000) {
1889
1925
  // A little tricky - we want to have a max shutdown time and enforce it, even if that means we have some
1890
1926
  // dangling promises. We'll keep track of the timeout and resolve/reject based on that.
1891
1927
  await this._initPromise;
@@ -1926,6 +1962,22 @@ class PostHogCoreStateless {
1926
1962
  doShutdown(),
1927
1963
  ]);
1928
1964
  }
1965
+ /**
1966
+ * Call shutdown() once before the node process exits, so ensure that all events have been sent and all promises
1967
+ * have resolved. Do not use this function if you intend to keep using this PostHog instance after calling it.
1968
+ * @param shutdownTimeoutMs
1969
+ */
1970
+ async shutdown(shutdownTimeoutMs = 30000) {
1971
+ if (this.shutdownPromise) {
1972
+ 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'));
1973
+ }
1974
+ else {
1975
+ this.shutdownPromise = this._shutdown(shutdownTimeoutMs).finally(() => {
1976
+ this.shutdownPromise = null;
1977
+ });
1978
+ }
1979
+ return this.shutdownPromise;
1980
+ }
1929
1981
  }
1930
1982
 
1931
1983
  class PostHogMemoryStorage {
@@ -4117,9 +4169,9 @@ class PostHog extends PostHogCoreStateless {
4117
4169
  async reloadFeatureFlags() {
4118
4170
  await this.featureFlagsPoller?.loadFeatureFlags(true);
4119
4171
  }
4120
- async shutdown(shutdownTimeoutMs) {
4172
+ async _shutdown(shutdownTimeoutMs) {
4121
4173
  this.featureFlagsPoller?.stopPoller();
4122
- return super.shutdown(shutdownTimeoutMs);
4174
+ return super._shutdown(shutdownTimeoutMs);
4123
4175
  }
4124
4176
  addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties) {
4125
4177
  const allPersonProperties = {