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/lib/index.d.ts CHANGED
@@ -441,12 +441,19 @@ declare abstract class PostHogCoreStateless {
441
441
  *** TRACKING
442
442
  ***/
443
443
  protected identifyStateless(distinctId: string, properties?: PostHogEventProperties, options?: PostHogCaptureOptions): void;
444
+ protected identifyStatelessImmediate(distinctId: string, properties?: PostHogEventProperties, options?: PostHogCaptureOptions): Promise<void>;
444
445
  protected captureStateless(distinctId: string, event: string, properties?: {
445
446
  [key: string]: any;
446
447
  }, options?: PostHogCaptureOptions): void;
448
+ protected captureStatelessImmediate(distinctId: string, event: string, properties?: {
449
+ [key: string]: any;
450
+ }, options?: PostHogCaptureOptions): Promise<void>;
447
451
  protected aliasStateless(alias: string, distinctId: string, properties?: {
448
452
  [key: string]: any;
449
453
  }, options?: PostHogCaptureOptions): void;
454
+ protected aliasStatelessImmediate(alias: string, distinctId: string, properties?: {
455
+ [key: string]: any;
456
+ }, options?: PostHogCaptureOptions): Promise<void>;
450
457
  /***
451
458
  *** GROUPS
452
459
  ***/
@@ -495,6 +502,8 @@ declare abstract class PostHogCoreStateless {
495
502
  *** QUEUEING AND FLUSHING
496
503
  ***/
497
504
  protected enqueue(type: string, _message: any, options?: PostHogCaptureOptions): void;
505
+ protected sendImmediate(type: string, _message: any, options?: PostHogCaptureOptions): Promise<void>;
506
+ private prepareMessage;
498
507
  private clearFlushTimer;
499
508
  /**
500
509
  * Helper for flushing the queue in the background
@@ -542,6 +551,15 @@ type PostHogNodeV1 = {
542
551
  * @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event.
543
552
  */
544
553
  capture({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessage): void;
554
+ /**
555
+ * @description Capture an event immediately. Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
556
+ * @param distinctId which uniquely identifies your user
557
+ * @param event We recommend using [verb] [noun], like movie played or movie updated to easily identify what your events mean later on.
558
+ * @param properties OPTIONAL | which can be a object with any information you'd like to add
559
+ * @param groups OPTIONAL | object of what groups are related to this event, example: { company: 'id:5' }. Can be used to analyze companies instead of users.
560
+ * @param sendFeatureFlags OPTIONAL | Used with experiments. Determines whether to send feature flag values with the event.
561
+ */
562
+ captureImmediate({ distinctId, event, properties, groups, sendFeatureFlags }: EventMessage): Promise<void>;
545
563
  /**
546
564
  * @description Identify lets you add metadata on your users so you can more easily identify who they are in PostHog,
547
565
  * and even do things like segment users by these properties.
@@ -550,6 +568,13 @@ type PostHogNodeV1 = {
550
568
  * @param properties with a dict with any key: value pairs
551
569
  */
552
570
  identify({ distinctId, properties }: IdentifyMessage): void;
571
+ /**
572
+ * @description Identify lets you add metadata on your users so you can more easily identify who they are in PostHog.
573
+ * Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
574
+ * @param distinctId which uniquely identifies your user
575
+ * @param properties with a dict with any key: value pairs
576
+ */
577
+ identifyImmediate({ distinctId, properties }: IdentifyMessage): Promise<void>;
553
578
  /**
554
579
  * @description To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call.
555
580
  * This will allow you to answer questions like "Which marketing channels leads to users churning after a month?"
@@ -565,6 +590,16 @@ type PostHogNodeV1 = {
565
590
  distinctId: string;
566
591
  alias: string;
567
592
  }): void;
593
+ /**
594
+ * @description To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call.
595
+ * Useful for edge environments where the usual queue-based sending is not preferable. Do not mix immediate and non-immediate calls.
596
+ * @param distinctId the current unique id
597
+ * @param alias the unique ID of the user before
598
+ */
599
+ aliasImmediate(data: {
600
+ distinctId: string;
601
+ alias: string;
602
+ }): Promise<void>;
568
603
  /**
569
604
  * @description PostHog feature flags (https://posthog.com/docs/features/feature-flags)
570
605
  * allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog,
@@ -726,12 +761,19 @@ declare class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
726
761
  disable(): Promise<void>;
727
762
  debug(enabled?: boolean): void;
728
763
  capture(props: EventMessage): void;
764
+ captureImmediate(props: EventMessage): Promise<void>;
729
765
  identify({ distinctId, properties, disableGeoip }: IdentifyMessage): void;
766
+ identifyImmediate({ distinctId, properties, disableGeoip }: IdentifyMessage): Promise<void>;
730
767
  alias(data: {
731
768
  distinctId: string;
732
769
  alias: string;
733
770
  disableGeoip?: boolean;
734
771
  }): void;
772
+ aliasImmediate(data: {
773
+ distinctId: string;
774
+ alias: string;
775
+ disableGeoip?: boolean;
776
+ }): Promise<void>;
735
777
  isLocalEvaluationReady(): boolean;
736
778
  waitForLocalEvaluationReady(timeoutMs?: number): Promise<boolean>;
737
779
  getFeatureFlag(key: string, distinctId: string, options?: {
package/lib/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { posix, dirname, sep } from 'path';
2
2
 
3
- var version = "4.14.0";
3
+ var version = "4.15.0";
4
4
 
5
5
  var PostHogPersistedProperty;
6
6
  (function (PostHogPersistedProperty) {
@@ -256,6 +256,7 @@ const NEW_FLAGS_EXCLUDED_HASHES = new Set([
256
256
  'fc80b8e2',
257
257
  '75cc0998',
258
258
  ]);
259
+ const STRING_FORMAT = 'utf8';
259
260
  function assert(truthyValue, message) {
260
261
  if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
261
262
  throw new Error(message);
@@ -1194,11 +1195,21 @@ const uuidv7 = () => uuidv7obj().toString();
1194
1195
  const uuidv7obj = () => (defaultGenerator || (defaultGenerator = new V7Generator())).generate();
1195
1196
 
1196
1197
  class PostHogFetchHttpError extends Error {
1197
- constructor(response) {
1198
- super('HTTP error while fetching PostHog: ' + response.status);
1198
+ constructor(response, reqByteLength) {
1199
+ super('HTTP error while fetching PostHog: status=' + response.status + ', reqByteLength=' + reqByteLength);
1199
1200
  this.response = response;
1201
+ this.reqByteLength = reqByteLength;
1200
1202
  this.name = 'PostHogFetchHttpError';
1201
1203
  }
1204
+ get status() {
1205
+ return this.response.status;
1206
+ }
1207
+ get text() {
1208
+ return this.response.text();
1209
+ }
1210
+ get json() {
1211
+ return this.response.json();
1212
+ }
1202
1213
  }
1203
1214
  class PostHogFetchNetworkError extends Error {
1204
1215
  constructor(error) {
@@ -1210,6 +1221,20 @@ class PostHogFetchNetworkError extends Error {
1210
1221
  this.name = 'PostHogFetchNetworkError';
1211
1222
  }
1212
1223
  }
1224
+ async function logFlushError(err) {
1225
+ if (err instanceof PostHogFetchHttpError) {
1226
+ let text = '';
1227
+ try {
1228
+ text = await err.text;
1229
+ }
1230
+ catch { }
1231
+ console.error(`Error while flushing PostHog: message=${err.message}, response body=${text}`, err);
1232
+ }
1233
+ else {
1234
+ console.error('Error while flushing PostHog', err);
1235
+ }
1236
+ return Promise.resolve();
1237
+ }
1213
1238
  function isPostHogFetchError(err) {
1214
1239
  return typeof err === 'object' && (err instanceof PostHogFetchHttpError || err instanceof PostHogFetchNetworkError);
1215
1240
  }
@@ -1343,12 +1368,26 @@ class PostHogCoreStateless {
1343
1368
  this.enqueue('identify', payload, options);
1344
1369
  });
1345
1370
  }
1371
+ async identifyStatelessImmediate(distinctId, properties, options) {
1372
+ const payload = {
1373
+ ...this.buildPayload({
1374
+ distinct_id: distinctId,
1375
+ event: '$identify',
1376
+ properties,
1377
+ }),
1378
+ };
1379
+ await this.sendImmediate('identify', payload, options);
1380
+ }
1346
1381
  captureStateless(distinctId, event, properties, options) {
1347
1382
  this.wrap(() => {
1348
1383
  const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
1349
1384
  this.enqueue('capture', payload, options);
1350
1385
  });
1351
1386
  }
1387
+ async captureStatelessImmediate(distinctId, event, properties, options) {
1388
+ const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
1389
+ await this.sendImmediate('capture', payload, options);
1390
+ }
1352
1391
  aliasStateless(alias, distinctId, properties, options) {
1353
1392
  this.wrap(() => {
1354
1393
  const payload = this.buildPayload({
@@ -1363,6 +1402,18 @@ class PostHogCoreStateless {
1363
1402
  this.enqueue('alias', payload, options);
1364
1403
  });
1365
1404
  }
1405
+ async aliasStatelessImmediate(alias, distinctId, properties, options) {
1406
+ const payload = this.buildPayload({
1407
+ event: '$create_alias',
1408
+ distinct_id: distinctId,
1409
+ properties: {
1410
+ ...(properties || {}),
1411
+ distinct_id: distinctId,
1412
+ alias,
1413
+ },
1414
+ });
1415
+ await this.sendImmediate('alias', payload, options);
1416
+ }
1366
1417
  /***
1367
1418
  *** GROUPS
1368
1419
  ***/
@@ -1606,25 +1657,7 @@ class PostHogCoreStateless {
1606
1657
  this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
1607
1658
  return;
1608
1659
  }
1609
- const message = {
1610
- ..._message,
1611
- type: type,
1612
- library: this.getLibraryId(),
1613
- library_version: this.getLibraryVersion(),
1614
- timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
1615
- uuid: options?.uuid ? options.uuid : uuidv7(),
1616
- };
1617
- const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
1618
- if (addGeoipDisableProperty) {
1619
- if (!message.properties) {
1620
- message.properties = {};
1621
- }
1622
- message['properties']['$geoip_disable'] = true;
1623
- }
1624
- if (message.distinctId) {
1625
- message.distinct_id = message.distinctId;
1626
- delete message.distinctId;
1627
- }
1660
+ const message = this.prepareMessage(type, _message, options);
1628
1661
  const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1629
1662
  if (queue.length >= this.maxQueueSize) {
1630
1663
  queue.shift();
@@ -1642,6 +1675,73 @@ class PostHogCoreStateless {
1642
1675
  }
1643
1676
  });
1644
1677
  }
1678
+ async sendImmediate(type, _message, options) {
1679
+ if (this.disabled) {
1680
+ this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'));
1681
+ return;
1682
+ }
1683
+ if (!this._isInitialized) {
1684
+ await this._initPromise;
1685
+ }
1686
+ if (this.optedOut) {
1687
+ this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
1688
+ return;
1689
+ }
1690
+ const data = {
1691
+ api_key: this.apiKey,
1692
+ batch: [this.prepareMessage(type, _message, options)],
1693
+ sent_at: currentISOTime(),
1694
+ };
1695
+ if (this.historicalMigration) {
1696
+ data.historical_migration = true;
1697
+ }
1698
+ const payload = JSON.stringify(data);
1699
+ const url = this.captureMode === 'form'
1700
+ ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1701
+ : `${this.host}/batch/`;
1702
+ const fetchOptions = this.captureMode === 'form'
1703
+ ? {
1704
+ method: 'POST',
1705
+ mode: 'no-cors',
1706
+ credentials: 'omit',
1707
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
1708
+ body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
1709
+ }
1710
+ : {
1711
+ method: 'POST',
1712
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1713
+ body: payload,
1714
+ };
1715
+ try {
1716
+ await this.fetchWithRetry(url, fetchOptions);
1717
+ }
1718
+ catch (err) {
1719
+ this._events.emit('error', err);
1720
+ throw err;
1721
+ }
1722
+ }
1723
+ prepareMessage(type, _message, options) {
1724
+ const message = {
1725
+ ..._message,
1726
+ type: type,
1727
+ library: this.getLibraryId(),
1728
+ library_version: this.getLibraryVersion(),
1729
+ timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
1730
+ uuid: options?.uuid ? options.uuid : uuidv7(),
1731
+ };
1732
+ const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
1733
+ if (addGeoipDisableProperty) {
1734
+ if (!message.properties) {
1735
+ message.properties = {};
1736
+ }
1737
+ message['properties']['$geoip_disable'] = true;
1738
+ }
1739
+ if (message.distinctId) {
1740
+ message.distinct_id = message.distinctId;
1741
+ delete message.distinctId;
1742
+ }
1743
+ return message;
1744
+ }
1645
1745
  clearFlushTimer() {
1646
1746
  if (this._flushTimer) {
1647
1747
  clearTimeout(this._flushTimer);
@@ -1653,7 +1753,9 @@ class PostHogCoreStateless {
1653
1753
  * Avoids unnecessary promise errors
1654
1754
  */
1655
1755
  flushBackground() {
1656
- void this.flush().catch(() => { });
1756
+ void this.flush().catch(async (err) => {
1757
+ await logFlushError(err);
1758
+ });
1657
1759
  }
1658
1760
  async flush() {
1659
1761
  if (!this.flushPromise) {
@@ -1737,6 +1839,8 @@ class PostHogCoreStateless {
1737
1839
  setTimeout(() => ctrl.abort(), ms);
1738
1840
  return ctrl.signal;
1739
1841
  });
1842
+ const body = options.body ? options.body : '';
1843
+ const reqByteLength = Buffer.byteLength(body, STRING_FORMAT);
1740
1844
  return await retriable(async () => {
1741
1845
  let res = null;
1742
1846
  try {
@@ -1754,7 +1858,7 @@ class PostHogCoreStateless {
1754
1858
  // https://developer.mozilla.org/en-US/docs/Web/API/Request/mode#no-cors
1755
1859
  const isNoCors = options.mode === 'no-cors';
1756
1860
  if (!isNoCors && (res.status < 200 || res.status >= 400)) {
1757
- throw new PostHogFetchHttpError(res);
1861
+ throw new PostHogFetchHttpError(res, reqByteLength);
1758
1862
  }
1759
1863
  return res;
1760
1864
  }, { ...this._retryOptions, ...retryOptions });
@@ -1786,7 +1890,7 @@ class PostHogCoreStateless {
1786
1890
  if (!isPostHogFetchError(e)) {
1787
1891
  throw e;
1788
1892
  }
1789
- this.logMsgIfDebug(() => console.error('Error while shutting down PostHog', e));
1893
+ await logFlushError(e);
1790
1894
  }
1791
1895
  };
1792
1896
  return Promise.race([
@@ -3667,6 +3771,79 @@ class PostHog extends PostHogCoreStateless {
3667
3771
  });
3668
3772
  this.addPendingPromise(capturePromise);
3669
3773
  }
3774
+ async captureImmediate(props) {
3775
+ if (typeof props === 'string') {
3776
+ this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
3777
+ }
3778
+ const {
3779
+ distinctId,
3780
+ event,
3781
+ properties,
3782
+ groups,
3783
+ sendFeatureFlags,
3784
+ timestamp,
3785
+ disableGeoip,
3786
+ uuid
3787
+ } = props;
3788
+ const _capture = props => {
3789
+ return super.captureStatelessImmediate(distinctId, event, props, {
3790
+ timestamp,
3791
+ disableGeoip,
3792
+ uuid
3793
+ });
3794
+ };
3795
+ const _getFlags = async (distinctId, groups, disableGeoip) => {
3796
+ return (await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)).flags;
3797
+ };
3798
+ const capturePromise = Promise.resolve().then(async () => {
3799
+ if (sendFeatureFlags) {
3800
+ // If we are sending feature flags, we need to make sure we have the latest flags
3801
+ // return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
3802
+ return await _getFlags(distinctId, groups, disableGeoip);
3803
+ }
3804
+ if (event === '$feature_flag_called') {
3805
+ // 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.
3806
+ return {};
3807
+ }
3808
+ if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
3809
+ // Otherwise we may as well check for the flags locally and include them if they are already loaded
3810
+ const groupsWithStringValues = {};
3811
+ for (const [key, value] of Object.entries(groups || {})) {
3812
+ groupsWithStringValues[key] = String(value);
3813
+ }
3814
+ return await this.getAllFlags(distinctId, {
3815
+ groups: groupsWithStringValues,
3816
+ disableGeoip,
3817
+ onlyEvaluateLocally: true
3818
+ });
3819
+ }
3820
+ return {};
3821
+ }).then(flags => {
3822
+ // Derive the relevant flag properties to add
3823
+ const additionalProperties = {};
3824
+ if (flags) {
3825
+ for (const [feature, variant] of Object.entries(flags)) {
3826
+ additionalProperties[`$feature/${feature}`] = variant;
3827
+ }
3828
+ }
3829
+ const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
3830
+ if (activeFlags.length > 0) {
3831
+ additionalProperties['$active_feature_flags'] = activeFlags;
3832
+ }
3833
+ return additionalProperties;
3834
+ }).catch(() => {
3835
+ // Something went wrong getting the flag info - we should capture the event anyways
3836
+ return {};
3837
+ }).then(additionalProperties => {
3838
+ // No matter what - capture the event
3839
+ _capture({
3840
+ ...additionalProperties,
3841
+ ...properties,
3842
+ $groups: groups
3843
+ });
3844
+ });
3845
+ await capturePromise;
3846
+ }
3670
3847
  identify({
3671
3848
  distinctId,
3672
3849
  properties,
@@ -3685,11 +3862,33 @@ class PostHog extends PostHogCoreStateless {
3685
3862
  disableGeoip
3686
3863
  });
3687
3864
  }
3865
+ async identifyImmediate({
3866
+ distinctId,
3867
+ properties,
3868
+ disableGeoip
3869
+ }) {
3870
+ // promote $set and $set_once to top level
3871
+ const userPropsOnce = properties?.$set_once;
3872
+ delete properties?.$set_once;
3873
+ // if no $set is provided we assume all properties are $set
3874
+ const userProps = properties?.$set || properties;
3875
+ await super.identifyStatelessImmediate(distinctId, {
3876
+ $set: userProps,
3877
+ $set_once: userPropsOnce
3878
+ }, {
3879
+ disableGeoip
3880
+ });
3881
+ }
3688
3882
  alias(data) {
3689
3883
  super.aliasStateless(data.alias, data.distinctId, undefined, {
3690
3884
  disableGeoip: data.disableGeoip
3691
3885
  });
3692
3886
  }
3887
+ async aliasImmediate(data) {
3888
+ await super.aliasStatelessImmediate(data.alias, data.distinctId, undefined, {
3889
+ disableGeoip: data.disableGeoip
3890
+ });
3891
+ }
3693
3892
  isLocalEvaluationReady() {
3694
3893
  return this.featureFlagsPoller?.isLocalEvaluationReady() ?? false;
3695
3894
  }