posthog-node 5.8.0 → 5.8.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,4 +1,4 @@
1
- import { PostHogCoreStateless, getFeatureFlagValue } from '@posthog/core';
1
+ import { PostHogCoreStateless, getFeatureFlagValue, safeSetTimeout as safeSetTimeout$1 } from '@posthog/core';
2
2
 
3
3
  /**
4
4
  * @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
@@ -313,7 +313,7 @@ function makeUncaughtExceptionHandler(captureFn, onFatalFn) {
313
313
  });
314
314
  if (!calledFatalError && processWouldExit) {
315
315
  calledFatalError = true;
316
- onFatalFn();
316
+ onFatalFn(error);
317
317
  }
318
318
  }, {
319
319
  _posthogErrorHandler: true
@@ -324,7 +324,7 @@ function addUncaughtExceptionListener(captureFn, onFatalFn) {
324
324
  }
325
325
  function addUnhandledRejectionListener(captureFn) {
326
326
  global.process.on('unhandledRejection', reason => {
327
- captureFn(reason, {
327
+ return captureFn(reason, {
328
328
  mechanism: {
329
329
  type: 'onunhandledrejection',
330
330
  handled: false
@@ -341,8 +341,7 @@ let cachedFilenameChunkIds;
341
341
  function getFilenameToChunkIdMap(stackParser) {
342
342
  const chunkIdMap = globalThis._posthogChunkIds;
343
343
  if (!chunkIdMap) {
344
- console.error('No chunk id map found');
345
- return {};
344
+ return null;
346
345
  }
347
346
  const chunkIdKeys = Object.keys(chunkIdMap);
348
347
  if (cachedFilenameChunkIds && chunkIdKeys.length === lastKeysCount) {
@@ -613,7 +612,7 @@ function parseStackFrames(stackParser, error) {
613
612
  function applyChunkIds(frames, parser) {
614
613
  const filenameChunkIdMap = getFilenameToChunkIdMap(parser);
615
614
  frames.forEach(frame => {
616
- if (frame.filename) {
615
+ if (frame.filename && filenameChunkIdMap) {
617
616
  frame.chunk_id = filenameChunkIdMap[frame.filename];
618
617
  }
619
618
  });
@@ -642,6 +641,12 @@ function clampToRange(value, min, max, logger, fallbackValue) {
642
641
  }
643
642
 
644
643
  class BucketedRateLimiter {
644
+ stop() {
645
+ if (this._removeInterval) {
646
+ clearInterval(this._removeInterval);
647
+ this._removeInterval = void 0;
648
+ }
649
+ }
645
650
  constructor(_options){
646
651
  this._options = _options;
647
652
  this._buckets = {};
@@ -673,7 +678,7 @@ class BucketedRateLimiter {
673
678
  this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
674
679
  this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
675
680
  this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
676
- setInterval(()=>{
681
+ this._removeInterval = setInterval(()=>{
677
682
  this._refillBuckets();
678
683
  }, this._refillInterval);
679
684
  }
@@ -687,6 +692,22 @@ function safeSetTimeout(fn, timeout) {
687
692
 
688
693
  const SHUTDOWN_TIMEOUT = 2000;
689
694
  class ErrorTracking {
695
+ constructor(client, options, _logger) {
696
+ this.client = client;
697
+ this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
698
+ this._logger = _logger;
699
+ // by default captures ten exceptions before rate limiting by exception type
700
+ // refills at a rate of one token / 10 second period
701
+ // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
702
+ this._rateLimiter = new BucketedRateLimiter({
703
+ refillRate: 1,
704
+ bucketSize: 10,
705
+ refillInterval: 10000,
706
+ // ten seconds in milliseconds
707
+ _logger: this._logger
708
+ });
709
+ this.startAutocaptureIfEnabled();
710
+ }
690
711
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
691
712
  const properties = {
692
713
  ...additionalProperties
@@ -706,31 +727,16 @@ class ErrorTracking {
706
727
  }
707
728
  };
708
729
  }
709
- constructor(client, options, _logger) {
710
- this.client = client;
711
- this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
712
- this._logger = _logger;
713
- // by default captures ten exceptions before rate limiting by exception type
714
- // refills at a rate of one token / 10 second period
715
- // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
716
- this._rateLimiter = new BucketedRateLimiter({
717
- refillRate: 1,
718
- bucketSize: 10,
719
- refillInterval: 10000,
720
- // ten seconds in milliseconds
721
- _logger: this._logger
722
- });
723
- this.startAutocaptureIfEnabled();
724
- }
725
730
  startAutocaptureIfEnabled() {
726
731
  if (this.isEnabled()) {
727
732
  addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this));
728
733
  addUnhandledRejectionListener(this.onException.bind(this));
729
734
  }
730
735
  }
731
- onException(exception, hint) {
732
- return ErrorTracking.buildEventMessage(exception, hint).then(msg => {
733
- const exceptionProperties = msg.properties;
736
+ async onException(exception, hint) {
737
+ this.client.addPendingPromise((async () => {
738
+ const eventMessage = await ErrorTracking.buildEventMessage(exception, hint);
739
+ const exceptionProperties = eventMessage.properties;
734
740
  const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
735
741
  const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
736
742
  if (isRateLimited) {
@@ -739,15 +745,20 @@ class ErrorTracking {
739
745
  });
740
746
  return;
741
747
  }
742
- this.client.capture(msg);
743
- });
748
+ return this.client.capture(eventMessage);
749
+ })());
744
750
  }
745
- async onFatalError() {
751
+ async onFatalError(exception) {
752
+ console.error(exception);
746
753
  await this.client.shutdown(SHUTDOWN_TIMEOUT);
754
+ process.exit(1);
747
755
  }
748
756
  isEnabled() {
749
757
  return !this.client.isDisabled && this._exceptionAutocaptureEnabled;
750
758
  }
759
+ shutdown() {
760
+ this._rateLimiter.stop();
761
+ }
751
762
  }
752
763
 
753
764
  function setupExpressErrorHandler(_posthog, app) {
@@ -767,7 +778,7 @@ function setupExpressErrorHandler(_posthog, app) {
767
778
  });
768
779
  }
769
780
 
770
- var version = "5.8.0";
781
+ var version = "5.8.1";
771
782
 
772
783
  /**
773
784
  * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
@@ -1732,146 +1743,43 @@ class PostHogBackendClient extends PostHogCoreStateless {
1732
1743
  if (typeof props === 'string') {
1733
1744
  this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
1734
1745
  }
1735
- const {
1736
- distinctId,
1737
- event,
1738
- properties,
1739
- groups,
1740
- sendFeatureFlags,
1741
- timestamp,
1742
- disableGeoip,
1743
- uuid
1744
- } = props;
1745
- // Run before_send if configured
1746
- const eventMessage = this._runBeforeSend({
1746
+ this.addPendingPromise(this.prepareEventMessage(props).then(({
1747
1747
  distinctId,
1748
1748
  event,
1749
1749
  properties,
1750
- groups,
1751
- sendFeatureFlags,
1752
- timestamp,
1753
- disableGeoip,
1754
- uuid
1755
- });
1756
- if (!eventMessage) {
1757
- return;
1758
- }
1759
- const _capture = props => {
1760
- super.captureStateless(eventMessage.distinctId, eventMessage.event, props, {
1761
- timestamp: eventMessage.timestamp,
1762
- disableGeoip: eventMessage.disableGeoip,
1763
- uuid: eventMessage.uuid
1750
+ options
1751
+ }) => {
1752
+ return super.captureStateless(distinctId, event, properties, {
1753
+ timestamp: options.timestamp,
1754
+ disableGeoip: options.disableGeoip,
1755
+ uuid: options.uuid
1764
1756
  });
1765
- };
1766
- // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
1767
- const capturePromise = Promise.resolve().then(async () => {
1768
- if (sendFeatureFlags) {
1769
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
1770
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
1771
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
1772
- }
1773
- if (event === '$feature_flag_called') {
1774
- // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
1775
- return {};
1776
- }
1777
- return {};
1778
- }).then(flags => {
1779
- // Derive the relevant flag properties to add
1780
- const additionalProperties = {};
1781
- if (flags) {
1782
- for (const [feature, variant] of Object.entries(flags)) {
1783
- additionalProperties[`$feature/${feature}`] = variant;
1784
- }
1757
+ }).catch(err => {
1758
+ if (err) {
1759
+ console.error(err);
1785
1760
  }
1786
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
1787
- if (activeFlags.length > 0) {
1788
- additionalProperties['$active_feature_flags'] = activeFlags;
1789
- }
1790
- return additionalProperties;
1791
- }).catch(() => {
1792
- // Something went wrong getting the flag info - we should capture the event anyways
1793
- return {};
1794
- }).then(additionalProperties => {
1795
- // No matter what - capture the event
1796
- _capture({
1797
- ...additionalProperties,
1798
- ...(eventMessage.properties || {}),
1799
- $groups: eventMessage.groups || groups
1800
- });
1801
- });
1802
- this.addPendingPromise(capturePromise);
1761
+ }));
1803
1762
  }
1804
1763
  async captureImmediate(props) {
1805
1764
  if (typeof props === 'string') {
1806
- this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
1765
+ this.logMsgIfDebug(() => console.warn('Called captureImmediate() with a string as the first argument when an object was expected.'));
1807
1766
  }
1808
- const {
1767
+ return this.addPendingPromise(this.prepareEventMessage(props).then(({
1809
1768
  distinctId,
1810
1769
  event,
1811
1770
  properties,
1812
- groups,
1813
- sendFeatureFlags,
1814
- timestamp,
1815
- disableGeoip,
1816
- uuid
1817
- } = props;
1818
- // Run before_send if configured
1819
- const eventMessage = this._runBeforeSend({
1820
- distinctId,
1821
- event,
1822
- properties,
1823
- groups,
1824
- sendFeatureFlags,
1825
- timestamp,
1826
- disableGeoip,
1827
- uuid
1828
- });
1829
- if (!eventMessage) {
1830
- return;
1831
- }
1832
- const _capture = props => {
1833
- return super.captureStatelessImmediate(eventMessage.distinctId, eventMessage.event, props, {
1834
- timestamp: eventMessage.timestamp,
1835
- disableGeoip: eventMessage.disableGeoip,
1836
- uuid: eventMessage.uuid
1771
+ options
1772
+ }) => {
1773
+ return super.captureStatelessImmediate(distinctId, event, properties, {
1774
+ timestamp: options.timestamp,
1775
+ disableGeoip: options.disableGeoip,
1776
+ uuid: options.uuid
1837
1777
  });
1838
- };
1839
- const capturePromise = Promise.resolve().then(async () => {
1840
- if (sendFeatureFlags) {
1841
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
1842
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
1843
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
1844
- }
1845
- if (event === '$feature_flag_called') {
1846
- // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
1847
- return {};
1848
- }
1849
- return {};
1850
- }).then(flags => {
1851
- // Derive the relevant flag properties to add
1852
- const additionalProperties = {};
1853
- if (flags) {
1854
- for (const [feature, variant] of Object.entries(flags)) {
1855
- additionalProperties[`$feature/${feature}`] = variant;
1856
- }
1778
+ }).catch(err => {
1779
+ if (err) {
1780
+ console.error(err);
1857
1781
  }
1858
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
1859
- if (activeFlags.length > 0) {
1860
- additionalProperties['$active_feature_flags'] = activeFlags;
1861
- }
1862
- return additionalProperties;
1863
- }).catch(() => {
1864
- // Something went wrong getting the flag info - we should capture the event anyways
1865
- return {};
1866
- }).then(additionalProperties => {
1867
- // No matter what - capture the event
1868
- _capture({
1869
- ...additionalProperties,
1870
- ...(eventMessage.properties || {}),
1871
- $groups: eventMessage.groups || groups
1872
- });
1873
- });
1874
- await capturePromise;
1782
+ }));
1875
1783
  }
1876
1784
  identify({
1877
1785
  distinctId,
@@ -2141,6 +2049,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
2141
2049
  }
2142
2050
  async _shutdown(shutdownTimeoutMs) {
2143
2051
  this.featureFlagsPoller?.stopPoller();
2052
+ this.errorTracking.shutdown();
2144
2053
  return super._shutdown(shutdownTimeoutMs);
2145
2054
  }
2146
2055
  async _requestRemoteConfigPayload(flagKey) {
@@ -2159,7 +2068,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
2159
2068
  let abortTimeout = null;
2160
2069
  if (this.options.requestTimeout && typeof this.options.requestTimeout === 'number') {
2161
2070
  const controller = new AbortController();
2162
- abortTimeout = safeSetTimeout(() => {
2071
+ abortTimeout = safeSetTimeout$1(() => {
2163
2072
  controller.abort();
2164
2073
  }, this.options.requestTimeout);
2165
2074
  options.signal = controller.signal;
@@ -2268,18 +2177,88 @@ class PostHogBackendClient extends PostHogCoreStateless {
2268
2177
  }
2269
2178
  captureException(error, distinctId, additionalProperties) {
2270
2179
  const syntheticException = new Error('PostHog syntheticException');
2271
- ErrorTracking.buildEventMessage(error, {
2180
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2272
2181
  syntheticException
2273
- }, distinctId, additionalProperties).then(msg => {
2274
- this.capture(msg);
2275
- });
2182
+ }, distinctId, additionalProperties).then(msg => this.capture(msg)));
2276
2183
  }
2277
2184
  async captureExceptionImmediate(error, distinctId, additionalProperties) {
2278
2185
  const syntheticException = new Error('PostHog syntheticException');
2279
- const evtMsg = await ErrorTracking.buildEventMessage(error, {
2186
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2280
2187
  syntheticException
2281
- }, distinctId, additionalProperties);
2282
- return await this.captureImmediate(evtMsg);
2188
+ }, distinctId, additionalProperties).then(msg => this.captureImmediate(msg)));
2189
+ }
2190
+ async prepareEventMessage(props) {
2191
+ const {
2192
+ distinctId,
2193
+ event,
2194
+ properties,
2195
+ groups,
2196
+ sendFeatureFlags,
2197
+ timestamp,
2198
+ disableGeoip,
2199
+ uuid
2200
+ } = props;
2201
+ // Run before_send if configured
2202
+ const eventMessage = this._runBeforeSend({
2203
+ distinctId,
2204
+ event,
2205
+ properties,
2206
+ groups,
2207
+ sendFeatureFlags,
2208
+ timestamp,
2209
+ disableGeoip,
2210
+ uuid
2211
+ });
2212
+ if (!eventMessage) {
2213
+ return Promise.reject(null);
2214
+ }
2215
+ // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
2216
+ const eventProperties = await Promise.resolve().then(async () => {
2217
+ if (sendFeatureFlags) {
2218
+ // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
2219
+ const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
2220
+ return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
2221
+ }
2222
+ if (event === '$feature_flag_called') {
2223
+ // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
2224
+ return {};
2225
+ }
2226
+ return {};
2227
+ }).then(flags => {
2228
+ // Derive the relevant flag properties to add
2229
+ const additionalProperties = {};
2230
+ if (flags) {
2231
+ for (const [feature, variant] of Object.entries(flags)) {
2232
+ additionalProperties[`$feature/${feature}`] = variant;
2233
+ }
2234
+ }
2235
+ const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
2236
+ if (activeFlags.length > 0) {
2237
+ additionalProperties['$active_feature_flags'] = activeFlags;
2238
+ }
2239
+ return additionalProperties;
2240
+ }).catch(() => {
2241
+ // Something went wrong getting the flag info - we should capture the event anyways
2242
+ return {};
2243
+ }).then(additionalProperties => {
2244
+ // No matter what - capture the event
2245
+ const props = {
2246
+ ...additionalProperties,
2247
+ ...(eventMessage.properties || {}),
2248
+ $groups: eventMessage.groups || groups
2249
+ };
2250
+ return props;
2251
+ });
2252
+ return {
2253
+ distinctId: eventMessage.distinctId,
2254
+ event: eventMessage.event,
2255
+ properties: eventProperties,
2256
+ options: {
2257
+ timestamp: eventMessage.timestamp,
2258
+ disableGeoip: eventMessage.disableGeoip,
2259
+ uuid: eventMessage.uuid
2260
+ }
2261
+ };
2283
2262
  }
2284
2263
  _runBeforeSend(eventMessage) {
2285
2264
  const beforeSend = this.options.before_send;