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