posthog-node 4.13.0 → 4.15.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/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 4.15.0 - 2025-04-30
2
+
3
+ 1. chore: add immediate-mode
4
+ 2. chore: better error logging when flushing events
5
+
6
+ # 4.14.0 - 2025-04-24
7
+
8
+ 1. feat: Add super properties as a concept to the Node SDK
9
+
1
10
  # 4.13.0 - 2025-04-21
2
11
 
3
12
  1. feat: Add method to wait for local evaluation feature flag definitions to be loaded
package/lib/index.cjs.js CHANGED
@@ -22,7 +22,7 @@ function _interopNamespace(e) {
22
22
  return Object.freeze(n);
23
23
  }
24
24
 
25
- var version = "4.13.0";
25
+ var version = "4.15.0";
26
26
 
27
27
  var PostHogPersistedProperty;
28
28
  (function (PostHogPersistedProperty) {
@@ -278,6 +278,7 @@ const NEW_FLAGS_EXCLUDED_HASHES = new Set([
278
278
  'fc80b8e2',
279
279
  '75cc0998',
280
280
  ]);
281
+ const STRING_FORMAT = 'utf8';
281
282
  function assert(truthyValue, message) {
282
283
  if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
283
284
  throw new Error(message);
@@ -327,7 +328,7 @@ function safeSetTimeout(fn, timeout) {
327
328
  return t;
328
329
  }
329
330
  function getFetch() {
330
- return typeof fetch !== 'undefined' ? fetch : typeof global.fetch !== 'undefined' ? global.fetch : undefined;
331
+ return typeof fetch !== 'undefined' ? fetch : typeof globalThis.fetch !== 'undefined' ? globalThis.fetch : undefined;
331
332
  }
332
333
  // FNV-1a hash function
333
334
  // https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
@@ -1216,11 +1217,21 @@ const uuidv7 = () => uuidv7obj().toString();
1216
1217
  const uuidv7obj = () => (defaultGenerator || (defaultGenerator = new V7Generator())).generate();
1217
1218
 
1218
1219
  class PostHogFetchHttpError extends Error {
1219
- constructor(response) {
1220
- super('HTTP error while fetching PostHog: ' + response.status);
1220
+ constructor(response, reqByteLength) {
1221
+ super('HTTP error while fetching PostHog: status=' + response.status + ', reqByteLength=' + reqByteLength);
1221
1222
  this.response = response;
1223
+ this.reqByteLength = reqByteLength;
1222
1224
  this.name = 'PostHogFetchHttpError';
1223
1225
  }
1226
+ get status() {
1227
+ return this.response.status;
1228
+ }
1229
+ get text() {
1230
+ return this.response.text();
1231
+ }
1232
+ get json() {
1233
+ return this.response.json();
1234
+ }
1224
1235
  }
1225
1236
  class PostHogFetchNetworkError extends Error {
1226
1237
  constructor(error) {
@@ -1232,6 +1243,20 @@ class PostHogFetchNetworkError extends Error {
1232
1243
  this.name = 'PostHogFetchNetworkError';
1233
1244
  }
1234
1245
  }
1246
+ async function logFlushError(err) {
1247
+ if (err instanceof PostHogFetchHttpError) {
1248
+ let text = '';
1249
+ try {
1250
+ text = await err.text;
1251
+ }
1252
+ catch { }
1253
+ console.error(`Error while flushing PostHog: message=${err.message}, response body=${text}`, err);
1254
+ }
1255
+ else {
1256
+ console.error('Error while flushing PostHog', err);
1257
+ }
1258
+ return Promise.resolve();
1259
+ }
1235
1260
  function isPostHogFetchError(err) {
1236
1261
  return typeof err === 'object' && (err instanceof PostHogFetchHttpError || err instanceof PostHogFetchNetworkError);
1237
1262
  }
@@ -1365,12 +1390,26 @@ class PostHogCoreStateless {
1365
1390
  this.enqueue('identify', payload, options);
1366
1391
  });
1367
1392
  }
1393
+ async identifyStatelessImmediate(distinctId, properties, options) {
1394
+ const payload = {
1395
+ ...this.buildPayload({
1396
+ distinct_id: distinctId,
1397
+ event: '$identify',
1398
+ properties,
1399
+ }),
1400
+ };
1401
+ await this.sendImmediate('identify', payload, options);
1402
+ }
1368
1403
  captureStateless(distinctId, event, properties, options) {
1369
1404
  this.wrap(() => {
1370
1405
  const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
1371
1406
  this.enqueue('capture', payload, options);
1372
1407
  });
1373
1408
  }
1409
+ async captureStatelessImmediate(distinctId, event, properties, options) {
1410
+ const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
1411
+ await this.sendImmediate('capture', payload, options);
1412
+ }
1374
1413
  aliasStateless(alias, distinctId, properties, options) {
1375
1414
  this.wrap(() => {
1376
1415
  const payload = this.buildPayload({
@@ -1385,6 +1424,18 @@ class PostHogCoreStateless {
1385
1424
  this.enqueue('alias', payload, options);
1386
1425
  });
1387
1426
  }
1427
+ async aliasStatelessImmediate(alias, distinctId, properties, options) {
1428
+ const payload = this.buildPayload({
1429
+ event: '$create_alias',
1430
+ distinct_id: distinctId,
1431
+ properties: {
1432
+ ...(properties || {}),
1433
+ distinct_id: distinctId,
1434
+ alias,
1435
+ },
1436
+ });
1437
+ await this.sendImmediate('alias', payload, options);
1438
+ }
1388
1439
  /***
1389
1440
  *** GROUPS
1390
1441
  ***/
@@ -1595,6 +1646,30 @@ class PostHogCoreStateless {
1595
1646
  }
1596
1647
  return newSurveys ?? [];
1597
1648
  }
1649
+ get props() {
1650
+ if (!this._props) {
1651
+ this._props = this.getPersistedProperty(PostHogPersistedProperty.Props);
1652
+ }
1653
+ return this._props || {};
1654
+ }
1655
+ set props(val) {
1656
+ this._props = val;
1657
+ }
1658
+ async register(properties) {
1659
+ this.wrap(() => {
1660
+ this.props = {
1661
+ ...this.props,
1662
+ ...properties,
1663
+ };
1664
+ this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
1665
+ });
1666
+ }
1667
+ async unregister(property) {
1668
+ this.wrap(() => {
1669
+ delete this.props[property];
1670
+ this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
1671
+ });
1672
+ }
1598
1673
  /***
1599
1674
  *** QUEUEING AND FLUSHING
1600
1675
  ***/
@@ -1604,25 +1679,7 @@ class PostHogCoreStateless {
1604
1679
  this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
1605
1680
  return;
1606
1681
  }
1607
- const message = {
1608
- ..._message,
1609
- type: type,
1610
- library: this.getLibraryId(),
1611
- library_version: this.getLibraryVersion(),
1612
- timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
1613
- uuid: options?.uuid ? options.uuid : uuidv7(),
1614
- };
1615
- const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
1616
- if (addGeoipDisableProperty) {
1617
- if (!message.properties) {
1618
- message.properties = {};
1619
- }
1620
- message['properties']['$geoip_disable'] = true;
1621
- }
1622
- if (message.distinctId) {
1623
- message.distinct_id = message.distinctId;
1624
- delete message.distinctId;
1625
- }
1682
+ const message = this.prepareMessage(type, _message, options);
1626
1683
  const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1627
1684
  if (queue.length >= this.maxQueueSize) {
1628
1685
  queue.shift();
@@ -1640,6 +1697,73 @@ class PostHogCoreStateless {
1640
1697
  }
1641
1698
  });
1642
1699
  }
1700
+ async sendImmediate(type, _message, options) {
1701
+ if (this.disabled) {
1702
+ this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'));
1703
+ return;
1704
+ }
1705
+ if (!this._isInitialized) {
1706
+ await this._initPromise;
1707
+ }
1708
+ if (this.optedOut) {
1709
+ this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
1710
+ return;
1711
+ }
1712
+ const data = {
1713
+ api_key: this.apiKey,
1714
+ batch: [this.prepareMessage(type, _message, options)],
1715
+ sent_at: currentISOTime(),
1716
+ };
1717
+ if (this.historicalMigration) {
1718
+ data.historical_migration = true;
1719
+ }
1720
+ const payload = JSON.stringify(data);
1721
+ const url = this.captureMode === 'form'
1722
+ ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1723
+ : `${this.host}/batch/`;
1724
+ const fetchOptions = this.captureMode === 'form'
1725
+ ? {
1726
+ method: 'POST',
1727
+ mode: 'no-cors',
1728
+ credentials: 'omit',
1729
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
1730
+ body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
1731
+ }
1732
+ : {
1733
+ method: 'POST',
1734
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1735
+ body: payload,
1736
+ };
1737
+ try {
1738
+ await this.fetchWithRetry(url, fetchOptions);
1739
+ }
1740
+ catch (err) {
1741
+ this._events.emit('error', err);
1742
+ throw err;
1743
+ }
1744
+ }
1745
+ prepareMessage(type, _message, options) {
1746
+ const message = {
1747
+ ..._message,
1748
+ type: type,
1749
+ library: this.getLibraryId(),
1750
+ library_version: this.getLibraryVersion(),
1751
+ timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
1752
+ uuid: options?.uuid ? options.uuid : uuidv7(),
1753
+ };
1754
+ const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
1755
+ if (addGeoipDisableProperty) {
1756
+ if (!message.properties) {
1757
+ message.properties = {};
1758
+ }
1759
+ message['properties']['$geoip_disable'] = true;
1760
+ }
1761
+ if (message.distinctId) {
1762
+ message.distinct_id = message.distinctId;
1763
+ delete message.distinctId;
1764
+ }
1765
+ return message;
1766
+ }
1643
1767
  clearFlushTimer() {
1644
1768
  if (this._flushTimer) {
1645
1769
  clearTimeout(this._flushTimer);
@@ -1651,7 +1775,9 @@ class PostHogCoreStateless {
1651
1775
  * Avoids unnecessary promise errors
1652
1776
  */
1653
1777
  flushBackground() {
1654
- void this.flush().catch(() => { });
1778
+ void this.flush().catch(async (err) => {
1779
+ await logFlushError(err);
1780
+ });
1655
1781
  }
1656
1782
  async flush() {
1657
1783
  if (!this.flushPromise) {
@@ -1735,6 +1861,8 @@ class PostHogCoreStateless {
1735
1861
  setTimeout(() => ctrl.abort(), ms);
1736
1862
  return ctrl.signal;
1737
1863
  });
1864
+ const body = options.body ? options.body : '';
1865
+ const reqByteLength = Buffer.byteLength(body, STRING_FORMAT);
1738
1866
  return await retriable(async () => {
1739
1867
  let res = null;
1740
1868
  try {
@@ -1752,7 +1880,7 @@ class PostHogCoreStateless {
1752
1880
  // https://developer.mozilla.org/en-US/docs/Web/API/Request/mode#no-cors
1753
1881
  const isNoCors = options.mode === 'no-cors';
1754
1882
  if (!isNoCors && (res.status < 200 || res.status >= 400)) {
1755
- throw new PostHogFetchHttpError(res);
1883
+ throw new PostHogFetchHttpError(res, reqByteLength);
1756
1884
  }
1757
1885
  return res;
1758
1886
  }, { ...this._retryOptions, ...retryOptions });
@@ -1784,7 +1912,7 @@ class PostHogCoreStateless {
1784
1912
  if (!isPostHogFetchError(e)) {
1785
1913
  throw e;
1786
1914
  }
1787
- this.logMsgIfDebug(() => console.error('Error while shutting down PostHog', e));
1915
+ await logFlushError(e);
1788
1916
  }
1789
1917
  };
1790
1918
  return Promise.race([
@@ -3665,6 +3793,79 @@ class PostHog extends PostHogCoreStateless {
3665
3793
  });
3666
3794
  this.addPendingPromise(capturePromise);
3667
3795
  }
3796
+ async captureImmediate(props) {
3797
+ if (typeof props === 'string') {
3798
+ this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
3799
+ }
3800
+ const {
3801
+ distinctId,
3802
+ event,
3803
+ properties,
3804
+ groups,
3805
+ sendFeatureFlags,
3806
+ timestamp,
3807
+ disableGeoip,
3808
+ uuid
3809
+ } = props;
3810
+ const _capture = props => {
3811
+ return super.captureStatelessImmediate(distinctId, event, props, {
3812
+ timestamp,
3813
+ disableGeoip,
3814
+ uuid
3815
+ });
3816
+ };
3817
+ const _getFlags = async (distinctId, groups, disableGeoip) => {
3818
+ return (await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)).flags;
3819
+ };
3820
+ const capturePromise = Promise.resolve().then(async () => {
3821
+ if (sendFeatureFlags) {
3822
+ // If we are sending feature flags, we need to make sure we have the latest flags
3823
+ // return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
3824
+ return await _getFlags(distinctId, groups, disableGeoip);
3825
+ }
3826
+ if (event === '$feature_flag_called') {
3827
+ // 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.
3828
+ return {};
3829
+ }
3830
+ if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
3831
+ // Otherwise we may as well check for the flags locally and include them if they are already loaded
3832
+ const groupsWithStringValues = {};
3833
+ for (const [key, value] of Object.entries(groups || {})) {
3834
+ groupsWithStringValues[key] = String(value);
3835
+ }
3836
+ return await this.getAllFlags(distinctId, {
3837
+ groups: groupsWithStringValues,
3838
+ disableGeoip,
3839
+ onlyEvaluateLocally: true
3840
+ });
3841
+ }
3842
+ return {};
3843
+ }).then(flags => {
3844
+ // Derive the relevant flag properties to add
3845
+ const additionalProperties = {};
3846
+ if (flags) {
3847
+ for (const [feature, variant] of Object.entries(flags)) {
3848
+ additionalProperties[`$feature/${feature}`] = variant;
3849
+ }
3850
+ }
3851
+ const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
3852
+ if (activeFlags.length > 0) {
3853
+ additionalProperties['$active_feature_flags'] = activeFlags;
3854
+ }
3855
+ return additionalProperties;
3856
+ }).catch(() => {
3857
+ // Something went wrong getting the flag info - we should capture the event anyways
3858
+ return {};
3859
+ }).then(additionalProperties => {
3860
+ // No matter what - capture the event
3861
+ _capture({
3862
+ ...additionalProperties,
3863
+ ...properties,
3864
+ $groups: groups
3865
+ });
3866
+ });
3867
+ await capturePromise;
3868
+ }
3668
3869
  identify({
3669
3870
  distinctId,
3670
3871
  properties,
@@ -3683,11 +3884,33 @@ class PostHog extends PostHogCoreStateless {
3683
3884
  disableGeoip
3684
3885
  });
3685
3886
  }
3887
+ async identifyImmediate({
3888
+ distinctId,
3889
+ properties,
3890
+ disableGeoip
3891
+ }) {
3892
+ // promote $set and $set_once to top level
3893
+ const userPropsOnce = properties?.$set_once;
3894
+ delete properties?.$set_once;
3895
+ // if no $set is provided we assume all properties are $set
3896
+ const userProps = properties?.$set || properties;
3897
+ await super.identifyStatelessImmediate(distinctId, {
3898
+ $set: userProps,
3899
+ $set_once: userPropsOnce
3900
+ }, {
3901
+ disableGeoip
3902
+ });
3903
+ }
3686
3904
  alias(data) {
3687
3905
  super.aliasStateless(data.alias, data.distinctId, undefined, {
3688
3906
  disableGeoip: data.disableGeoip
3689
3907
  });
3690
3908
  }
3909
+ async aliasImmediate(data) {
3910
+ await super.aliasStatelessImmediate(data.alias, data.distinctId, undefined, {
3911
+ disableGeoip: data.disableGeoip
3912
+ });
3913
+ }
3691
3914
  isLocalEvaluationReady() {
3692
3915
  return this.featureFlagsPoller?.isLocalEvaluationReady() ?? false;
3693
3916
  }