posthog-node 5.7.0 → 5.8.1

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.
@@ -318,7 +318,7 @@ function makeUncaughtExceptionHandler(captureFn, onFatalFn) {
318
318
  });
319
319
  if (!calledFatalError && processWouldExit) {
320
320
  calledFatalError = true;
321
- onFatalFn();
321
+ onFatalFn(error);
322
322
  }
323
323
  }, {
324
324
  _posthogErrorHandler: true
@@ -329,7 +329,7 @@ function addUncaughtExceptionListener(captureFn, onFatalFn) {
329
329
  }
330
330
  function addUnhandledRejectionListener(captureFn) {
331
331
  global.process.on('unhandledRejection', reason => {
332
- captureFn(reason, {
332
+ return captureFn(reason, {
333
333
  mechanism: {
334
334
  type: 'onunhandledrejection',
335
335
  handled: false
@@ -346,8 +346,7 @@ let cachedFilenameChunkIds;
346
346
  function getFilenameToChunkIdMap(stackParser) {
347
347
  const chunkIdMap = globalThis._posthogChunkIds;
348
348
  if (!chunkIdMap) {
349
- console.error('No chunk id map found');
350
- return {};
349
+ return null;
351
350
  }
352
351
  const chunkIdKeys = Object.keys(chunkIdMap);
353
352
  if (cachedFilenameChunkIds && chunkIdKeys.length === lastKeysCount) {
@@ -618,15 +617,102 @@ function parseStackFrames(stackParser, error) {
618
617
  function applyChunkIds(frames, parser) {
619
618
  const filenameChunkIdMap = getFilenameToChunkIdMap(parser);
620
619
  frames.forEach(frame => {
621
- if (frame.filename) {
620
+ if (frame.filename && filenameChunkIdMap) {
622
621
  frame.chunk_id = filenameChunkIdMap[frame.filename];
623
622
  }
624
623
  });
625
624
  return frames;
626
625
  }
627
626
 
627
+ const ObjProto = Object.prototype;
628
+ const type_utils_toString = ObjProto.toString;
629
+ const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
630
+
631
+ function clampToRange(value, min, max, logger, fallbackValue) {
632
+ if (min > max) {
633
+ logger.warn('min cannot be greater than max.');
634
+ min = max;
635
+ }
636
+ if (isNumber(value)) if (value > max) {
637
+ logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
638
+ return max;
639
+ } else {
640
+ if (!(value < min)) return value;
641
+ logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
642
+ return min;
643
+ }
644
+ logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
645
+ return clampToRange(max, min, max, logger);
646
+ }
647
+
648
+ class BucketedRateLimiter {
649
+ stop() {
650
+ if (this._removeInterval) {
651
+ clearInterval(this._removeInterval);
652
+ this._removeInterval = void 0;
653
+ }
654
+ }
655
+ constructor(_options){
656
+ this._options = _options;
657
+ this._buckets = {};
658
+ this._refillBuckets = ()=>{
659
+ Object.keys(this._buckets).forEach((key)=>{
660
+ const newTokens = this._getBucket(key) + this._refillRate;
661
+ if (newTokens >= this._bucketSize) delete this._buckets[key];
662
+ else this._setBucket(key, newTokens);
663
+ });
664
+ };
665
+ this._getBucket = (key)=>this._buckets[String(key)];
666
+ this._setBucket = (key, value)=>{
667
+ this._buckets[String(key)] = value;
668
+ };
669
+ this.consumeRateLimit = (key)=>{
670
+ var _this__getBucket;
671
+ let tokens = null != (_this__getBucket = this._getBucket(key)) ? _this__getBucket : this._bucketSize;
672
+ tokens = Math.max(tokens - 1, 0);
673
+ if (0 === tokens) return true;
674
+ this._setBucket(key, tokens);
675
+ const hasReachedZero = 0 === tokens;
676
+ if (hasReachedZero) {
677
+ var _this__onBucketRateLimited, _this;
678
+ null == (_this__onBucketRateLimited = (_this = this)._onBucketRateLimited) || _this__onBucketRateLimited.call(_this, key);
679
+ }
680
+ return hasReachedZero;
681
+ };
682
+ this._onBucketRateLimited = this._options._onBucketRateLimited;
683
+ this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
684
+ this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
685
+ this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
686
+ this._removeInterval = setInterval(()=>{
687
+ this._refillBuckets();
688
+ }, this._refillInterval);
689
+ }
690
+ }
691
+
692
+ function safeSetTimeout(fn, timeout) {
693
+ const t = setTimeout(fn, timeout);
694
+ (null == t ? void 0 : t.unref) && (null == t || t.unref());
695
+ return t;
696
+ }
697
+
628
698
  const SHUTDOWN_TIMEOUT = 2000;
629
699
  class ErrorTracking {
700
+ constructor(client, options, _logger) {
701
+ this.client = client;
702
+ this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
703
+ this._logger = _logger;
704
+ // by default captures ten exceptions before rate limiting by exception type
705
+ // refills at a rate of one token / 10 second period
706
+ // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
707
+ this._rateLimiter = new BucketedRateLimiter({
708
+ refillRate: 1,
709
+ bucketSize: 10,
710
+ refillInterval: 10000,
711
+ // ten seconds in milliseconds
712
+ _logger: this._logger
713
+ });
714
+ this.startAutocaptureIfEnabled();
715
+ }
630
716
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
631
717
  const properties = {
632
718
  ...additionalProperties
@@ -646,28 +732,38 @@ class ErrorTracking {
646
732
  }
647
733
  };
648
734
  }
649
- constructor(client, options) {
650
- this.client = client;
651
- this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
652
- this.startAutocaptureIfEnabled();
653
- }
654
735
  startAutocaptureIfEnabled() {
655
736
  if (this.isEnabled()) {
656
737
  addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this));
657
738
  addUnhandledRejectionListener(this.onException.bind(this));
658
739
  }
659
740
  }
660
- onException(exception, hint) {
661
- void ErrorTracking.buildEventMessage(exception, hint).then(msg => {
662
- this.client.capture(msg);
663
- });
741
+ async onException(exception, hint) {
742
+ this.client.addPendingPromise((async () => {
743
+ const eventMessage = await ErrorTracking.buildEventMessage(exception, hint);
744
+ const exceptionProperties = eventMessage.properties;
745
+ const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
746
+ const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
747
+ if (isRateLimited) {
748
+ this._logger.info('Skipping exception capture because of client rate limiting.', {
749
+ exception: exceptionType
750
+ });
751
+ return;
752
+ }
753
+ return this.client.capture(eventMessage);
754
+ })());
664
755
  }
665
- async onFatalError() {
756
+ async onFatalError(exception) {
757
+ console.error(exception);
666
758
  await this.client.shutdown(SHUTDOWN_TIMEOUT);
759
+ process.exit(1);
667
760
  }
668
761
  isEnabled() {
669
762
  return !this.client.isDisabled && this._exceptionAutocaptureEnabled;
670
763
  }
764
+ shutdown() {
765
+ this._rateLimiter.stop();
766
+ }
671
767
  }
672
768
 
673
769
  function setupExpressErrorHandler(_posthog, app) {
@@ -1090,7 +1186,7 @@ function snipLine(line, colno) {
1090
1186
  return newLine;
1091
1187
  }
1092
1188
 
1093
- var version = "5.7.0";
1189
+ var version = "5.8.1";
1094
1190
 
1095
1191
  /**
1096
1192
  * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
@@ -1378,7 +1474,70 @@ class FeatureFlagsPoller {
1378
1474
  }
1379
1475
  return null;
1380
1476
  }
1381
- async matchFeatureFlagProperties(flag, distinctId, properties) {
1477
+ async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
1478
+ const targetFlagKey = property.key;
1479
+ if (!this.featureFlagsByKey) {
1480
+ throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
1481
+ }
1482
+ // Check if dependency_chain is present - it should always be provided for flag dependencies
1483
+ if (!('dependency_chain' in property)) {
1484
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
1485
+ }
1486
+ const dependencyChain = property.dependency_chain;
1487
+ // Check for missing or invalid dependency chain (This should never happen, but being defensive)
1488
+ if (!Array.isArray(dependencyChain)) {
1489
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
1490
+ }
1491
+ // Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
1492
+ if (dependencyChain.length === 0) {
1493
+ throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
1494
+ }
1495
+ // Evaluate all dependencies in the chain order
1496
+ for (const depFlagKey of dependencyChain) {
1497
+ if (!(depFlagKey in evaluationCache)) {
1498
+ // Need to evaluate this dependency first
1499
+ const depFlag = this.featureFlagsByKey[depFlagKey];
1500
+ if (!depFlag) {
1501
+ // Missing flag dependency - cannot evaluate locally
1502
+ throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
1503
+ } else if (!depFlag.active) {
1504
+ // Inactive flag evaluates to false
1505
+ evaluationCache[depFlagKey] = false;
1506
+ } else {
1507
+ // Recursively evaluate the dependency
1508
+ try {
1509
+ const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
1510
+ evaluationCache[depFlagKey] = depResult;
1511
+ } catch (error) {
1512
+ // If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
1513
+ throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
1514
+ }
1515
+ }
1516
+ }
1517
+ // Check if dependency evaluation was inconclusive
1518
+ const cachedResult = evaluationCache[depFlagKey];
1519
+ if (cachedResult === null || cachedResult === undefined) {
1520
+ throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
1521
+ }
1522
+ }
1523
+ // The target flag is specified in property.key (This should match the last element in the dependency chain)
1524
+ const targetFlagValue = evaluationCache[targetFlagKey];
1525
+ return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
1526
+ }
1527
+ flagEvaluatesToExpectedValue(expectedValue, flagValue) {
1528
+ // If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
1529
+ // If the expected value is false, then only return true if the flag evaluated to false.
1530
+ if (typeof expectedValue === 'boolean') {
1531
+ return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
1532
+ }
1533
+ // If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
1534
+ if (typeof expectedValue === 'string') {
1535
+ return flagValue === expectedValue;
1536
+ }
1537
+ // The `flag_evaluates_to` operator is not supported for numbers and arrays.
1538
+ return false;
1539
+ }
1540
+ async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
1382
1541
  const flagFilters = flag.filters || {};
1383
1542
  const flagConditions = flagFilters.groups || [];
1384
1543
  let isInconclusive = false;
@@ -1400,7 +1559,7 @@ class FeatureFlagsPoller {
1400
1559
  });
1401
1560
  for (const condition of sortedFlagConditions) {
1402
1561
  try {
1403
- if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
1562
+ if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
1404
1563
  const variantOverride = condition.variant;
1405
1564
  const flagVariants = flagFilters.multivariate?.variants || [];
1406
1565
  if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
@@ -1426,7 +1585,7 @@ class FeatureFlagsPoller {
1426
1585
  // We can only return False when all conditions are False
1427
1586
  return false;
1428
1587
  }
1429
- async isConditionMatch(flag, distinctId, condition, properties) {
1588
+ async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
1430
1589
  const rolloutPercentage = condition.rollout_percentage;
1431
1590
  const warnFunction = msg => {
1432
1591
  this.logMsgIfDebug(() => console.warn(msg));
@@ -1438,8 +1597,7 @@ class FeatureFlagsPoller {
1438
1597
  if (propertyType === 'cohort') {
1439
1598
  matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
1440
1599
  } 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;
1600
+ matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
1443
1601
  } else {
1444
1602
  matches = matchProperty(prop, properties, warnFunction);
1445
1603
  }
@@ -1600,7 +1758,7 @@ class FeatureFlagsPoller {
1600
1758
  let abortTimeout = null;
1601
1759
  if (this.timeout && typeof this.timeout === 'number') {
1602
1760
  const controller = new AbortController();
1603
- abortTimeout = core.safeSetTimeout(() => {
1761
+ abortTimeout = safeSetTimeout(() => {
1604
1762
  controller.abort();
1605
1763
  }, this.timeout);
1606
1764
  options.signal = controller.signal;
@@ -1704,6 +1862,10 @@ function matchProperty(property, propertyValues, warnFunction) {
1704
1862
  case 'is_date_after':
1705
1863
  case 'is_date_before':
1706
1864
  {
1865
+ // Boolean values should never be used with date operations
1866
+ if (typeof value === 'boolean') {
1867
+ throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
1868
+ }
1707
1869
  let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
1708
1870
  if (parsedDate == null) {
1709
1871
  parsedDate = convertToDateTime(value);
@@ -1887,6 +2049,37 @@ class PostHogMemoryStorage {
1887
2049
  }
1888
2050
  }
1889
2051
 
2052
+ const _createLogger = (prefix, logMsgIfDebug) => {
2053
+ const logger = {
2054
+ _log: (level, ...args) => {
2055
+ logMsgIfDebug(() => {
2056
+ const consoleLog = console[level];
2057
+ consoleLog(prefix, ...args);
2058
+ });
2059
+ },
2060
+ info: (...args) => {
2061
+ logger._log('log', ...args);
2062
+ },
2063
+ warn: (...args) => {
2064
+ logger._log('warn', ...args);
2065
+ },
2066
+ error: (...args) => {
2067
+ logger._log('error', ...args);
2068
+ },
2069
+ critical: (...args) => {
2070
+ // Critical errors are always logged to the console
2071
+ // eslint-disable-next-line no-console
2072
+ console.error(prefix, ...args);
2073
+ },
2074
+ uninitializedWarning: methodName => {
2075
+ logger.error(`You must initialize PostHog before calling ${methodName}`);
2076
+ },
2077
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
2078
+ };
2079
+ return logger;
2080
+ };
2081
+ const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
2082
+
1890
2083
  // Standard local evaluation rate limit is 600 per minute (10 per second),
1891
2084
  // so the fastest a poller should ever be set is 100ms.
1892
2085
  const MINIMUM_POLLING_INTERVAL = 100;
@@ -1898,6 +2091,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1898
2091
  super(apiKey, options);
1899
2092
  this._memoryStorage = new PostHogMemoryStorage();
1900
2093
  this.options = options;
2094
+ this.logger = createLogger(this.logMsgIfDebug.bind(this));
1901
2095
  this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
1902
2096
  if (options.personalApiKey) {
1903
2097
  if (options.personalApiKey.includes('phc_')) {
@@ -1924,7 +2118,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1924
2118
  });
1925
2119
  }
1926
2120
  }
1927
- this.errorTracking = new ErrorTracking(this, options);
2121
+ this.errorTracking = new ErrorTracking(this, options, this.logger);
1928
2122
  this.distinctIdHasSentFlagCalls = {};
1929
2123
  this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
1930
2124
  }
@@ -1957,146 +2151,43 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
1957
2151
  if (typeof props === 'string') {
1958
2152
  this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
1959
2153
  }
1960
- const {
2154
+ this.addPendingPromise(this.prepareEventMessage(props).then(({
1961
2155
  distinctId,
1962
2156
  event,
1963
2157
  properties,
1964
- groups,
1965
- sendFeatureFlags,
1966
- timestamp,
1967
- disableGeoip,
1968
- uuid
1969
- } = props;
1970
- // Run before_send if configured
1971
- const eventMessage = this._runBeforeSend({
1972
- distinctId,
1973
- event,
1974
- properties,
1975
- groups,
1976
- sendFeatureFlags,
1977
- timestamp,
1978
- disableGeoip,
1979
- uuid
1980
- });
1981
- if (!eventMessage) {
1982
- return;
1983
- }
1984
- const _capture = props => {
1985
- super.captureStateless(eventMessage.distinctId, eventMessage.event, props, {
1986
- timestamp: eventMessage.timestamp,
1987
- disableGeoip: eventMessage.disableGeoip,
1988
- uuid: eventMessage.uuid
2158
+ options
2159
+ }) => {
2160
+ return super.captureStateless(distinctId, event, properties, {
2161
+ timestamp: options.timestamp,
2162
+ disableGeoip: options.disableGeoip,
2163
+ uuid: options.uuid
1989
2164
  });
1990
- };
1991
- // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
1992
- const capturePromise = Promise.resolve().then(async () => {
1993
- if (sendFeatureFlags) {
1994
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
1995
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
1996
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
1997
- }
1998
- if (event === '$feature_flag_called') {
1999
- // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
2000
- return {};
2001
- }
2002
- return {};
2003
- }).then(flags => {
2004
- // Derive the relevant flag properties to add
2005
- const additionalProperties = {};
2006
- if (flags) {
2007
- for (const [feature, variant] of Object.entries(flags)) {
2008
- additionalProperties[`$feature/${feature}`] = variant;
2009
- }
2165
+ }).catch(err => {
2166
+ if (err) {
2167
+ console.error(err);
2010
2168
  }
2011
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
2012
- if (activeFlags.length > 0) {
2013
- additionalProperties['$active_feature_flags'] = activeFlags;
2014
- }
2015
- return additionalProperties;
2016
- }).catch(() => {
2017
- // Something went wrong getting the flag info - we should capture the event anyways
2018
- return {};
2019
- }).then(additionalProperties => {
2020
- // No matter what - capture the event
2021
- _capture({
2022
- ...additionalProperties,
2023
- ...(eventMessage.properties || {}),
2024
- $groups: eventMessage.groups || groups
2025
- });
2026
- });
2027
- this.addPendingPromise(capturePromise);
2169
+ }));
2028
2170
  }
2029
2171
  async captureImmediate(props) {
2030
2172
  if (typeof props === 'string') {
2031
- this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
2173
+ this.logMsgIfDebug(() => console.warn('Called captureImmediate() with a string as the first argument when an object was expected.'));
2032
2174
  }
2033
- const {
2175
+ return this.addPendingPromise(this.prepareEventMessage(props).then(({
2034
2176
  distinctId,
2035
2177
  event,
2036
2178
  properties,
2037
- groups,
2038
- sendFeatureFlags,
2039
- timestamp,
2040
- disableGeoip,
2041
- uuid
2042
- } = props;
2043
- // Run before_send if configured
2044
- const eventMessage = this._runBeforeSend({
2045
- distinctId,
2046
- event,
2047
- properties,
2048
- groups,
2049
- sendFeatureFlags,
2050
- timestamp,
2051
- disableGeoip,
2052
- uuid
2053
- });
2054
- if (!eventMessage) {
2055
- return;
2056
- }
2057
- const _capture = props => {
2058
- return super.captureStatelessImmediate(eventMessage.distinctId, eventMessage.event, props, {
2059
- timestamp: eventMessage.timestamp,
2060
- disableGeoip: eventMessage.disableGeoip,
2061
- uuid: eventMessage.uuid
2179
+ options
2180
+ }) => {
2181
+ return super.captureStatelessImmediate(distinctId, event, properties, {
2182
+ timestamp: options.timestamp,
2183
+ disableGeoip: options.disableGeoip,
2184
+ uuid: options.uuid
2062
2185
  });
2063
- };
2064
- const capturePromise = Promise.resolve().then(async () => {
2065
- if (sendFeatureFlags) {
2066
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
2067
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
2068
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
2069
- }
2070
- if (event === '$feature_flag_called') {
2071
- // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
2072
- return {};
2073
- }
2074
- return {};
2075
- }).then(flags => {
2076
- // Derive the relevant flag properties to add
2077
- const additionalProperties = {};
2078
- if (flags) {
2079
- for (const [feature, variant] of Object.entries(flags)) {
2080
- additionalProperties[`$feature/${feature}`] = variant;
2081
- }
2186
+ }).catch(err => {
2187
+ if (err) {
2188
+ console.error(err);
2082
2189
  }
2083
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
2084
- if (activeFlags.length > 0) {
2085
- additionalProperties['$active_feature_flags'] = activeFlags;
2086
- }
2087
- return additionalProperties;
2088
- }).catch(() => {
2089
- // Something went wrong getting the flag info - we should capture the event anyways
2090
- return {};
2091
- }).then(additionalProperties => {
2092
- // No matter what - capture the event
2093
- _capture({
2094
- ...additionalProperties,
2095
- ...(eventMessage.properties || {}),
2096
- $groups: eventMessage.groups || groups
2097
- });
2098
- });
2099
- await capturePromise;
2190
+ }));
2100
2191
  }
2101
2192
  identify({
2102
2193
  distinctId,
@@ -2366,6 +2457,7 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
2366
2457
  }
2367
2458
  async _shutdown(shutdownTimeoutMs) {
2368
2459
  this.featureFlagsPoller?.stopPoller();
2460
+ this.errorTracking.shutdown();
2369
2461
  return super._shutdown(shutdownTimeoutMs);
2370
2462
  }
2371
2463
  async _requestRemoteConfigPayload(flagKey) {
@@ -2493,18 +2585,88 @@ class PostHogBackendClient extends core.PostHogCoreStateless {
2493
2585
  }
2494
2586
  captureException(error, distinctId, additionalProperties) {
2495
2587
  const syntheticException = new Error('PostHog syntheticException');
2496
- ErrorTracking.buildEventMessage(error, {
2588
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2497
2589
  syntheticException
2498
- }, distinctId, additionalProperties).then(msg => {
2499
- this.capture(msg);
2500
- });
2590
+ }, distinctId, additionalProperties).then(msg => this.capture(msg)));
2501
2591
  }
2502
2592
  async captureExceptionImmediate(error, distinctId, additionalProperties) {
2503
2593
  const syntheticException = new Error('PostHog syntheticException');
2504
- const evtMsg = await ErrorTracking.buildEventMessage(error, {
2594
+ this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
2505
2595
  syntheticException
2506
- }, distinctId, additionalProperties);
2507
- return await this.captureImmediate(evtMsg);
2596
+ }, distinctId, additionalProperties).then(msg => this.captureImmediate(msg)));
2597
+ }
2598
+ async prepareEventMessage(props) {
2599
+ const {
2600
+ distinctId,
2601
+ event,
2602
+ properties,
2603
+ groups,
2604
+ sendFeatureFlags,
2605
+ timestamp,
2606
+ disableGeoip,
2607
+ uuid
2608
+ } = props;
2609
+ // Run before_send if configured
2610
+ const eventMessage = this._runBeforeSend({
2611
+ distinctId,
2612
+ event,
2613
+ properties,
2614
+ groups,
2615
+ sendFeatureFlags,
2616
+ timestamp,
2617
+ disableGeoip,
2618
+ uuid
2619
+ });
2620
+ if (!eventMessage) {
2621
+ return Promise.reject(null);
2622
+ }
2623
+ // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
2624
+ const eventProperties = await Promise.resolve().then(async () => {
2625
+ if (sendFeatureFlags) {
2626
+ // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
2627
+ const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
2628
+ return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
2629
+ }
2630
+ if (event === '$feature_flag_called') {
2631
+ // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
2632
+ return {};
2633
+ }
2634
+ return {};
2635
+ }).then(flags => {
2636
+ // Derive the relevant flag properties to add
2637
+ const additionalProperties = {};
2638
+ if (flags) {
2639
+ for (const [feature, variant] of Object.entries(flags)) {
2640
+ additionalProperties[`$feature/${feature}`] = variant;
2641
+ }
2642
+ }
2643
+ const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
2644
+ if (activeFlags.length > 0) {
2645
+ additionalProperties['$active_feature_flags'] = activeFlags;
2646
+ }
2647
+ return additionalProperties;
2648
+ }).catch(() => {
2649
+ // Something went wrong getting the flag info - we should capture the event anyways
2650
+ return {};
2651
+ }).then(additionalProperties => {
2652
+ // No matter what - capture the event
2653
+ const props = {
2654
+ ...additionalProperties,
2655
+ ...(eventMessage.properties || {}),
2656
+ $groups: eventMessage.groups || groups
2657
+ };
2658
+ return props;
2659
+ });
2660
+ return {
2661
+ distinctId: eventMessage.distinctId,
2662
+ event: eventMessage.event,
2663
+ properties: eventProperties,
2664
+ options: {
2665
+ timestamp: eventMessage.timestamp,
2666
+ disableGeoip: eventMessage.disableGeoip,
2667
+ uuid: eventMessage.uuid
2668
+ }
2669
+ };
2508
2670
  }
2509
2671
  _runBeforeSend(eventMessage) {
2510
2672
  const beforeSend = this.options.before_send;