posthog-node 5.7.0 → 5.8.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.
@@ -622,6 +622,71 @@ function applyChunkIds(frames, parser) {
622
622
  return frames;
623
623
  }
624
624
 
625
+ const ObjProto = Object.prototype;
626
+ const type_utils_toString = ObjProto.toString;
627
+ const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
628
+
629
+ function clampToRange(value, min, max, logger, fallbackValue) {
630
+ if (min > max) {
631
+ logger.warn('min cannot be greater than max.');
632
+ min = max;
633
+ }
634
+ if (isNumber(value)) if (value > max) {
635
+ logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
636
+ return max;
637
+ } else {
638
+ if (!(value < min)) return value;
639
+ logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
640
+ return min;
641
+ }
642
+ logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
643
+ return clampToRange(max, min, max, logger);
644
+ }
645
+
646
+ class BucketedRateLimiter {
647
+ constructor(_options){
648
+ this._options = _options;
649
+ this._buckets = {};
650
+ this._refillBuckets = ()=>{
651
+ Object.keys(this._buckets).forEach((key)=>{
652
+ const newTokens = this._getBucket(key) + this._refillRate;
653
+ if (newTokens >= this._bucketSize) delete this._buckets[key];
654
+ else this._setBucket(key, newTokens);
655
+ });
656
+ };
657
+ this._getBucket = (key)=>this._buckets[String(key)];
658
+ this._setBucket = (key, value)=>{
659
+ this._buckets[String(key)] = value;
660
+ };
661
+ this.consumeRateLimit = (key)=>{
662
+ var _this__getBucket;
663
+ let tokens = null != (_this__getBucket = this._getBucket(key)) ? _this__getBucket : this._bucketSize;
664
+ tokens = Math.max(tokens - 1, 0);
665
+ if (0 === tokens) return true;
666
+ this._setBucket(key, tokens);
667
+ const hasReachedZero = 0 === tokens;
668
+ if (hasReachedZero) {
669
+ var _this__onBucketRateLimited, _this;
670
+ null == (_this__onBucketRateLimited = (_this = this)._onBucketRateLimited) || _this__onBucketRateLimited.call(_this, key);
671
+ }
672
+ return hasReachedZero;
673
+ };
674
+ this._onBucketRateLimited = this._options._onBucketRateLimited;
675
+ this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
676
+ this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
677
+ this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
678
+ setInterval(()=>{
679
+ this._refillBuckets();
680
+ }, this._refillInterval);
681
+ }
682
+ }
683
+
684
+ function safeSetTimeout(fn, timeout) {
685
+ const t = setTimeout(fn, timeout);
686
+ (null == t ? void 0 : t.unref) && (null == t || t.unref());
687
+ return t;
688
+ }
689
+
625
690
  const SHUTDOWN_TIMEOUT = 2000;
626
691
  class ErrorTracking {
627
692
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
@@ -643,9 +708,20 @@ class ErrorTracking {
643
708
  }
644
709
  };
645
710
  }
646
- constructor(client, options) {
711
+ constructor(client, options, _logger) {
647
712
  this.client = client;
648
713
  this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
714
+ this._logger = _logger;
715
+ // by default captures ten exceptions before rate limiting by exception type
716
+ // refills at a rate of one token / 10 second period
717
+ // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
718
+ this._rateLimiter = new BucketedRateLimiter({
719
+ refillRate: 1,
720
+ bucketSize: 10,
721
+ refillInterval: 10000,
722
+ // ten seconds in milliseconds
723
+ _logger: this._logger
724
+ });
649
725
  this.startAutocaptureIfEnabled();
650
726
  }
651
727
  startAutocaptureIfEnabled() {
@@ -655,7 +731,16 @@ class ErrorTracking {
655
731
  }
656
732
  }
657
733
  onException(exception, hint) {
658
- void ErrorTracking.buildEventMessage(exception, hint).then(msg => {
734
+ return ErrorTracking.buildEventMessage(exception, hint).then(msg => {
735
+ const exceptionProperties = msg.properties;
736
+ const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
737
+ const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
738
+ if (isRateLimited) {
739
+ this._logger.info('Skipping exception capture because of client rate limiting.', {
740
+ exception: exceptionType
741
+ });
742
+ return;
743
+ }
659
744
  this.client.capture(msg);
660
745
  });
661
746
  }
@@ -684,7 +769,7 @@ function setupExpressErrorHandler(_posthog, app) {
684
769
  });
685
770
  }
686
771
 
687
- var version = "5.7.0";
772
+ var version = "5.8.0";
688
773
 
689
774
  /**
690
775
  * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
@@ -972,7 +1057,70 @@ class FeatureFlagsPoller {
972
1057
  }
973
1058
  return null;
974
1059
  }
975
- async matchFeatureFlagProperties(flag, distinctId, properties) {
1060
+ async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
1061
+ const targetFlagKey = property.key;
1062
+ if (!this.featureFlagsByKey) {
1063
+ throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
1064
+ }
1065
+ // Check if dependency_chain is present - it should always be provided for flag dependencies
1066
+ if (!('dependency_chain' in property)) {
1067
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
1068
+ }
1069
+ const dependencyChain = property.dependency_chain;
1070
+ // Check for missing or invalid dependency chain (This should never happen, but being defensive)
1071
+ if (!Array.isArray(dependencyChain)) {
1072
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
1073
+ }
1074
+ // Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
1075
+ if (dependencyChain.length === 0) {
1076
+ throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
1077
+ }
1078
+ // Evaluate all dependencies in the chain order
1079
+ for (const depFlagKey of dependencyChain) {
1080
+ if (!(depFlagKey in evaluationCache)) {
1081
+ // Need to evaluate this dependency first
1082
+ const depFlag = this.featureFlagsByKey[depFlagKey];
1083
+ if (!depFlag) {
1084
+ // Missing flag dependency - cannot evaluate locally
1085
+ throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
1086
+ } else if (!depFlag.active) {
1087
+ // Inactive flag evaluates to false
1088
+ evaluationCache[depFlagKey] = false;
1089
+ } else {
1090
+ // Recursively evaluate the dependency
1091
+ try {
1092
+ const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
1093
+ evaluationCache[depFlagKey] = depResult;
1094
+ } catch (error) {
1095
+ // If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
1096
+ throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
1097
+ }
1098
+ }
1099
+ }
1100
+ // Check if dependency evaluation was inconclusive
1101
+ const cachedResult = evaluationCache[depFlagKey];
1102
+ if (cachedResult === null || cachedResult === undefined) {
1103
+ throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
1104
+ }
1105
+ }
1106
+ // The target flag is specified in property.key (This should match the last element in the dependency chain)
1107
+ const targetFlagValue = evaluationCache[targetFlagKey];
1108
+ return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
1109
+ }
1110
+ flagEvaluatesToExpectedValue(expectedValue, flagValue) {
1111
+ // If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
1112
+ // If the expected value is false, then only return true if the flag evaluated to false.
1113
+ if (typeof expectedValue === 'boolean') {
1114
+ return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
1115
+ }
1116
+ // If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
1117
+ if (typeof expectedValue === 'string') {
1118
+ return flagValue === expectedValue;
1119
+ }
1120
+ // The `flag_evaluates_to` operator is not supported for numbers and arrays.
1121
+ return false;
1122
+ }
1123
+ async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
976
1124
  const flagFilters = flag.filters || {};
977
1125
  const flagConditions = flagFilters.groups || [];
978
1126
  let isInconclusive = false;
@@ -994,7 +1142,7 @@ class FeatureFlagsPoller {
994
1142
  });
995
1143
  for (const condition of sortedFlagConditions) {
996
1144
  try {
997
- if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
1145
+ if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
998
1146
  const variantOverride = condition.variant;
999
1147
  const flagVariants = flagFilters.multivariate?.variants || [];
1000
1148
  if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
@@ -1020,7 +1168,7 @@ class FeatureFlagsPoller {
1020
1168
  // We can only return False when all conditions are False
1021
1169
  return false;
1022
1170
  }
1023
- async isConditionMatch(flag, distinctId, condition, properties) {
1171
+ async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
1024
1172
  const rolloutPercentage = condition.rollout_percentage;
1025
1173
  const warnFunction = msg => {
1026
1174
  this.logMsgIfDebug(() => console.warn(msg));
@@ -1032,8 +1180,7 @@ class FeatureFlagsPoller {
1032
1180
  if (propertyType === 'cohort') {
1033
1181
  matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
1034
1182
  } 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;
1183
+ matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
1037
1184
  } else {
1038
1185
  matches = matchProperty(prop, properties, warnFunction);
1039
1186
  }
@@ -1194,7 +1341,7 @@ class FeatureFlagsPoller {
1194
1341
  let abortTimeout = null;
1195
1342
  if (this.timeout && typeof this.timeout === 'number') {
1196
1343
  const controller = new AbortController();
1197
- abortTimeout = core.safeSetTimeout(() => {
1344
+ abortTimeout = safeSetTimeout(() => {
1198
1345
  controller.abort();
1199
1346
  }, this.timeout);
1200
1347
  options.signal = controller.signal;
@@ -1298,6 +1445,10 @@ function matchProperty(property, propertyValues, warnFunction) {
1298
1445
  case 'is_date_after':
1299
1446
  case 'is_date_before':
1300
1447
  {
1448
+ // Boolean values should never be used with date operations
1449
+ if (typeof value === 'boolean') {
1450
+ throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
1451
+ }
1301
1452
  let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
1302
1453
  if (parsedDate == null) {
1303
1454
  parsedDate = convertToDateTime(value);
@@ -1481,6 +1632,37 @@ class PostHogMemoryStorage {
1481
1632
  }
1482
1633
  }
1483
1634
 
1635
+ const _createLogger = (prefix, logMsgIfDebug) => {
1636
+ const logger = {
1637
+ _log: (level, ...args) => {
1638
+ logMsgIfDebug(() => {
1639
+ const consoleLog = console[level];
1640
+ consoleLog(prefix, ...args);
1641
+ });
1642
+ },
1643
+ info: (...args) => {
1644
+ logger._log('log', ...args);
1645
+ },
1646
+ warn: (...args) => {
1647
+ logger._log('warn', ...args);
1648
+ },
1649
+ error: (...args) => {
1650
+ logger._log('error', ...args);
1651
+ },
1652
+ critical: (...args) => {
1653
+ // Critical errors are always logged to the console
1654
+ // eslint-disable-next-line no-console
1655
+ console.error(prefix, ...args);
1656
+ },
1657
+ uninitializedWarning: methodName => {
1658
+ logger.error(`You must initialize PostHog before calling ${methodName}`);
1659
+ },
1660
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
1661
+ };
1662
+ return logger;
1663
+ };
1664
+ const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
1665
+
1484
1666
  // Standard local evaluation rate limit is 600 per minute (10 per second),
1485
1667
  // so the fastest a poller should ever be set is 100ms.
1486
1668
  const MINIMUM_POLLING_INTERVAL = 100;
@@ -1492,6 +1674,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1492
1674
  super(apiKey, options);
1493
1675
  this._memoryStorage = new PostHogMemoryStorage();
1494
1676
  this.options = options;
1677
+ this.logger = createLogger(this.logMsgIfDebug.bind(this));
1495
1678
  this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
1496
1679
  if (options.personalApiKey) {
1497
1680
  if (options.personalApiKey.includes('phc_')) {
@@ -1518,7 +1701,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1518
1701
  });
1519
1702
  }
1520
1703
  }
1521
- this.errorTracking = new ErrorTracking(this, options);
1704
+ this.errorTracking = new ErrorTracking(this, options, this.logger);
1522
1705
  this.distinctIdHasSentFlagCalls = {};
1523
1706
  this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
1524
1707
  }
@@ -1978,7 +2161,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1978
2161
  let abortTimeout = null;
1979
2162
  if (this.options.requestTimeout && typeof this.options.requestTimeout === 'number') {
1980
2163
  const controller = new AbortController();
1981
- abortTimeout = core.safeSetTimeout(() => {
2164
+ abortTimeout = safeSetTimeout(() => {
1982
2165
  controller.abort();
1983
2166
  }, this.options.requestTimeout);
1984
2167
  options.signal = controller.signal;