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,7 +1,7 @@
1
1
  import { dirname, posix, sep } from 'path';
2
2
  import { createReadStream } from 'node:fs';
3
3
  import { createInterface } from 'node:readline';
4
- import { PostHogCoreStateless, getFeatureFlagValue } from '@posthog/core';
4
+ import { PostHogCoreStateless, getFeatureFlagValue, safeSetTimeout as safeSetTimeout$1 } from '@posthog/core';
5
5
 
6
6
  /**
7
7
  * @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
@@ -316,7 +316,7 @@ function makeUncaughtExceptionHandler(captureFn, onFatalFn) {
316
316
  });
317
317
  if (!calledFatalError && processWouldExit) {
318
318
  calledFatalError = true;
319
- onFatalFn();
319
+ onFatalFn(error);
320
320
  }
321
321
  }, {
322
322
  _posthogErrorHandler: true
@@ -327,7 +327,7 @@ function addUncaughtExceptionListener(captureFn, onFatalFn) {
327
327
  }
328
328
  function addUnhandledRejectionListener(captureFn) {
329
329
  global.process.on('unhandledRejection', reason => {
330
- captureFn(reason, {
330
+ return captureFn(reason, {
331
331
  mechanism: {
332
332
  type: 'onunhandledrejection',
333
333
  handled: false
@@ -344,8 +344,7 @@ let cachedFilenameChunkIds;
344
344
  function getFilenameToChunkIdMap(stackParser) {
345
345
  const chunkIdMap = globalThis._posthogChunkIds;
346
346
  if (!chunkIdMap) {
347
- console.error('No chunk id map found');
348
- return {};
347
+ return null;
349
348
  }
350
349
  const chunkIdKeys = Object.keys(chunkIdMap);
351
350
  if (cachedFilenameChunkIds && chunkIdKeys.length === lastKeysCount) {
@@ -616,7 +615,7 @@ function parseStackFrames(stackParser, error) {
616
615
  function applyChunkIds(frames, parser) {
617
616
  const filenameChunkIdMap = getFilenameToChunkIdMap(parser);
618
617
  frames.forEach(frame => {
619
- if (frame.filename) {
618
+ if (frame.filename && filenameChunkIdMap) {
620
619
  frame.chunk_id = filenameChunkIdMap[frame.filename];
621
620
  }
622
621
  });
@@ -645,6 +644,12 @@ function clampToRange(value, min, max, logger, fallbackValue) {
645
644
  }
646
645
 
647
646
  class BucketedRateLimiter {
647
+ stop() {
648
+ if (this._removeInterval) {
649
+ clearInterval(this._removeInterval);
650
+ this._removeInterval = void 0;
651
+ }
652
+ }
648
653
  constructor(_options){
649
654
  this._options = _options;
650
655
  this._buckets = {};
@@ -676,7 +681,7 @@ class BucketedRateLimiter {
676
681
  this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
677
682
  this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
678
683
  this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
679
- setInterval(()=>{
684
+ this._removeInterval = setInterval(()=>{
680
685
  this._refillBuckets();
681
686
  }, this._refillInterval);
682
687
  }
@@ -690,6 +695,22 @@ function safeSetTimeout(fn, timeout) {
690
695
 
691
696
  const SHUTDOWN_TIMEOUT = 2000;
692
697
  class ErrorTracking {
698
+ constructor(client, options, _logger) {
699
+ this.client = client;
700
+ this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
701
+ this._logger = _logger;
702
+ // by default captures ten exceptions before rate limiting by exception type
703
+ // refills at a rate of one token / 10 second period
704
+ // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
705
+ this._rateLimiter = new BucketedRateLimiter({
706
+ refillRate: 1,
707
+ bucketSize: 10,
708
+ refillInterval: 10000,
709
+ // ten seconds in milliseconds
710
+ _logger: this._logger
711
+ });
712
+ this.startAutocaptureIfEnabled();
713
+ }
693
714
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
694
715
  const properties = {
695
716
  ...additionalProperties
@@ -709,31 +730,16 @@ class ErrorTracking {
709
730
  }
710
731
  };
711
732
  }
712
- constructor(client, options, _logger) {
713
- this.client = client;
714
- this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
715
- this._logger = _logger;
716
- // by default captures ten exceptions before rate limiting by exception type
717
- // refills at a rate of one token / 10 second period
718
- // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
719
- this._rateLimiter = new BucketedRateLimiter({
720
- refillRate: 1,
721
- bucketSize: 10,
722
- refillInterval: 10000,
723
- // ten seconds in milliseconds
724
- _logger: this._logger
725
- });
726
- this.startAutocaptureIfEnabled();
727
- }
728
733
  startAutocaptureIfEnabled() {
729
734
  if (this.isEnabled()) {
730
735
  addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this));
731
736
  addUnhandledRejectionListener(this.onException.bind(this));
732
737
  }
733
738
  }
734
- onException(exception, hint) {
735
- return ErrorTracking.buildEventMessage(exception, hint).then(msg => {
736
- const exceptionProperties = msg.properties;
739
+ async onException(exception, hint) {
740
+ this.client.addPendingPromise((async () => {
741
+ const eventMessage = await ErrorTracking.buildEventMessage(exception, hint);
742
+ const exceptionProperties = eventMessage.properties;
737
743
  const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
738
744
  const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
739
745
  if (isRateLimited) {
@@ -742,15 +748,20 @@ class ErrorTracking {
742
748
  });
743
749
  return;
744
750
  }
745
- this.client.capture(msg);
746
- });
751
+ return this.client.capture(eventMessage);
752
+ })());
747
753
  }
748
- async onFatalError() {
754
+ async onFatalError(exception) {
755
+ console.error(exception);
749
756
  await this.client.shutdown(SHUTDOWN_TIMEOUT);
757
+ process.exit(1);
750
758
  }
751
759
  isEnabled() {
752
760
  return !this.client.isDisabled && this._exceptionAutocaptureEnabled;
753
761
  }
762
+ shutdown() {
763
+ this._rateLimiter.stop();
764
+ }
754
765
  }
755
766
 
756
767
  function setupExpressErrorHandler(_posthog, app) {
@@ -1173,7 +1184,7 @@ function snipLine(line, colno) {
1173
1184
  return newLine;
1174
1185
  }
1175
1186
 
1176
- var version = "5.8.0";
1187
+ var version = "5.8.1";
1177
1188
 
1178
1189
  /**
1179
1190
  * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
@@ -2138,146 +2149,43 @@ class PostHogBackendClient extends PostHogCoreStateless {
2138
2149
  if (typeof props === 'string') {
2139
2150
  this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
2140
2151
  }
2141
- const {
2142
- distinctId,
2143
- event,
2144
- properties,
2145
- groups,
2146
- sendFeatureFlags,
2147
- timestamp,
2148
- disableGeoip,
2149
- uuid
2150
- } = props;
2151
- // Run before_send if configured
2152
- const eventMessage = this._runBeforeSend({
2152
+ this.addPendingPromise(this.prepareEventMessage(props).then(({
2153
2153
  distinctId,
2154
2154
  event,
2155
2155
  properties,
2156
- groups,
2157
- sendFeatureFlags,
2158
- timestamp,
2159
- disableGeoip,
2160
- uuid
2161
- });
2162
- if (!eventMessage) {
2163
- return;
2164
- }
2165
- const _capture = props => {
2166
- super.captureStateless(eventMessage.distinctId, eventMessage.event, props, {
2167
- timestamp: eventMessage.timestamp,
2168
- disableGeoip: eventMessage.disableGeoip,
2169
- uuid: eventMessage.uuid
2156
+ options
2157
+ }) => {
2158
+ return super.captureStateless(distinctId, event, properties, {
2159
+ timestamp: options.timestamp,
2160
+ disableGeoip: options.disableGeoip,
2161
+ uuid: options.uuid
2170
2162
  });
2171
- };
2172
- // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
2173
- const capturePromise = Promise.resolve().then(async () => {
2174
- if (sendFeatureFlags) {
2175
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
2176
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
2177
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
2178
- }
2179
- if (event === '$feature_flag_called') {
2180
- // 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.
2181
- return {};
2182
- }
2183
- return {};
2184
- }).then(flags => {
2185
- // Derive the relevant flag properties to add
2186
- const additionalProperties = {};
2187
- if (flags) {
2188
- for (const [feature, variant] of Object.entries(flags)) {
2189
- additionalProperties[`$feature/${feature}`] = variant;
2190
- }
2163
+ }).catch(err => {
2164
+ if (err) {
2165
+ console.error(err);
2191
2166
  }
2192
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
2193
- if (activeFlags.length > 0) {
2194
- additionalProperties['$active_feature_flags'] = activeFlags;
2195
- }
2196
- return additionalProperties;
2197
- }).catch(() => {
2198
- // Something went wrong getting the flag info - we should capture the event anyways
2199
- return {};
2200
- }).then(additionalProperties => {
2201
- // No matter what - capture the event
2202
- _capture({
2203
- ...additionalProperties,
2204
- ...(eventMessage.properties || {}),
2205
- $groups: eventMessage.groups || groups
2206
- });
2207
- });
2208
- this.addPendingPromise(capturePromise);
2167
+ }));
2209
2168
  }
2210
2169
  async captureImmediate(props) {
2211
2170
  if (typeof props === 'string') {
2212
- this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
2171
+ this.logMsgIfDebug(() => console.warn('Called captureImmediate() with a string as the first argument when an object was expected.'));
2213
2172
  }
2214
- const {
2173
+ return this.addPendingPromise(this.prepareEventMessage(props).then(({
2215
2174
  distinctId,
2216
2175
  event,
2217
2176
  properties,
2218
- groups,
2219
- sendFeatureFlags,
2220
- timestamp,
2221
- disableGeoip,
2222
- uuid
2223
- } = props;
2224
- // Run before_send if configured
2225
- const eventMessage = this._runBeforeSend({
2226
- distinctId,
2227
- event,
2228
- properties,
2229
- groups,
2230
- sendFeatureFlags,
2231
- timestamp,
2232
- disableGeoip,
2233
- uuid
2234
- });
2235
- if (!eventMessage) {
2236
- return;
2237
- }
2238
- const _capture = props => {
2239
- return super.captureStatelessImmediate(eventMessage.distinctId, eventMessage.event, props, {
2240
- timestamp: eventMessage.timestamp,
2241
- disableGeoip: eventMessage.disableGeoip,
2242
- uuid: eventMessage.uuid
2177
+ options
2178
+ }) => {
2179
+ return super.captureStatelessImmediate(distinctId, event, properties, {
2180
+ timestamp: options.timestamp,
2181
+ disableGeoip: options.disableGeoip,
2182
+ uuid: options.uuid
2243
2183
  });
2244
- };
2245
- const capturePromise = Promise.resolve().then(async () => {
2246
- if (sendFeatureFlags) {
2247
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
2248
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
2249
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
2250
- }
2251
- if (event === '$feature_flag_called') {
2252
- // 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.
2253
- return {};
2254
- }
2255
- return {};
2256
- }).then(flags => {
2257
- // Derive the relevant flag properties to add
2258
- const additionalProperties = {};
2259
- if (flags) {
2260
- for (const [feature, variant] of Object.entries(flags)) {
2261
- additionalProperties[`$feature/${feature}`] = variant;
2262
- }
2184
+ }).catch(err => {
2185
+ if (err) {
2186
+ console.error(err);
2263
2187
  }
2264
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
2265
- if (activeFlags.length > 0) {
2266
- additionalProperties['$active_feature_flags'] = activeFlags;
2267
- }
2268
- return additionalProperties;
2269
- }).catch(() => {
2270
- // Something went wrong getting the flag info - we should capture the event anyways
2271
- return {};
2272
- }).then(additionalProperties => {
2273
- // No matter what - capture the event
2274
- _capture({
2275
- ...additionalProperties,
2276
- ...(eventMessage.properties || {}),
2277
- $groups: eventMessage.groups || groups
2278
- });
2279
- });
2280
- await capturePromise;
2188
+ }));
2281
2189
  }
2282
2190
  identify({
2283
2191
  distinctId,
@@ -2547,6 +2455,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
2547
2455
  }
2548
2456
  async _shutdown(shutdownTimeoutMs) {
2549
2457
  this.featureFlagsPoller?.stopPoller();
2458
+ this.errorTracking.shutdown();
2550
2459
  return super._shutdown(shutdownTimeoutMs);
2551
2460
  }
2552
2461
  async _requestRemoteConfigPayload(flagKey) {
@@ -2565,7 +2474,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
2565
2474
  let abortTimeout = null;
2566
2475
  if (this.options.requestTimeout && typeof this.options.requestTimeout === 'number') {
2567
2476
  const controller = new AbortController();
2568
- abortTimeout = safeSetTimeout(() => {
2477
+ abortTimeout = safeSetTimeout$1(() => {
2569
2478
  controller.abort();
2570
2479
  }, this.options.requestTimeout);
2571
2480
  options.signal = controller.signal;
@@ -2674,18 +2583,88 @@ class PostHogBackendClient extends PostHogCoreStateless {
2674
2583
  }
2675
2584
  captureException(error, distinctId, additionalProperties) {
2676
2585
  const syntheticException = new Error('PostHog syntheticException');
2677
- ErrorTracking.buildEventMessage(error, {
2586
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2678
2587
  syntheticException
2679
- }, distinctId, additionalProperties).then(msg => {
2680
- this.capture(msg);
2681
- });
2588
+ }, distinctId, additionalProperties).then(msg => this.capture(msg)));
2682
2589
  }
2683
2590
  async captureExceptionImmediate(error, distinctId, additionalProperties) {
2684
2591
  const syntheticException = new Error('PostHog syntheticException');
2685
- const evtMsg = await ErrorTracking.buildEventMessage(error, {
2592
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2686
2593
  syntheticException
2687
- }, distinctId, additionalProperties);
2688
- return await this.captureImmediate(evtMsg);
2594
+ }, distinctId, additionalProperties).then(msg => this.captureImmediate(msg)));
2595
+ }
2596
+ async prepareEventMessage(props) {
2597
+ const {
2598
+ distinctId,
2599
+ event,
2600
+ properties,
2601
+ groups,
2602
+ sendFeatureFlags,
2603
+ timestamp,
2604
+ disableGeoip,
2605
+ uuid
2606
+ } = props;
2607
+ // Run before_send if configured
2608
+ const eventMessage = this._runBeforeSend({
2609
+ distinctId,
2610
+ event,
2611
+ properties,
2612
+ groups,
2613
+ sendFeatureFlags,
2614
+ timestamp,
2615
+ disableGeoip,
2616
+ uuid
2617
+ });
2618
+ if (!eventMessage) {
2619
+ return Promise.reject(null);
2620
+ }
2621
+ // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
2622
+ const eventProperties = await Promise.resolve().then(async () => {
2623
+ if (sendFeatureFlags) {
2624
+ // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
2625
+ const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
2626
+ return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
2627
+ }
2628
+ if (event === '$feature_flag_called') {
2629
+ // 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.
2630
+ return {};
2631
+ }
2632
+ return {};
2633
+ }).then(flags => {
2634
+ // Derive the relevant flag properties to add
2635
+ const additionalProperties = {};
2636
+ if (flags) {
2637
+ for (const [feature, variant] of Object.entries(flags)) {
2638
+ additionalProperties[`$feature/${feature}`] = variant;
2639
+ }
2640
+ }
2641
+ const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
2642
+ if (activeFlags.length > 0) {
2643
+ additionalProperties['$active_feature_flags'] = activeFlags;
2644
+ }
2645
+ return additionalProperties;
2646
+ }).catch(() => {
2647
+ // Something went wrong getting the flag info - we should capture the event anyways
2648
+ return {};
2649
+ }).then(additionalProperties => {
2650
+ // No matter what - capture the event
2651
+ const props = {
2652
+ ...additionalProperties,
2653
+ ...(eventMessage.properties || {}),
2654
+ $groups: eventMessage.groups || groups
2655
+ };
2656
+ return props;
2657
+ });
2658
+ return {
2659
+ distinctId: eventMessage.distinctId,
2660
+ event: eventMessage.event,
2661
+ properties: eventProperties,
2662
+ options: {
2663
+ timestamp: eventMessage.timestamp,
2664
+ disableGeoip: eventMessage.disableGeoip,
2665
+ uuid: eventMessage.uuid
2666
+ }
2667
+ };
2689
2668
  }
2690
2669
  _runBeforeSend(eventMessage) {
2691
2670
  const beforeSend = this.options.before_send;