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.
@@ -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 { safeSetTimeout, PostHogCoreStateless, getFeatureFlagValue } from '@posthog/core';
4
+ import { PostHogCoreStateless, getFeatureFlagValue } 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.
@@ -623,6 +623,71 @@ function applyChunkIds(frames, parser) {
623
623
  return frames;
624
624
  }
625
625
 
626
+ const ObjProto = Object.prototype;
627
+ const type_utils_toString = ObjProto.toString;
628
+ const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
629
+
630
+ function clampToRange(value, min, max, logger, fallbackValue) {
631
+ if (min > max) {
632
+ logger.warn('min cannot be greater than max.');
633
+ min = max;
634
+ }
635
+ if (isNumber(value)) if (value > max) {
636
+ logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
637
+ return max;
638
+ } else {
639
+ if (!(value < min)) return value;
640
+ logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
641
+ return min;
642
+ }
643
+ logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
644
+ return clampToRange(max, min, max, logger);
645
+ }
646
+
647
+ class BucketedRateLimiter {
648
+ constructor(_options){
649
+ this._options = _options;
650
+ this._buckets = {};
651
+ this._refillBuckets = ()=>{
652
+ Object.keys(this._buckets).forEach((key)=>{
653
+ const newTokens = this._getBucket(key) + this._refillRate;
654
+ if (newTokens >= this._bucketSize) delete this._buckets[key];
655
+ else this._setBucket(key, newTokens);
656
+ });
657
+ };
658
+ this._getBucket = (key)=>this._buckets[String(key)];
659
+ this._setBucket = (key, value)=>{
660
+ this._buckets[String(key)] = value;
661
+ };
662
+ this.consumeRateLimit = (key)=>{
663
+ var _this__getBucket;
664
+ let tokens = null != (_this__getBucket = this._getBucket(key)) ? _this__getBucket : this._bucketSize;
665
+ tokens = Math.max(tokens - 1, 0);
666
+ if (0 === tokens) return true;
667
+ this._setBucket(key, tokens);
668
+ const hasReachedZero = 0 === tokens;
669
+ if (hasReachedZero) {
670
+ var _this__onBucketRateLimited, _this;
671
+ null == (_this__onBucketRateLimited = (_this = this)._onBucketRateLimited) || _this__onBucketRateLimited.call(_this, key);
672
+ }
673
+ return hasReachedZero;
674
+ };
675
+ this._onBucketRateLimited = this._options._onBucketRateLimited;
676
+ this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
677
+ this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
678
+ this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
679
+ setInterval(()=>{
680
+ this._refillBuckets();
681
+ }, this._refillInterval);
682
+ }
683
+ }
684
+
685
+ function safeSetTimeout(fn, timeout) {
686
+ const t = setTimeout(fn, timeout);
687
+ (null == t ? void 0 : t.unref) && (null == t || t.unref());
688
+ return t;
689
+ }
690
+
626
691
  const SHUTDOWN_TIMEOUT = 2000;
627
692
  class ErrorTracking {
628
693
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
@@ -644,9 +709,20 @@ class ErrorTracking {
644
709
  }
645
710
  };
646
711
  }
647
- constructor(client, options) {
712
+ constructor(client, options, _logger) {
648
713
  this.client = client;
649
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
+ });
650
726
  this.startAutocaptureIfEnabled();
651
727
  }
652
728
  startAutocaptureIfEnabled() {
@@ -656,7 +732,16 @@ class ErrorTracking {
656
732
  }
657
733
  }
658
734
  onException(exception, hint) {
659
- void ErrorTracking.buildEventMessage(exception, hint).then(msg => {
735
+ return ErrorTracking.buildEventMessage(exception, hint).then(msg => {
736
+ const exceptionProperties = msg.properties;
737
+ const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
738
+ const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
739
+ if (isRateLimited) {
740
+ this._logger.info('Skipping exception capture because of client rate limiting.', {
741
+ exception: exceptionType
742
+ });
743
+ return;
744
+ }
660
745
  this.client.capture(msg);
661
746
  });
662
747
  }
@@ -1088,7 +1173,7 @@ function snipLine(line, colno) {
1088
1173
  return newLine;
1089
1174
  }
1090
1175
 
1091
- var version = "5.7.0";
1176
+ var version = "5.8.0";
1092
1177
 
1093
1178
  /**
1094
1179
  * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
@@ -1376,7 +1461,70 @@ class FeatureFlagsPoller {
1376
1461
  }
1377
1462
  return null;
1378
1463
  }
1379
- async matchFeatureFlagProperties(flag, distinctId, properties) {
1464
+ async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
1465
+ const targetFlagKey = property.key;
1466
+ if (!this.featureFlagsByKey) {
1467
+ throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
1468
+ }
1469
+ // Check if dependency_chain is present - it should always be provided for flag dependencies
1470
+ if (!('dependency_chain' in property)) {
1471
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
1472
+ }
1473
+ const dependencyChain = property.dependency_chain;
1474
+ // Check for missing or invalid dependency chain (This should never happen, but being defensive)
1475
+ if (!Array.isArray(dependencyChain)) {
1476
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
1477
+ }
1478
+ // Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
1479
+ if (dependencyChain.length === 0) {
1480
+ throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
1481
+ }
1482
+ // Evaluate all dependencies in the chain order
1483
+ for (const depFlagKey of dependencyChain) {
1484
+ if (!(depFlagKey in evaluationCache)) {
1485
+ // Need to evaluate this dependency first
1486
+ const depFlag = this.featureFlagsByKey[depFlagKey];
1487
+ if (!depFlag) {
1488
+ // Missing flag dependency - cannot evaluate locally
1489
+ throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
1490
+ } else if (!depFlag.active) {
1491
+ // Inactive flag evaluates to false
1492
+ evaluationCache[depFlagKey] = false;
1493
+ } else {
1494
+ // Recursively evaluate the dependency
1495
+ try {
1496
+ const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
1497
+ evaluationCache[depFlagKey] = depResult;
1498
+ } catch (error) {
1499
+ // If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
1500
+ throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
1501
+ }
1502
+ }
1503
+ }
1504
+ // Check if dependency evaluation was inconclusive
1505
+ const cachedResult = evaluationCache[depFlagKey];
1506
+ if (cachedResult === null || cachedResult === undefined) {
1507
+ throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
1508
+ }
1509
+ }
1510
+ // The target flag is specified in property.key (This should match the last element in the dependency chain)
1511
+ const targetFlagValue = evaluationCache[targetFlagKey];
1512
+ return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
1513
+ }
1514
+ flagEvaluatesToExpectedValue(expectedValue, flagValue) {
1515
+ // If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
1516
+ // If the expected value is false, then only return true if the flag evaluated to false.
1517
+ if (typeof expectedValue === 'boolean') {
1518
+ return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
1519
+ }
1520
+ // If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
1521
+ if (typeof expectedValue === 'string') {
1522
+ return flagValue === expectedValue;
1523
+ }
1524
+ // The `flag_evaluates_to` operator is not supported for numbers and arrays.
1525
+ return false;
1526
+ }
1527
+ async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
1380
1528
  const flagFilters = flag.filters || {};
1381
1529
  const flagConditions = flagFilters.groups || [];
1382
1530
  let isInconclusive = false;
@@ -1398,7 +1546,7 @@ class FeatureFlagsPoller {
1398
1546
  });
1399
1547
  for (const condition of sortedFlagConditions) {
1400
1548
  try {
1401
- if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
1549
+ if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
1402
1550
  const variantOverride = condition.variant;
1403
1551
  const flagVariants = flagFilters.multivariate?.variants || [];
1404
1552
  if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
@@ -1424,7 +1572,7 @@ class FeatureFlagsPoller {
1424
1572
  // We can only return False when all conditions are False
1425
1573
  return false;
1426
1574
  }
1427
- async isConditionMatch(flag, distinctId, condition, properties) {
1575
+ async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
1428
1576
  const rolloutPercentage = condition.rollout_percentage;
1429
1577
  const warnFunction = msg => {
1430
1578
  this.logMsgIfDebug(() => console.warn(msg));
@@ -1436,8 +1584,7 @@ class FeatureFlagsPoller {
1436
1584
  if (propertyType === 'cohort') {
1437
1585
  matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
1438
1586
  } else if (propertyType === 'flag') {
1439
- 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'}'`));
1440
- continue;
1587
+ matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
1441
1588
  } else {
1442
1589
  matches = matchProperty(prop, properties, warnFunction);
1443
1590
  }
@@ -1702,6 +1849,10 @@ function matchProperty(property, propertyValues, warnFunction) {
1702
1849
  case 'is_date_after':
1703
1850
  case 'is_date_before':
1704
1851
  {
1852
+ // Boolean values should never be used with date operations
1853
+ if (typeof value === 'boolean') {
1854
+ throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
1855
+ }
1705
1856
  let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
1706
1857
  if (parsedDate == null) {
1707
1858
  parsedDate = convertToDateTime(value);
@@ -1885,6 +2036,37 @@ class PostHogMemoryStorage {
1885
2036
  }
1886
2037
  }
1887
2038
 
2039
+ const _createLogger = (prefix, logMsgIfDebug) => {
2040
+ const logger = {
2041
+ _log: (level, ...args) => {
2042
+ logMsgIfDebug(() => {
2043
+ const consoleLog = console[level];
2044
+ consoleLog(prefix, ...args);
2045
+ });
2046
+ },
2047
+ info: (...args) => {
2048
+ logger._log('log', ...args);
2049
+ },
2050
+ warn: (...args) => {
2051
+ logger._log('warn', ...args);
2052
+ },
2053
+ error: (...args) => {
2054
+ logger._log('error', ...args);
2055
+ },
2056
+ critical: (...args) => {
2057
+ // Critical errors are always logged to the console
2058
+ // eslint-disable-next-line no-console
2059
+ console.error(prefix, ...args);
2060
+ },
2061
+ uninitializedWarning: methodName => {
2062
+ logger.error(`You must initialize PostHog before calling ${methodName}`);
2063
+ },
2064
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
2065
+ };
2066
+ return logger;
2067
+ };
2068
+ const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
2069
+
1888
2070
  // Standard local evaluation rate limit is 600 per minute (10 per second),
1889
2071
  // so the fastest a poller should ever be set is 100ms.
1890
2072
  const MINIMUM_POLLING_INTERVAL = 100;
@@ -1896,6 +2078,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
1896
2078
  super(apiKey, options);
1897
2079
  this._memoryStorage = new PostHogMemoryStorage();
1898
2080
  this.options = options;
2081
+ this.logger = createLogger(this.logMsgIfDebug.bind(this));
1899
2082
  this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
1900
2083
  if (options.personalApiKey) {
1901
2084
  if (options.personalApiKey.includes('phc_')) {
@@ -1922,7 +2105,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
1922
2105
  });
1923
2106
  }
1924
2107
  }
1925
- this.errorTracking = new ErrorTracking(this, options);
2108
+ this.errorTracking = new ErrorTracking(this, options, this.logger);
1926
2109
  this.distinctIdHasSentFlagCalls = {};
1927
2110
  this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
1928
2111
  }