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.
- package/dist/edge/index.cjs +194 -11
- package/dist/edge/index.cjs.map +1 -1
- package/dist/edge/index.mjs +193 -10
- package/dist/edge/index.mjs.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/node/index.cjs +194 -11
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.mjs +193 -10
- package/dist/node/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/edge/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
2164
|
+
abortTimeout = safeSetTimeout(() => {
|
|
1982
2165
|
controller.abort();
|
|
1983
2166
|
}, this.options.requestTimeout);
|
|
1984
2167
|
options.signal = controller.signal;
|