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