posthog-node 5.7.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 { safeSetTimeout, 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,15 +612,102 @@ 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
  });
620
619
  return frames;
621
620
  }
622
621
 
622
+ const ObjProto = Object.prototype;
623
+ const type_utils_toString = ObjProto.toString;
624
+ const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
625
+
626
+ function clampToRange(value, min, max, logger, fallbackValue) {
627
+ if (min > max) {
628
+ logger.warn('min cannot be greater than max.');
629
+ min = max;
630
+ }
631
+ if (isNumber(value)) if (value > max) {
632
+ logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
633
+ return max;
634
+ } else {
635
+ if (!(value < min)) return value;
636
+ logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
637
+ return min;
638
+ }
639
+ logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
640
+ return clampToRange(max, min, max, logger);
641
+ }
642
+
643
+ class BucketedRateLimiter {
644
+ stop() {
645
+ if (this._removeInterval) {
646
+ clearInterval(this._removeInterval);
647
+ this._removeInterval = void 0;
648
+ }
649
+ }
650
+ constructor(_options){
651
+ this._options = _options;
652
+ this._buckets = {};
653
+ this._refillBuckets = ()=>{
654
+ Object.keys(this._buckets).forEach((key)=>{
655
+ const newTokens = this._getBucket(key) + this._refillRate;
656
+ if (newTokens >= this._bucketSize) delete this._buckets[key];
657
+ else this._setBucket(key, newTokens);
658
+ });
659
+ };
660
+ this._getBucket = (key)=>this._buckets[String(key)];
661
+ this._setBucket = (key, value)=>{
662
+ this._buckets[String(key)] = value;
663
+ };
664
+ this.consumeRateLimit = (key)=>{
665
+ var _this__getBucket;
666
+ let tokens = null != (_this__getBucket = this._getBucket(key)) ? _this__getBucket : this._bucketSize;
667
+ tokens = Math.max(tokens - 1, 0);
668
+ if (0 === tokens) return true;
669
+ this._setBucket(key, tokens);
670
+ const hasReachedZero = 0 === tokens;
671
+ if (hasReachedZero) {
672
+ var _this__onBucketRateLimited, _this;
673
+ null == (_this__onBucketRateLimited = (_this = this)._onBucketRateLimited) || _this__onBucketRateLimited.call(_this, key);
674
+ }
675
+ return hasReachedZero;
676
+ };
677
+ this._onBucketRateLimited = this._options._onBucketRateLimited;
678
+ this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
679
+ this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
680
+ this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
681
+ this._removeInterval = setInterval(()=>{
682
+ this._refillBuckets();
683
+ }, this._refillInterval);
684
+ }
685
+ }
686
+
687
+ function safeSetTimeout(fn, timeout) {
688
+ const t = setTimeout(fn, timeout);
689
+ (null == t ? void 0 : t.unref) && (null == t || t.unref());
690
+ return t;
691
+ }
692
+
623
693
  const SHUTDOWN_TIMEOUT = 2000;
624
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
+ }
625
711
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
626
712
  const properties = {
627
713
  ...additionalProperties
@@ -641,28 +727,38 @@ class ErrorTracking {
641
727
  }
642
728
  };
643
729
  }
644
- constructor(client, options) {
645
- this.client = client;
646
- this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
647
- this.startAutocaptureIfEnabled();
648
- }
649
730
  startAutocaptureIfEnabled() {
650
731
  if (this.isEnabled()) {
651
732
  addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this));
652
733
  addUnhandledRejectionListener(this.onException.bind(this));
653
734
  }
654
735
  }
655
- onException(exception, hint) {
656
- void ErrorTracking.buildEventMessage(exception, hint).then(msg => {
657
- this.client.capture(msg);
658
- });
736
+ async onException(exception, hint) {
737
+ this.client.addPendingPromise((async () => {
738
+ const eventMessage = await ErrorTracking.buildEventMessage(exception, hint);
739
+ const exceptionProperties = eventMessage.properties;
740
+ const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
741
+ const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
742
+ if (isRateLimited) {
743
+ this._logger.info('Skipping exception capture because of client rate limiting.', {
744
+ exception: exceptionType
745
+ });
746
+ return;
747
+ }
748
+ return this.client.capture(eventMessage);
749
+ })());
659
750
  }
660
- async onFatalError() {
751
+ async onFatalError(exception) {
752
+ console.error(exception);
661
753
  await this.client.shutdown(SHUTDOWN_TIMEOUT);
754
+ process.exit(1);
662
755
  }
663
756
  isEnabled() {
664
757
  return !this.client.isDisabled && this._exceptionAutocaptureEnabled;
665
758
  }
759
+ shutdown() {
760
+ this._rateLimiter.stop();
761
+ }
666
762
  }
667
763
 
668
764
  function setupExpressErrorHandler(_posthog, app) {
@@ -682,7 +778,7 @@ function setupExpressErrorHandler(_posthog, app) {
682
778
  });
683
779
  }
684
780
 
685
- var version = "5.7.0";
781
+ var version = "5.8.1";
686
782
 
687
783
  /**
688
784
  * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
@@ -970,7 +1066,70 @@ class FeatureFlagsPoller {
970
1066
  }
971
1067
  return null;
972
1068
  }
973
- async matchFeatureFlagProperties(flag, distinctId, properties) {
1069
+ async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
1070
+ const targetFlagKey = property.key;
1071
+ if (!this.featureFlagsByKey) {
1072
+ throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
1073
+ }
1074
+ // Check if dependency_chain is present - it should always be provided for flag dependencies
1075
+ if (!('dependency_chain' in property)) {
1076
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
1077
+ }
1078
+ const dependencyChain = property.dependency_chain;
1079
+ // Check for missing or invalid dependency chain (This should never happen, but being defensive)
1080
+ if (!Array.isArray(dependencyChain)) {
1081
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
1082
+ }
1083
+ // Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
1084
+ if (dependencyChain.length === 0) {
1085
+ throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
1086
+ }
1087
+ // Evaluate all dependencies in the chain order
1088
+ for (const depFlagKey of dependencyChain) {
1089
+ if (!(depFlagKey in evaluationCache)) {
1090
+ // Need to evaluate this dependency first
1091
+ const depFlag = this.featureFlagsByKey[depFlagKey];
1092
+ if (!depFlag) {
1093
+ // Missing flag dependency - cannot evaluate locally
1094
+ throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
1095
+ } else if (!depFlag.active) {
1096
+ // Inactive flag evaluates to false
1097
+ evaluationCache[depFlagKey] = false;
1098
+ } else {
1099
+ // Recursively evaluate the dependency
1100
+ try {
1101
+ const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
1102
+ evaluationCache[depFlagKey] = depResult;
1103
+ } catch (error) {
1104
+ // If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
1105
+ throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
1106
+ }
1107
+ }
1108
+ }
1109
+ // Check if dependency evaluation was inconclusive
1110
+ const cachedResult = evaluationCache[depFlagKey];
1111
+ if (cachedResult === null || cachedResult === undefined) {
1112
+ throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
1113
+ }
1114
+ }
1115
+ // The target flag is specified in property.key (This should match the last element in the dependency chain)
1116
+ const targetFlagValue = evaluationCache[targetFlagKey];
1117
+ return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
1118
+ }
1119
+ flagEvaluatesToExpectedValue(expectedValue, flagValue) {
1120
+ // If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
1121
+ // If the expected value is false, then only return true if the flag evaluated to false.
1122
+ if (typeof expectedValue === 'boolean') {
1123
+ return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
1124
+ }
1125
+ // If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
1126
+ if (typeof expectedValue === 'string') {
1127
+ return flagValue === expectedValue;
1128
+ }
1129
+ // The `flag_evaluates_to` operator is not supported for numbers and arrays.
1130
+ return false;
1131
+ }
1132
+ async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
974
1133
  const flagFilters = flag.filters || {};
975
1134
  const flagConditions = flagFilters.groups || [];
976
1135
  let isInconclusive = false;
@@ -992,7 +1151,7 @@ class FeatureFlagsPoller {
992
1151
  });
993
1152
  for (const condition of sortedFlagConditions) {
994
1153
  try {
995
- if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
1154
+ if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
996
1155
  const variantOverride = condition.variant;
997
1156
  const flagVariants = flagFilters.multivariate?.variants || [];
998
1157
  if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
@@ -1018,7 +1177,7 @@ class FeatureFlagsPoller {
1018
1177
  // We can only return False when all conditions are False
1019
1178
  return false;
1020
1179
  }
1021
- async isConditionMatch(flag, distinctId, condition, properties) {
1180
+ async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
1022
1181
  const rolloutPercentage = condition.rollout_percentage;
1023
1182
  const warnFunction = msg => {
1024
1183
  this.logMsgIfDebug(() => console.warn(msg));
@@ -1030,8 +1189,7 @@ class FeatureFlagsPoller {
1030
1189
  if (propertyType === 'cohort') {
1031
1190
  matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
1032
1191
  } else if (propertyType === 'flag') {
1033
- this.logMsgIfDebug(() => console.warn(`[FEATURE FLAGS] Flag dependency filters are not supported in local evaluation. ` + `Skipping condition for flag '${flag.key}' with dependency on flag '${prop.key || 'unknown'}'`));
1034
- continue;
1192
+ matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
1035
1193
  } else {
1036
1194
  matches = matchProperty(prop, properties, warnFunction);
1037
1195
  }
@@ -1296,6 +1454,10 @@ function matchProperty(property, propertyValues, warnFunction) {
1296
1454
  case 'is_date_after':
1297
1455
  case 'is_date_before':
1298
1456
  {
1457
+ // Boolean values should never be used with date operations
1458
+ if (typeof value === 'boolean') {
1459
+ throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
1460
+ }
1299
1461
  let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
1300
1462
  if (parsedDate == null) {
1301
1463
  parsedDate = convertToDateTime(value);
@@ -1479,6 +1641,37 @@ class PostHogMemoryStorage {
1479
1641
  }
1480
1642
  }
1481
1643
 
1644
+ const _createLogger = (prefix, logMsgIfDebug) => {
1645
+ const logger = {
1646
+ _log: (level, ...args) => {
1647
+ logMsgIfDebug(() => {
1648
+ const consoleLog = console[level];
1649
+ consoleLog(prefix, ...args);
1650
+ });
1651
+ },
1652
+ info: (...args) => {
1653
+ logger._log('log', ...args);
1654
+ },
1655
+ warn: (...args) => {
1656
+ logger._log('warn', ...args);
1657
+ },
1658
+ error: (...args) => {
1659
+ logger._log('error', ...args);
1660
+ },
1661
+ critical: (...args) => {
1662
+ // Critical errors are always logged to the console
1663
+ // eslint-disable-next-line no-console
1664
+ console.error(prefix, ...args);
1665
+ },
1666
+ uninitializedWarning: methodName => {
1667
+ logger.error(`You must initialize PostHog before calling ${methodName}`);
1668
+ },
1669
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
1670
+ };
1671
+ return logger;
1672
+ };
1673
+ const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
1674
+
1482
1675
  // Standard local evaluation rate limit is 600 per minute (10 per second),
1483
1676
  // so the fastest a poller should ever be set is 100ms.
1484
1677
  const MINIMUM_POLLING_INTERVAL = 100;
@@ -1490,6 +1683,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
1490
1683
  super(apiKey, options);
1491
1684
  this._memoryStorage = new PostHogMemoryStorage();
1492
1685
  this.options = options;
1686
+ this.logger = createLogger(this.logMsgIfDebug.bind(this));
1493
1687
  this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
1494
1688
  if (options.personalApiKey) {
1495
1689
  if (options.personalApiKey.includes('phc_')) {
@@ -1516,7 +1710,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
1516
1710
  });
1517
1711
  }
1518
1712
  }
1519
- this.errorTracking = new ErrorTracking(this, options);
1713
+ this.errorTracking = new ErrorTracking(this, options, this.logger);
1520
1714
  this.distinctIdHasSentFlagCalls = {};
1521
1715
  this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
1522
1716
  }
@@ -1549,146 +1743,43 @@ class PostHogBackendClient extends PostHogCoreStateless {
1549
1743
  if (typeof props === 'string') {
1550
1744
  this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
1551
1745
  }
1552
- const {
1746
+ this.addPendingPromise(this.prepareEventMessage(props).then(({
1553
1747
  distinctId,
1554
1748
  event,
1555
1749
  properties,
1556
- groups,
1557
- sendFeatureFlags,
1558
- timestamp,
1559
- disableGeoip,
1560
- uuid
1561
- } = props;
1562
- // Run before_send if configured
1563
- const eventMessage = this._runBeforeSend({
1564
- distinctId,
1565
- event,
1566
- properties,
1567
- groups,
1568
- sendFeatureFlags,
1569
- timestamp,
1570
- disableGeoip,
1571
- uuid
1572
- });
1573
- if (!eventMessage) {
1574
- return;
1575
- }
1576
- const _capture = props => {
1577
- super.captureStateless(eventMessage.distinctId, eventMessage.event, props, {
1578
- timestamp: eventMessage.timestamp,
1579
- disableGeoip: eventMessage.disableGeoip,
1580
- 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
1581
1756
  });
1582
- };
1583
- // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
1584
- const capturePromise = Promise.resolve().then(async () => {
1585
- if (sendFeatureFlags) {
1586
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
1587
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
1588
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
1589
- }
1590
- if (event === '$feature_flag_called') {
1591
- // 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.
1592
- return {};
1593
- }
1594
- return {};
1595
- }).then(flags => {
1596
- // Derive the relevant flag properties to add
1597
- const additionalProperties = {};
1598
- if (flags) {
1599
- for (const [feature, variant] of Object.entries(flags)) {
1600
- additionalProperties[`$feature/${feature}`] = variant;
1601
- }
1602
- }
1603
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
1604
- if (activeFlags.length > 0) {
1605
- additionalProperties['$active_feature_flags'] = activeFlags;
1757
+ }).catch(err => {
1758
+ if (err) {
1759
+ console.error(err);
1606
1760
  }
1607
- return additionalProperties;
1608
- }).catch(() => {
1609
- // Something went wrong getting the flag info - we should capture the event anyways
1610
- return {};
1611
- }).then(additionalProperties => {
1612
- // No matter what - capture the event
1613
- _capture({
1614
- ...additionalProperties,
1615
- ...(eventMessage.properties || {}),
1616
- $groups: eventMessage.groups || groups
1617
- });
1618
- });
1619
- this.addPendingPromise(capturePromise);
1761
+ }));
1620
1762
  }
1621
1763
  async captureImmediate(props) {
1622
1764
  if (typeof props === 'string') {
1623
- 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.'));
1624
1766
  }
1625
- const {
1767
+ return this.addPendingPromise(this.prepareEventMessage(props).then(({
1626
1768
  distinctId,
1627
1769
  event,
1628
1770
  properties,
1629
- groups,
1630
- sendFeatureFlags,
1631
- timestamp,
1632
- disableGeoip,
1633
- uuid
1634
- } = props;
1635
- // Run before_send if configured
1636
- const eventMessage = this._runBeforeSend({
1637
- distinctId,
1638
- event,
1639
- properties,
1640
- groups,
1641
- sendFeatureFlags,
1642
- timestamp,
1643
- disableGeoip,
1644
- uuid
1645
- });
1646
- if (!eventMessage) {
1647
- return;
1648
- }
1649
- const _capture = props => {
1650
- return super.captureStatelessImmediate(eventMessage.distinctId, eventMessage.event, props, {
1651
- timestamp: eventMessage.timestamp,
1652
- disableGeoip: eventMessage.disableGeoip,
1653
- 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
1654
1777
  });
1655
- };
1656
- const capturePromise = Promise.resolve().then(async () => {
1657
- if (sendFeatureFlags) {
1658
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
1659
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
1660
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
1661
- }
1662
- if (event === '$feature_flag_called') {
1663
- // 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.
1664
- return {};
1665
- }
1666
- return {};
1667
- }).then(flags => {
1668
- // Derive the relevant flag properties to add
1669
- const additionalProperties = {};
1670
- if (flags) {
1671
- for (const [feature, variant] of Object.entries(flags)) {
1672
- additionalProperties[`$feature/${feature}`] = variant;
1673
- }
1778
+ }).catch(err => {
1779
+ if (err) {
1780
+ console.error(err);
1674
1781
  }
1675
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
1676
- if (activeFlags.length > 0) {
1677
- additionalProperties['$active_feature_flags'] = activeFlags;
1678
- }
1679
- return additionalProperties;
1680
- }).catch(() => {
1681
- // Something went wrong getting the flag info - we should capture the event anyways
1682
- return {};
1683
- }).then(additionalProperties => {
1684
- // No matter what - capture the event
1685
- _capture({
1686
- ...additionalProperties,
1687
- ...(eventMessage.properties || {}),
1688
- $groups: eventMessage.groups || groups
1689
- });
1690
- });
1691
- await capturePromise;
1782
+ }));
1692
1783
  }
1693
1784
  identify({
1694
1785
  distinctId,
@@ -1958,6 +2049,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
1958
2049
  }
1959
2050
  async _shutdown(shutdownTimeoutMs) {
1960
2051
  this.featureFlagsPoller?.stopPoller();
2052
+ this.errorTracking.shutdown();
1961
2053
  return super._shutdown(shutdownTimeoutMs);
1962
2054
  }
1963
2055
  async _requestRemoteConfigPayload(flagKey) {
@@ -1976,7 +2068,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
1976
2068
  let abortTimeout = null;
1977
2069
  if (this.options.requestTimeout && typeof this.options.requestTimeout === 'number') {
1978
2070
  const controller = new AbortController();
1979
- abortTimeout = safeSetTimeout(() => {
2071
+ abortTimeout = safeSetTimeout$1(() => {
1980
2072
  controller.abort();
1981
2073
  }, this.options.requestTimeout);
1982
2074
  options.signal = controller.signal;
@@ -2085,18 +2177,88 @@ class PostHogBackendClient extends PostHogCoreStateless {
2085
2177
  }
2086
2178
  captureException(error, distinctId, additionalProperties) {
2087
2179
  const syntheticException = new Error('PostHog syntheticException');
2088
- ErrorTracking.buildEventMessage(error, {
2180
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2089
2181
  syntheticException
2090
- }, distinctId, additionalProperties).then(msg => {
2091
- this.capture(msg);
2092
- });
2182
+ }, distinctId, additionalProperties).then(msg => this.capture(msg)));
2093
2183
  }
2094
2184
  async captureExceptionImmediate(error, distinctId, additionalProperties) {
2095
2185
  const syntheticException = new Error('PostHog syntheticException');
2096
- const evtMsg = await ErrorTracking.buildEventMessage(error, {
2186
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2097
2187
  syntheticException
2098
- }, distinctId, additionalProperties);
2099
- 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
+ };
2100
2262
  }
2101
2263
  _runBeforeSend(eventMessage) {
2102
2264
  const beforeSend = this.options.before_send;