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.
@@ -315,7 +315,7 @@ function makeUncaughtExceptionHandler(captureFn, onFatalFn) {
315
315
  });
316
316
  if (!calledFatalError && processWouldExit) {
317
317
  calledFatalError = true;
318
- onFatalFn();
318
+ onFatalFn(error);
319
319
  }
320
320
  }, {
321
321
  _posthogErrorHandler: true
@@ -326,7 +326,7 @@ function addUncaughtExceptionListener(captureFn, onFatalFn) {
326
326
  }
327
327
  function addUnhandledRejectionListener(captureFn) {
328
328
  global.process.on('unhandledRejection', reason => {
329
- captureFn(reason, {
329
+ return captureFn(reason, {
330
330
  mechanism: {
331
331
  type: 'onunhandledrejection',
332
332
  handled: false
@@ -343,8 +343,7 @@ let cachedFilenameChunkIds;
343
343
  function getFilenameToChunkIdMap(stackParser) {
344
344
  const chunkIdMap = globalThis._posthogChunkIds;
345
345
  if (!chunkIdMap) {
346
- console.error('No chunk id map found');
347
- return {};
346
+ return null;
348
347
  }
349
348
  const chunkIdKeys = Object.keys(chunkIdMap);
350
349
  if (cachedFilenameChunkIds && chunkIdKeys.length === lastKeysCount) {
@@ -615,15 +614,102 @@ function parseStackFrames(stackParser, error) {
615
614
  function applyChunkIds(frames, parser) {
616
615
  const filenameChunkIdMap = getFilenameToChunkIdMap(parser);
617
616
  frames.forEach(frame => {
618
- if (frame.filename) {
617
+ if (frame.filename && filenameChunkIdMap) {
619
618
  frame.chunk_id = filenameChunkIdMap[frame.filename];
620
619
  }
621
620
  });
622
621
  return frames;
623
622
  }
624
623
 
624
+ const ObjProto = Object.prototype;
625
+ const type_utils_toString = ObjProto.toString;
626
+ const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
627
+
628
+ function clampToRange(value, min, max, logger, fallbackValue) {
629
+ if (min > max) {
630
+ logger.warn('min cannot be greater than max.');
631
+ min = max;
632
+ }
633
+ if (isNumber(value)) if (value > max) {
634
+ logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
635
+ return max;
636
+ } else {
637
+ if (!(value < min)) return value;
638
+ logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
639
+ return min;
640
+ }
641
+ logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
642
+ return clampToRange(max, min, max, logger);
643
+ }
644
+
645
+ class BucketedRateLimiter {
646
+ stop() {
647
+ if (this._removeInterval) {
648
+ clearInterval(this._removeInterval);
649
+ this._removeInterval = void 0;
650
+ }
651
+ }
652
+ constructor(_options){
653
+ this._options = _options;
654
+ this._buckets = {};
655
+ this._refillBuckets = ()=>{
656
+ Object.keys(this._buckets).forEach((key)=>{
657
+ const newTokens = this._getBucket(key) + this._refillRate;
658
+ if (newTokens >= this._bucketSize) delete this._buckets[key];
659
+ else this._setBucket(key, newTokens);
660
+ });
661
+ };
662
+ this._getBucket = (key)=>this._buckets[String(key)];
663
+ this._setBucket = (key, value)=>{
664
+ this._buckets[String(key)] = value;
665
+ };
666
+ this.consumeRateLimit = (key)=>{
667
+ var _this__getBucket;
668
+ let tokens = null != (_this__getBucket = this._getBucket(key)) ? _this__getBucket : this._bucketSize;
669
+ tokens = Math.max(tokens - 1, 0);
670
+ if (0 === tokens) return true;
671
+ this._setBucket(key, tokens);
672
+ const hasReachedZero = 0 === tokens;
673
+ if (hasReachedZero) {
674
+ var _this__onBucketRateLimited, _this;
675
+ null == (_this__onBucketRateLimited = (_this = this)._onBucketRateLimited) || _this__onBucketRateLimited.call(_this, key);
676
+ }
677
+ return hasReachedZero;
678
+ };
679
+ this._onBucketRateLimited = this._options._onBucketRateLimited;
680
+ this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
681
+ this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
682
+ this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
683
+ this._removeInterval = setInterval(()=>{
684
+ this._refillBuckets();
685
+ }, this._refillInterval);
686
+ }
687
+ }
688
+
689
+ function safeSetTimeout(fn, timeout) {
690
+ const t = setTimeout(fn, timeout);
691
+ (null == t ? void 0 : t.unref) && (null == t || t.unref());
692
+ return t;
693
+ }
694
+
625
695
  const SHUTDOWN_TIMEOUT = 2000;
626
696
  class ErrorTracking {
697
+ constructor(client, options, _logger) {
698
+ this.client = client;
699
+ this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
700
+ this._logger = _logger;
701
+ // by default captures ten exceptions before rate limiting by exception type
702
+ // refills at a rate of one token / 10 second period
703
+ // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
704
+ this._rateLimiter = new BucketedRateLimiter({
705
+ refillRate: 1,
706
+ bucketSize: 10,
707
+ refillInterval: 10000,
708
+ // ten seconds in milliseconds
709
+ _logger: this._logger
710
+ });
711
+ this.startAutocaptureIfEnabled();
712
+ }
627
713
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
628
714
  const properties = {
629
715
  ...additionalProperties
@@ -643,28 +729,38 @@ class ErrorTracking {
643
729
  }
644
730
  };
645
731
  }
646
- constructor(client, options) {
647
- this.client = client;
648
- this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
649
- this.startAutocaptureIfEnabled();
650
- }
651
732
  startAutocaptureIfEnabled() {
652
733
  if (this.isEnabled()) {
653
734
  addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this));
654
735
  addUnhandledRejectionListener(this.onException.bind(this));
655
736
  }
656
737
  }
657
- onException(exception, hint) {
658
- void ErrorTracking.buildEventMessage(exception, hint).then(msg => {
659
- this.client.capture(msg);
660
- });
738
+ async onException(exception, hint) {
739
+ this.client.addPendingPromise((async () => {
740
+ const eventMessage = await ErrorTracking.buildEventMessage(exception, hint);
741
+ const exceptionProperties = eventMessage.properties;
742
+ const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
743
+ const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
744
+ if (isRateLimited) {
745
+ this._logger.info('Skipping exception capture because of client rate limiting.', {
746
+ exception: exceptionType
747
+ });
748
+ return;
749
+ }
750
+ return this.client.capture(eventMessage);
751
+ })());
661
752
  }
662
- async onFatalError() {
753
+ async onFatalError(exception) {
754
+ console.error(exception);
663
755
  await this.client.shutdown(SHUTDOWN_TIMEOUT);
756
+ process.exit(1);
664
757
  }
665
758
  isEnabled() {
666
759
  return !this.client.isDisabled && this._exceptionAutocaptureEnabled;
667
760
  }
761
+ shutdown() {
762
+ this._rateLimiter.stop();
763
+ }
668
764
  }
669
765
 
670
766
  function setupExpressErrorHandler(_posthog, app) {
@@ -684,7 +780,7 @@ function setupExpressErrorHandler(_posthog, app) {
684
780
  });
685
781
  }
686
782
 
687
- var version = "5.7.0";
783
+ var version = "5.8.1";
688
784
 
689
785
  /**
690
786
  * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
@@ -972,7 +1068,70 @@ class FeatureFlagsPoller {
972
1068
  }
973
1069
  return null;
974
1070
  }
975
- async matchFeatureFlagProperties(flag, distinctId, properties) {
1071
+ async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
1072
+ const targetFlagKey = property.key;
1073
+ if (!this.featureFlagsByKey) {
1074
+ throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
1075
+ }
1076
+ // Check if dependency_chain is present - it should always be provided for flag dependencies
1077
+ if (!('dependency_chain' in property)) {
1078
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
1079
+ }
1080
+ const dependencyChain = property.dependency_chain;
1081
+ // Check for missing or invalid dependency chain (This should never happen, but being defensive)
1082
+ if (!Array.isArray(dependencyChain)) {
1083
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
1084
+ }
1085
+ // Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
1086
+ if (dependencyChain.length === 0) {
1087
+ throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
1088
+ }
1089
+ // Evaluate all dependencies in the chain order
1090
+ for (const depFlagKey of dependencyChain) {
1091
+ if (!(depFlagKey in evaluationCache)) {
1092
+ // Need to evaluate this dependency first
1093
+ const depFlag = this.featureFlagsByKey[depFlagKey];
1094
+ if (!depFlag) {
1095
+ // Missing flag dependency - cannot evaluate locally
1096
+ throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
1097
+ } else if (!depFlag.active) {
1098
+ // Inactive flag evaluates to false
1099
+ evaluationCache[depFlagKey] = false;
1100
+ } else {
1101
+ // Recursively evaluate the dependency
1102
+ try {
1103
+ const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
1104
+ evaluationCache[depFlagKey] = depResult;
1105
+ } catch (error) {
1106
+ // If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
1107
+ throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
1108
+ }
1109
+ }
1110
+ }
1111
+ // Check if dependency evaluation was inconclusive
1112
+ const cachedResult = evaluationCache[depFlagKey];
1113
+ if (cachedResult === null || cachedResult === undefined) {
1114
+ throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
1115
+ }
1116
+ }
1117
+ // The target flag is specified in property.key (This should match the last element in the dependency chain)
1118
+ const targetFlagValue = evaluationCache[targetFlagKey];
1119
+ return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
1120
+ }
1121
+ flagEvaluatesToExpectedValue(expectedValue, flagValue) {
1122
+ // If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
1123
+ // If the expected value is false, then only return true if the flag evaluated to false.
1124
+ if (typeof expectedValue === 'boolean') {
1125
+ return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
1126
+ }
1127
+ // If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
1128
+ if (typeof expectedValue === 'string') {
1129
+ return flagValue === expectedValue;
1130
+ }
1131
+ // The `flag_evaluates_to` operator is not supported for numbers and arrays.
1132
+ return false;
1133
+ }
1134
+ async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
976
1135
  const flagFilters = flag.filters || {};
977
1136
  const flagConditions = flagFilters.groups || [];
978
1137
  let isInconclusive = false;
@@ -994,7 +1153,7 @@ class FeatureFlagsPoller {
994
1153
  });
995
1154
  for (const condition of sortedFlagConditions) {
996
1155
  try {
997
- if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
1156
+ if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
998
1157
  const variantOverride = condition.variant;
999
1158
  const flagVariants = flagFilters.multivariate?.variants || [];
1000
1159
  if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
@@ -1020,7 +1179,7 @@ class FeatureFlagsPoller {
1020
1179
  // We can only return False when all conditions are False
1021
1180
  return false;
1022
1181
  }
1023
- async isConditionMatch(flag, distinctId, condition, properties) {
1182
+ async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
1024
1183
  const rolloutPercentage = condition.rollout_percentage;
1025
1184
  const warnFunction = msg => {
1026
1185
  this.logMsgIfDebug(() => console.warn(msg));
@@ -1032,8 +1191,7 @@ class FeatureFlagsPoller {
1032
1191
  if (propertyType === 'cohort') {
1033
1192
  matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
1034
1193
  } else if (propertyType === 'flag') {
1035
- 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'}'`));
1036
- continue;
1194
+ matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
1037
1195
  } else {
1038
1196
  matches = matchProperty(prop, properties, warnFunction);
1039
1197
  }
@@ -1194,7 +1352,7 @@ class FeatureFlagsPoller {
1194
1352
  let abortTimeout = null;
1195
1353
  if (this.timeout && typeof this.timeout === 'number') {
1196
1354
  const controller = new AbortController();
1197
- abortTimeout = core.safeSetTimeout(() => {
1355
+ abortTimeout = safeSetTimeout(() => {
1198
1356
  controller.abort();
1199
1357
  }, this.timeout);
1200
1358
  options.signal = controller.signal;
@@ -1298,6 +1456,10 @@ function matchProperty(property, propertyValues, warnFunction) {
1298
1456
  case 'is_date_after':
1299
1457
  case 'is_date_before':
1300
1458
  {
1459
+ // Boolean values should never be used with date operations
1460
+ if (typeof value === 'boolean') {
1461
+ throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
1462
+ }
1301
1463
  let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
1302
1464
  if (parsedDate == null) {
1303
1465
  parsedDate = convertToDateTime(value);
@@ -1481,6 +1643,37 @@ class PostHogMemoryStorage {
1481
1643
  }
1482
1644
  }
1483
1645
 
1646
+ const _createLogger = (prefix, logMsgIfDebug) => {
1647
+ const logger = {
1648
+ _log: (level, ...args) => {
1649
+ logMsgIfDebug(() => {
1650
+ const consoleLog = console[level];
1651
+ consoleLog(prefix, ...args);
1652
+ });
1653
+ },
1654
+ info: (...args) => {
1655
+ logger._log('log', ...args);
1656
+ },
1657
+ warn: (...args) => {
1658
+ logger._log('warn', ...args);
1659
+ },
1660
+ error: (...args) => {
1661
+ logger._log('error', ...args);
1662
+ },
1663
+ critical: (...args) => {
1664
+ // Critical errors are always logged to the console
1665
+ // eslint-disable-next-line no-console
1666
+ console.error(prefix, ...args);
1667
+ },
1668
+ uninitializedWarning: methodName => {
1669
+ logger.error(`You must initialize PostHog before calling ${methodName}`);
1670
+ },
1671
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
1672
+ };
1673
+ return logger;
1674
+ };
1675
+ const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
1676
+
1484
1677
  // Standard local evaluation rate limit is 600 per minute (10 per second),
1485
1678
  // so the fastest a poller should ever be set is 100ms.
1486
1679
  const MINIMUM_POLLING_INTERVAL = 100;
@@ -1492,6 +1685,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1492
1685
  super(apiKey, options);
1493
1686
  this._memoryStorage = new PostHogMemoryStorage();
1494
1687
  this.options = options;
1688
+ this.logger = createLogger(this.logMsgIfDebug.bind(this));
1495
1689
  this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
1496
1690
  if (options.personalApiKey) {
1497
1691
  if (options.personalApiKey.includes('phc_')) {
@@ -1518,7 +1712,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1518
1712
  });
1519
1713
  }
1520
1714
  }
1521
- this.errorTracking = new ErrorTracking(this, options);
1715
+ this.errorTracking = new ErrorTracking(this, options, this.logger);
1522
1716
  this.distinctIdHasSentFlagCalls = {};
1523
1717
  this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
1524
1718
  }
@@ -1551,146 +1745,43 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1551
1745
  if (typeof props === 'string') {
1552
1746
  this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
1553
1747
  }
1554
- const {
1748
+ this.addPendingPromise(this.prepareEventMessage(props).then(({
1555
1749
  distinctId,
1556
1750
  event,
1557
1751
  properties,
1558
- groups,
1559
- sendFeatureFlags,
1560
- timestamp,
1561
- disableGeoip,
1562
- uuid
1563
- } = props;
1564
- // Run before_send if configured
1565
- const eventMessage = this._runBeforeSend({
1566
- distinctId,
1567
- event,
1568
- properties,
1569
- groups,
1570
- sendFeatureFlags,
1571
- timestamp,
1572
- disableGeoip,
1573
- uuid
1574
- });
1575
- if (!eventMessage) {
1576
- return;
1577
- }
1578
- const _capture = props => {
1579
- super.captureStateless(eventMessage.distinctId, eventMessage.event, props, {
1580
- timestamp: eventMessage.timestamp,
1581
- disableGeoip: eventMessage.disableGeoip,
1582
- uuid: eventMessage.uuid
1752
+ options
1753
+ }) => {
1754
+ return super.captureStateless(distinctId, event, properties, {
1755
+ timestamp: options.timestamp,
1756
+ disableGeoip: options.disableGeoip,
1757
+ uuid: options.uuid
1583
1758
  });
1584
- };
1585
- // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
1586
- const capturePromise = Promise.resolve().then(async () => {
1587
- if (sendFeatureFlags) {
1588
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
1589
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
1590
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
1591
- }
1592
- if (event === '$feature_flag_called') {
1593
- // 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.
1594
- return {};
1595
- }
1596
- return {};
1597
- }).then(flags => {
1598
- // Derive the relevant flag properties to add
1599
- const additionalProperties = {};
1600
- if (flags) {
1601
- for (const [feature, variant] of Object.entries(flags)) {
1602
- additionalProperties[`$feature/${feature}`] = variant;
1603
- }
1759
+ }).catch(err => {
1760
+ if (err) {
1761
+ console.error(err);
1604
1762
  }
1605
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
1606
- if (activeFlags.length > 0) {
1607
- additionalProperties['$active_feature_flags'] = activeFlags;
1608
- }
1609
- return additionalProperties;
1610
- }).catch(() => {
1611
- // Something went wrong getting the flag info - we should capture the event anyways
1612
- return {};
1613
- }).then(additionalProperties => {
1614
- // No matter what - capture the event
1615
- _capture({
1616
- ...additionalProperties,
1617
- ...(eventMessage.properties || {}),
1618
- $groups: eventMessage.groups || groups
1619
- });
1620
- });
1621
- this.addPendingPromise(capturePromise);
1763
+ }));
1622
1764
  }
1623
1765
  async captureImmediate(props) {
1624
1766
  if (typeof props === 'string') {
1625
- this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
1767
+ this.logMsgIfDebug(() => console.warn('Called captureImmediate() with a string as the first argument when an object was expected.'));
1626
1768
  }
1627
- const {
1769
+ return this.addPendingPromise(this.prepareEventMessage(props).then(({
1628
1770
  distinctId,
1629
1771
  event,
1630
1772
  properties,
1631
- groups,
1632
- sendFeatureFlags,
1633
- timestamp,
1634
- disableGeoip,
1635
- uuid
1636
- } = props;
1637
- // Run before_send if configured
1638
- const eventMessage = this._runBeforeSend({
1639
- distinctId,
1640
- event,
1641
- properties,
1642
- groups,
1643
- sendFeatureFlags,
1644
- timestamp,
1645
- disableGeoip,
1646
- uuid
1647
- });
1648
- if (!eventMessage) {
1649
- return;
1650
- }
1651
- const _capture = props => {
1652
- return super.captureStatelessImmediate(eventMessage.distinctId, eventMessage.event, props, {
1653
- timestamp: eventMessage.timestamp,
1654
- disableGeoip: eventMessage.disableGeoip,
1655
- uuid: eventMessage.uuid
1773
+ options
1774
+ }) => {
1775
+ return super.captureStatelessImmediate(distinctId, event, properties, {
1776
+ timestamp: options.timestamp,
1777
+ disableGeoip: options.disableGeoip,
1778
+ uuid: options.uuid
1656
1779
  });
1657
- };
1658
- const capturePromise = Promise.resolve().then(async () => {
1659
- if (sendFeatureFlags) {
1660
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
1661
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
1662
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
1663
- }
1664
- if (event === '$feature_flag_called') {
1665
- // 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.
1666
- return {};
1667
- }
1668
- return {};
1669
- }).then(flags => {
1670
- // Derive the relevant flag properties to add
1671
- const additionalProperties = {};
1672
- if (flags) {
1673
- for (const [feature, variant] of Object.entries(flags)) {
1674
- additionalProperties[`$feature/${feature}`] = variant;
1675
- }
1780
+ }).catch(err => {
1781
+ if (err) {
1782
+ console.error(err);
1676
1783
  }
1677
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
1678
- if (activeFlags.length > 0) {
1679
- additionalProperties['$active_feature_flags'] = activeFlags;
1680
- }
1681
- return additionalProperties;
1682
- }).catch(() => {
1683
- // Something went wrong getting the flag info - we should capture the event anyways
1684
- return {};
1685
- }).then(additionalProperties => {
1686
- // No matter what - capture the event
1687
- _capture({
1688
- ...additionalProperties,
1689
- ...(eventMessage.properties || {}),
1690
- $groups: eventMessage.groups || groups
1691
- });
1692
- });
1693
- await capturePromise;
1784
+ }));
1694
1785
  }
1695
1786
  identify({
1696
1787
  distinctId,
@@ -1960,6 +2051,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1960
2051
  }
1961
2052
  async _shutdown(shutdownTimeoutMs) {
1962
2053
  this.featureFlagsPoller?.stopPoller();
2054
+ this.errorTracking.shutdown();
1963
2055
  return super._shutdown(shutdownTimeoutMs);
1964
2056
  }
1965
2057
  async _requestRemoteConfigPayload(flagKey) {
@@ -2087,18 +2179,88 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
2087
2179
  }
2088
2180
  captureException(error, distinctId, additionalProperties) {
2089
2181
  const syntheticException = new Error('PostHog syntheticException');
2090
- ErrorTracking.buildEventMessage(error, {
2182
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2091
2183
  syntheticException
2092
- }, distinctId, additionalProperties).then(msg => {
2093
- this.capture(msg);
2094
- });
2184
+ }, distinctId, additionalProperties).then(msg => this.capture(msg)));
2095
2185
  }
2096
2186
  async captureExceptionImmediate(error, distinctId, additionalProperties) {
2097
2187
  const syntheticException = new Error('PostHog syntheticException');
2098
- const evtMsg = await ErrorTracking.buildEventMessage(error, {
2188
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2099
2189
  syntheticException
2100
- }, distinctId, additionalProperties);
2101
- return await this.captureImmediate(evtMsg);
2190
+ }, distinctId, additionalProperties).then(msg => this.captureImmediate(msg)));
2191
+ }
2192
+ async prepareEventMessage(props) {
2193
+ const {
2194
+ distinctId,
2195
+ event,
2196
+ properties,
2197
+ groups,
2198
+ sendFeatureFlags,
2199
+ timestamp,
2200
+ disableGeoip,
2201
+ uuid
2202
+ } = props;
2203
+ // Run before_send if configured
2204
+ const eventMessage = this._runBeforeSend({
2205
+ distinctId,
2206
+ event,
2207
+ properties,
2208
+ groups,
2209
+ sendFeatureFlags,
2210
+ timestamp,
2211
+ disableGeoip,
2212
+ uuid
2213
+ });
2214
+ if (!eventMessage) {
2215
+ return Promise.reject(null);
2216
+ }
2217
+ // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
2218
+ const eventProperties = await Promise.resolve().then(async () => {
2219
+ if (sendFeatureFlags) {
2220
+ // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
2221
+ const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
2222
+ return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
2223
+ }
2224
+ if (event === '$feature_flag_called') {
2225
+ // 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.
2226
+ return {};
2227
+ }
2228
+ return {};
2229
+ }).then(flags => {
2230
+ // Derive the relevant flag properties to add
2231
+ const additionalProperties = {};
2232
+ if (flags) {
2233
+ for (const [feature, variant] of Object.entries(flags)) {
2234
+ additionalProperties[`$feature/${feature}`] = variant;
2235
+ }
2236
+ }
2237
+ const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
2238
+ if (activeFlags.length > 0) {
2239
+ additionalProperties['$active_feature_flags'] = activeFlags;
2240
+ }
2241
+ return additionalProperties;
2242
+ }).catch(() => {
2243
+ // Something went wrong getting the flag info - we should capture the event anyways
2244
+ return {};
2245
+ }).then(additionalProperties => {
2246
+ // No matter what - capture the event
2247
+ const props = {
2248
+ ...additionalProperties,
2249
+ ...(eventMessage.properties || {}),
2250
+ $groups: eventMessage.groups || groups
2251
+ };
2252
+ return props;
2253
+ });
2254
+ return {
2255
+ distinctId: eventMessage.distinctId,
2256
+ event: eventMessage.event,
2257
+ properties: eventProperties,
2258
+ options: {
2259
+ timestamp: eventMessage.timestamp,
2260
+ disableGeoip: eventMessage.disableGeoip,
2261
+ uuid: eventMessage.uuid
2262
+ }
2263
+ };
2102
2264
  }
2103
2265
  _runBeforeSend(eventMessage) {
2104
2266
  const beforeSend = this.options.before_send;