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.
@@ -625,6 +625,71 @@ function applyChunkIds(frames, parser) {
625
625
  return frames;
626
626
  }
627
627
 
628
+ const ObjProto = Object.prototype;
629
+ const type_utils_toString = ObjProto.toString;
630
+ const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
631
+
632
+ function clampToRange(value, min, max, logger, fallbackValue) {
633
+ if (min > max) {
634
+ logger.warn('min cannot be greater than max.');
635
+ min = max;
636
+ }
637
+ if (isNumber(value)) if (value > max) {
638
+ logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
639
+ return max;
640
+ } else {
641
+ if (!(value < min)) return value;
642
+ logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
643
+ return min;
644
+ }
645
+ logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
646
+ return clampToRange(max, min, max, logger);
647
+ }
648
+
649
+ class BucketedRateLimiter {
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
+ 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
+
628
693
  const SHUTDOWN_TIMEOUT = 2000;
629
694
  class ErrorTracking {
630
695
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
@@ -646,9 +711,20 @@ class ErrorTracking {
646
711
  }
647
712
  };
648
713
  }
649
- constructor(client, options) {
714
+ constructor(client, options, _logger) {
650
715
  this.client = client;
651
716
  this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
717
+ this._logger = _logger;
718
+ // by default captures ten exceptions before rate limiting by exception type
719
+ // refills at a rate of one token / 10 second period
720
+ // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
721
+ this._rateLimiter = new BucketedRateLimiter({
722
+ refillRate: 1,
723
+ bucketSize: 10,
724
+ refillInterval: 10000,
725
+ // ten seconds in milliseconds
726
+ _logger: this._logger
727
+ });
652
728
  this.startAutocaptureIfEnabled();
653
729
  }
654
730
  startAutocaptureIfEnabled() {
@@ -658,7 +734,16 @@ class ErrorTracking {
658
734
  }
659
735
  }
660
736
  onException(exception, hint) {
661
- void ErrorTracking.buildEventMessage(exception, hint).then(msg => {
737
+ return ErrorTracking.buildEventMessage(exception, hint).then(msg => {
738
+ const exceptionProperties = msg.properties;
739
+ const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
740
+ const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
741
+ if (isRateLimited) {
742
+ this._logger.info('Skipping exception capture because of client rate limiting.', {
743
+ exception: exceptionType
744
+ });
745
+ return;
746
+ }
662
747
  this.client.capture(msg);
663
748
  });
664
749
  }
@@ -1090,7 +1175,7 @@ function snipLine(line, colno) {
1090
1175
  return newLine;
1091
1176
  }
1092
1177
 
1093
- var version = "5.7.0";
1178
+ var version = "5.8.0";
1094
1179
 
1095
1180
  /**
1096
1181
  * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
@@ -1378,7 +1463,70 @@ class FeatureFlagsPoller {
1378
1463
  }
1379
1464
  return null;
1380
1465
  }
1381
- async matchFeatureFlagProperties(flag, distinctId, properties) {
1466
+ async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
1467
+ const targetFlagKey = property.key;
1468
+ if (!this.featureFlagsByKey) {
1469
+ throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
1470
+ }
1471
+ // Check if dependency_chain is present - it should always be provided for flag dependencies
1472
+ if (!('dependency_chain' in property)) {
1473
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
1474
+ }
1475
+ const dependencyChain = property.dependency_chain;
1476
+ // Check for missing or invalid dependency chain (This should never happen, but being defensive)
1477
+ if (!Array.isArray(dependencyChain)) {
1478
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
1479
+ }
1480
+ // Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
1481
+ if (dependencyChain.length === 0) {
1482
+ throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
1483
+ }
1484
+ // Evaluate all dependencies in the chain order
1485
+ for (const depFlagKey of dependencyChain) {
1486
+ if (!(depFlagKey in evaluationCache)) {
1487
+ // Need to evaluate this dependency first
1488
+ const depFlag = this.featureFlagsByKey[depFlagKey];
1489
+ if (!depFlag) {
1490
+ // Missing flag dependency - cannot evaluate locally
1491
+ throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
1492
+ } else if (!depFlag.active) {
1493
+ // Inactive flag evaluates to false
1494
+ evaluationCache[depFlagKey] = false;
1495
+ } else {
1496
+ // Recursively evaluate the dependency
1497
+ try {
1498
+ const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
1499
+ evaluationCache[depFlagKey] = depResult;
1500
+ } catch (error) {
1501
+ // If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
1502
+ throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
1503
+ }
1504
+ }
1505
+ }
1506
+ // Check if dependency evaluation was inconclusive
1507
+ const cachedResult = evaluationCache[depFlagKey];
1508
+ if (cachedResult === null || cachedResult === undefined) {
1509
+ throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
1510
+ }
1511
+ }
1512
+ // The target flag is specified in property.key (This should match the last element in the dependency chain)
1513
+ const targetFlagValue = evaluationCache[targetFlagKey];
1514
+ return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
1515
+ }
1516
+ flagEvaluatesToExpectedValue(expectedValue, flagValue) {
1517
+ // If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
1518
+ // If the expected value is false, then only return true if the flag evaluated to false.
1519
+ if (typeof expectedValue === 'boolean') {
1520
+ return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
1521
+ }
1522
+ // If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
1523
+ if (typeof expectedValue === 'string') {
1524
+ return flagValue === expectedValue;
1525
+ }
1526
+ // The `flag_evaluates_to` operator is not supported for numbers and arrays.
1527
+ return false;
1528
+ }
1529
+ async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
1382
1530
  const flagFilters = flag.filters || {};
1383
1531
  const flagConditions = flagFilters.groups || [];
1384
1532
  let isInconclusive = false;
@@ -1400,7 +1548,7 @@ class FeatureFlagsPoller {
1400
1548
  });
1401
1549
  for (const condition of sortedFlagConditions) {
1402
1550
  try {
1403
- if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
1551
+ if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
1404
1552
  const variantOverride = condition.variant;
1405
1553
  const flagVariants = flagFilters.multivariate?.variants || [];
1406
1554
  if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
@@ -1426,7 +1574,7 @@ class FeatureFlagsPoller {
1426
1574
  // We can only return False when all conditions are False
1427
1575
  return false;
1428
1576
  }
1429
- async isConditionMatch(flag, distinctId, condition, properties) {
1577
+ async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
1430
1578
  const rolloutPercentage = condition.rollout_percentage;
1431
1579
  const warnFunction = msg => {
1432
1580
  this.logMsgIfDebug(() => console.warn(msg));
@@ -1438,8 +1586,7 @@ class FeatureFlagsPoller {
1438
1586
  if (propertyType === 'cohort') {
1439
1587
  matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
1440
1588
  } else if (propertyType === 'flag') {
1441
- 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'}'`));
1442
- continue;
1589
+ matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
1443
1590
  } else {
1444
1591
  matches = matchProperty(prop, properties, warnFunction);
1445
1592
  }
@@ -1600,7 +1747,7 @@ class FeatureFlagsPoller {
1600
1747
  let abortTimeout = null;
1601
1748
  if (this.timeout && typeof this.timeout === 'number') {
1602
1749
  const controller = new AbortController();
1603
- abortTimeout = core.safeSetTimeout(() => {
1750
+ abortTimeout = safeSetTimeout(() => {
1604
1751
  controller.abort();
1605
1752
  }, this.timeout);
1606
1753
  options.signal = controller.signal;
@@ -1704,6 +1851,10 @@ function matchProperty(property, propertyValues, warnFunction) {
1704
1851
  case 'is_date_after':
1705
1852
  case 'is_date_before':
1706
1853
  {
1854
+ // Boolean values should never be used with date operations
1855
+ if (typeof value === 'boolean') {
1856
+ throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
1857
+ }
1707
1858
  let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
1708
1859
  if (parsedDate == null) {
1709
1860
  parsedDate = convertToDateTime(value);
@@ -1887,6 +2038,37 @@ class PostHogMemoryStorage {
1887
2038
  }
1888
2039
  }
1889
2040
 
2041
+ const _createLogger = (prefix, logMsgIfDebug) => {
2042
+ const logger = {
2043
+ _log: (level, ...args) => {
2044
+ logMsgIfDebug(() => {
2045
+ const consoleLog = console[level];
2046
+ consoleLog(prefix, ...args);
2047
+ });
2048
+ },
2049
+ info: (...args) => {
2050
+ logger._log('log', ...args);
2051
+ },
2052
+ warn: (...args) => {
2053
+ logger._log('warn', ...args);
2054
+ },
2055
+ error: (...args) => {
2056
+ logger._log('error', ...args);
2057
+ },
2058
+ critical: (...args) => {
2059
+ // Critical errors are always logged to the console
2060
+ // eslint-disable-next-line no-console
2061
+ console.error(prefix, ...args);
2062
+ },
2063
+ uninitializedWarning: methodName => {
2064
+ logger.error(`You must initialize PostHog before calling ${methodName}`);
2065
+ },
2066
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
2067
+ };
2068
+ return logger;
2069
+ };
2070
+ const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
2071
+
1890
2072
  // Standard local evaluation rate limit is 600 per minute (10 per second),
1891
2073
  // so the fastest a poller should ever be set is 100ms.
1892
2074
  const MINIMUM_POLLING_INTERVAL = 100;
@@ -1898,6 +2080,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1898
2080
  super(apiKey, options);
1899
2081
  this._memoryStorage = new PostHogMemoryStorage();
1900
2082
  this.options = options;
2083
+ this.logger = createLogger(this.logMsgIfDebug.bind(this));
1901
2084
  this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
1902
2085
  if (options.personalApiKey) {
1903
2086
  if (options.personalApiKey.includes('phc_')) {
@@ -1924,7 +2107,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1924
2107
  });
1925
2108
  }
1926
2109
  }
1927
- this.errorTracking = new ErrorTracking(this, options);
2110
+ this.errorTracking = new ErrorTracking(this, options, this.logger);
1928
2111
  this.distinctIdHasSentFlagCalls = {};
1929
2112
  this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
1930
2113
  }
@@ -2384,7 +2567,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
2384
2567
  let abortTimeout = null;
2385
2568
  if (this.options.requestTimeout && typeof this.options.requestTimeout === 'number') {
2386
2569
  const controller = new AbortController();
2387
- abortTimeout = core.safeSetTimeout(() => {
2570
+ abortTimeout = safeSetTimeout(() => {
2388
2571
  controller.abort();
2389
2572
  }, this.options.requestTimeout);
2390
2573
  options.signal = controller.signal;