posthog-node 4.14.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,8 @@
1
+ # 4.15.0 - 2025-04-30
2
+
3
+ 1. chore: add immediate-mode
4
+ 2. chore: better error logging when flushing events
5
+
1
6
  # 4.14.0 - 2025-04-24
2
7
 
3
8
  1. feat: Add super properties as a concept to the Node SDK
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.14.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);
@@ -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
  ***/
@@ -1628,25 +1679,7 @@ class PostHogCoreStateless {
1628
1679
  this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
1629
1680
  return;
1630
1681
  }
1631
- const message = {
1632
- ..._message,
1633
- type: type,
1634
- library: this.getLibraryId(),
1635
- library_version: this.getLibraryVersion(),
1636
- timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
1637
- uuid: options?.uuid ? options.uuid : uuidv7(),
1638
- };
1639
- const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
1640
- if (addGeoipDisableProperty) {
1641
- if (!message.properties) {
1642
- message.properties = {};
1643
- }
1644
- message['properties']['$geoip_disable'] = true;
1645
- }
1646
- if (message.distinctId) {
1647
- message.distinct_id = message.distinctId;
1648
- delete message.distinctId;
1649
- }
1682
+ const message = this.prepareMessage(type, _message, options);
1650
1683
  const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1651
1684
  if (queue.length >= this.maxQueueSize) {
1652
1685
  queue.shift();
@@ -1664,6 +1697,73 @@ class PostHogCoreStateless {
1664
1697
  }
1665
1698
  });
1666
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
+ }
1667
1767
  clearFlushTimer() {
1668
1768
  if (this._flushTimer) {
1669
1769
  clearTimeout(this._flushTimer);
@@ -1675,7 +1775,9 @@ class PostHogCoreStateless {
1675
1775
  * Avoids unnecessary promise errors
1676
1776
  */
1677
1777
  flushBackground() {
1678
- void this.flush().catch(() => { });
1778
+ void this.flush().catch(async (err) => {
1779
+ await logFlushError(err);
1780
+ });
1679
1781
  }
1680
1782
  async flush() {
1681
1783
  if (!this.flushPromise) {
@@ -1759,6 +1861,8 @@ class PostHogCoreStateless {
1759
1861
  setTimeout(() => ctrl.abort(), ms);
1760
1862
  return ctrl.signal;
1761
1863
  });
1864
+ const body = options.body ? options.body : '';
1865
+ const reqByteLength = Buffer.byteLength(body, STRING_FORMAT);
1762
1866
  return await retriable(async () => {
1763
1867
  let res = null;
1764
1868
  try {
@@ -1776,7 +1880,7 @@ class PostHogCoreStateless {
1776
1880
  // https://developer.mozilla.org/en-US/docs/Web/API/Request/mode#no-cors
1777
1881
  const isNoCors = options.mode === 'no-cors';
1778
1882
  if (!isNoCors && (res.status < 200 || res.status >= 400)) {
1779
- throw new PostHogFetchHttpError(res);
1883
+ throw new PostHogFetchHttpError(res, reqByteLength);
1780
1884
  }
1781
1885
  return res;
1782
1886
  }, { ...this._retryOptions, ...retryOptions });
@@ -1808,7 +1912,7 @@ class PostHogCoreStateless {
1808
1912
  if (!isPostHogFetchError(e)) {
1809
1913
  throw e;
1810
1914
  }
1811
- this.logMsgIfDebug(() => console.error('Error while shutting down PostHog', e));
1915
+ await logFlushError(e);
1812
1916
  }
1813
1917
  };
1814
1918
  return Promise.race([
@@ -3689,6 +3793,79 @@ class PostHog extends PostHogCoreStateless {
3689
3793
  });
3690
3794
  this.addPendingPromise(capturePromise);
3691
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
+ }
3692
3869
  identify({
3693
3870
  distinctId,
3694
3871
  properties,
@@ -3707,11 +3884,33 @@ class PostHog extends PostHogCoreStateless {
3707
3884
  disableGeoip
3708
3885
  });
3709
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
+ }
3710
3904
  alias(data) {
3711
3905
  super.aliasStateless(data.alias, data.distinctId, undefined, {
3712
3906
  disableGeoip: data.disableGeoip
3713
3907
  });
3714
3908
  }
3909
+ async aliasImmediate(data) {
3910
+ await super.aliasStatelessImmediate(data.alias, data.distinctId, undefined, {
3911
+ disableGeoip: data.disableGeoip
3912
+ });
3913
+ }
3715
3914
  isLocalEvaluationReady() {
3716
3915
  return this.featureFlagsPoller?.isLocalEvaluationReady() ?? false;
3717
3916
  }