posthog-node 4.14.0 → 4.16.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 +9 -0
- package/lib/index.cjs.js +328 -77
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +51 -2
- package/lib/index.esm.js +328 -77
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/index.d.ts +17 -0
- package/lib/posthog-core/src/utils.d.ts +2 -1
- package/lib/posthog-node/src/posthog-node.d.ts +8 -1
- package/lib/posthog-node/src/types.d.ts +26 -0
- package/lib/posthog-node/test/test-utils.d.ts +1 -0
- package/package.json +1 -1
- package/src/posthog-node.ts +102 -2
- package/src/types.ts +26 -0
- package/test/posthog-node.spec.ts +21 -23
- package/test/test-utils.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
# 4.16.0 - 2025-05-01
|
|
2
|
+
|
|
3
|
+
1. chore: improve flush event
|
|
4
|
+
|
|
5
|
+
# 4.15.0 - 2025-04-30
|
|
6
|
+
|
|
7
|
+
1. chore: add immediate-mode
|
|
8
|
+
2. chore: better error logging when flushing events
|
|
9
|
+
|
|
1
10
|
# 4.14.0 - 2025-04-24
|
|
2
11
|
|
|
3
12
|
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.
|
|
25
|
+
var version = "4.16.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,9 +1243,26 @@ 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
|
}
|
|
1263
|
+
function isPostHogFetchContentTooLargeError(err) {
|
|
1264
|
+
return typeof err === 'object' && err instanceof PostHogFetchHttpError && err.status === 413;
|
|
1265
|
+
}
|
|
1238
1266
|
var QuotaLimitedFeature;
|
|
1239
1267
|
(function (QuotaLimitedFeature) {
|
|
1240
1268
|
QuotaLimitedFeature["FeatureFlags"] = "feature_flags";
|
|
@@ -1243,6 +1271,7 @@ var QuotaLimitedFeature;
|
|
|
1243
1271
|
class PostHogCoreStateless {
|
|
1244
1272
|
constructor(apiKey, options) {
|
|
1245
1273
|
this.flushPromise = null;
|
|
1274
|
+
this.shutdownPromise = null;
|
|
1246
1275
|
this.pendingPromises = {};
|
|
1247
1276
|
// internal
|
|
1248
1277
|
this._events = new SimpleEventEmitter();
|
|
@@ -1365,12 +1394,26 @@ class PostHogCoreStateless {
|
|
|
1365
1394
|
this.enqueue('identify', payload, options);
|
|
1366
1395
|
});
|
|
1367
1396
|
}
|
|
1397
|
+
async identifyStatelessImmediate(distinctId, properties, options) {
|
|
1398
|
+
const payload = {
|
|
1399
|
+
...this.buildPayload({
|
|
1400
|
+
distinct_id: distinctId,
|
|
1401
|
+
event: '$identify',
|
|
1402
|
+
properties,
|
|
1403
|
+
}),
|
|
1404
|
+
};
|
|
1405
|
+
await this.sendImmediate('identify', payload, options);
|
|
1406
|
+
}
|
|
1368
1407
|
captureStateless(distinctId, event, properties, options) {
|
|
1369
1408
|
this.wrap(() => {
|
|
1370
1409
|
const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
|
|
1371
1410
|
this.enqueue('capture', payload, options);
|
|
1372
1411
|
});
|
|
1373
1412
|
}
|
|
1413
|
+
async captureStatelessImmediate(distinctId, event, properties, options) {
|
|
1414
|
+
const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
|
|
1415
|
+
await this.sendImmediate('capture', payload, options);
|
|
1416
|
+
}
|
|
1374
1417
|
aliasStateless(alias, distinctId, properties, options) {
|
|
1375
1418
|
this.wrap(() => {
|
|
1376
1419
|
const payload = this.buildPayload({
|
|
@@ -1385,6 +1428,18 @@ class PostHogCoreStateless {
|
|
|
1385
1428
|
this.enqueue('alias', payload, options);
|
|
1386
1429
|
});
|
|
1387
1430
|
}
|
|
1431
|
+
async aliasStatelessImmediate(alias, distinctId, properties, options) {
|
|
1432
|
+
const payload = this.buildPayload({
|
|
1433
|
+
event: '$create_alias',
|
|
1434
|
+
distinct_id: distinctId,
|
|
1435
|
+
properties: {
|
|
1436
|
+
...(properties || {}),
|
|
1437
|
+
distinct_id: distinctId,
|
|
1438
|
+
alias,
|
|
1439
|
+
},
|
|
1440
|
+
});
|
|
1441
|
+
await this.sendImmediate('alias', payload, options);
|
|
1442
|
+
}
|
|
1388
1443
|
/***
|
|
1389
1444
|
*** GROUPS
|
|
1390
1445
|
***/
|
|
@@ -1628,25 +1683,7 @@ class PostHogCoreStateless {
|
|
|
1628
1683
|
this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
|
|
1629
1684
|
return;
|
|
1630
1685
|
}
|
|
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
|
-
}
|
|
1686
|
+
const message = this.prepareMessage(type, _message, options);
|
|
1650
1687
|
const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
|
|
1651
1688
|
if (queue.length >= this.maxQueueSize) {
|
|
1652
1689
|
queue.shift();
|
|
@@ -1664,6 +1701,73 @@ class PostHogCoreStateless {
|
|
|
1664
1701
|
}
|
|
1665
1702
|
});
|
|
1666
1703
|
}
|
|
1704
|
+
async sendImmediate(type, _message, options) {
|
|
1705
|
+
if (this.disabled) {
|
|
1706
|
+
this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'));
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
if (!this._isInitialized) {
|
|
1710
|
+
await this._initPromise;
|
|
1711
|
+
}
|
|
1712
|
+
if (this.optedOut) {
|
|
1713
|
+
this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const data = {
|
|
1717
|
+
api_key: this.apiKey,
|
|
1718
|
+
batch: [this.prepareMessage(type, _message, options)],
|
|
1719
|
+
sent_at: currentISOTime(),
|
|
1720
|
+
};
|
|
1721
|
+
if (this.historicalMigration) {
|
|
1722
|
+
data.historical_migration = true;
|
|
1723
|
+
}
|
|
1724
|
+
const payload = JSON.stringify(data);
|
|
1725
|
+
const url = this.captureMode === 'form'
|
|
1726
|
+
? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
|
|
1727
|
+
: `${this.host}/batch/`;
|
|
1728
|
+
const fetchOptions = this.captureMode === 'form'
|
|
1729
|
+
? {
|
|
1730
|
+
method: 'POST',
|
|
1731
|
+
mode: 'no-cors',
|
|
1732
|
+
credentials: 'omit',
|
|
1733
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1734
|
+
body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
|
|
1735
|
+
}
|
|
1736
|
+
: {
|
|
1737
|
+
method: 'POST',
|
|
1738
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
1739
|
+
body: payload,
|
|
1740
|
+
};
|
|
1741
|
+
try {
|
|
1742
|
+
await this.fetchWithRetry(url, fetchOptions);
|
|
1743
|
+
}
|
|
1744
|
+
catch (err) {
|
|
1745
|
+
this._events.emit('error', err);
|
|
1746
|
+
throw err;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
prepareMessage(type, _message, options) {
|
|
1750
|
+
const message = {
|
|
1751
|
+
..._message,
|
|
1752
|
+
type: type,
|
|
1753
|
+
library: this.getLibraryId(),
|
|
1754
|
+
library_version: this.getLibraryVersion(),
|
|
1755
|
+
timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
|
|
1756
|
+
uuid: options?.uuid ? options.uuid : uuidv7(),
|
|
1757
|
+
};
|
|
1758
|
+
const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
|
|
1759
|
+
if (addGeoipDisableProperty) {
|
|
1760
|
+
if (!message.properties) {
|
|
1761
|
+
message.properties = {};
|
|
1762
|
+
}
|
|
1763
|
+
message['properties']['$geoip_disable'] = true;
|
|
1764
|
+
}
|
|
1765
|
+
if (message.distinctId) {
|
|
1766
|
+
message.distinct_id = message.distinctId;
|
|
1767
|
+
delete message.distinctId;
|
|
1768
|
+
}
|
|
1769
|
+
return message;
|
|
1770
|
+
}
|
|
1667
1771
|
clearFlushTimer() {
|
|
1668
1772
|
if (this._flushTimer) {
|
|
1669
1773
|
clearTimeout(this._flushTimer);
|
|
@@ -1675,16 +1779,26 @@ class PostHogCoreStateless {
|
|
|
1675
1779
|
* Avoids unnecessary promise errors
|
|
1676
1780
|
*/
|
|
1677
1781
|
flushBackground() {
|
|
1678
|
-
void this.flush().catch(() => {
|
|
1782
|
+
void this.flush().catch(async (err) => {
|
|
1783
|
+
await logFlushError(err);
|
|
1784
|
+
});
|
|
1679
1785
|
}
|
|
1680
1786
|
async flush() {
|
|
1681
|
-
|
|
1682
|
-
|
|
1787
|
+
// Wait for the current flush operation to finish (regardless of success or failure), then try to flush again.
|
|
1788
|
+
// Use allSettled instead of finally to be defensive around flush throwing errors immediately rather than rejecting.
|
|
1789
|
+
const nextFlushPromise = Promise.allSettled([this.flushPromise]).then(() => {
|
|
1790
|
+
return this._flush();
|
|
1791
|
+
});
|
|
1792
|
+
this.flushPromise = nextFlushPromise;
|
|
1793
|
+
void this.addPendingPromise(nextFlushPromise);
|
|
1794
|
+
Promise.allSettled([nextFlushPromise]).then(() => {
|
|
1795
|
+
// If there are no others waiting to flush, clear the promise.
|
|
1796
|
+
// We don't strictly need to do this, but it could make debugging easier
|
|
1797
|
+
if (this.flushPromise === nextFlushPromise) {
|
|
1683
1798
|
this.flushPromise = null;
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
return this.flushPromise;
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
return nextFlushPromise;
|
|
1688
1802
|
}
|
|
1689
1803
|
getCustomHeaders() {
|
|
1690
1804
|
// Don't set the user agent if we're not on a browser. The latest spec allows
|
|
@@ -1701,56 +1815,80 @@ class PostHogCoreStateless {
|
|
|
1701
1815
|
async _flush() {
|
|
1702
1816
|
this.clearFlushTimer();
|
|
1703
1817
|
await this._initPromise;
|
|
1704
|
-
|
|
1818
|
+
let queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
|
|
1705
1819
|
if (!queue.length) {
|
|
1706
1820
|
return [];
|
|
1707
1821
|
}
|
|
1708
|
-
const
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
const fetchOptions = this.captureMode === 'form'
|
|
1727
|
-
? {
|
|
1728
|
-
method: 'POST',
|
|
1729
|
-
mode: 'no-cors',
|
|
1730
|
-
credentials: 'omit',
|
|
1731
|
-
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1732
|
-
body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
|
|
1822
|
+
const sentMessages = [];
|
|
1823
|
+
const originalQueueLength = queue.length;
|
|
1824
|
+
while (queue.length > 0 && sentMessages.length < originalQueueLength) {
|
|
1825
|
+
const batchItems = queue.slice(0, this.maxBatchSize);
|
|
1826
|
+
const batchMessages = batchItems.map((item) => item.message);
|
|
1827
|
+
const persistQueueChange = () => {
|
|
1828
|
+
const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
|
|
1829
|
+
const newQueue = refreshedQueue.slice(batchItems.length);
|
|
1830
|
+
this.setPersistedProperty(PostHogPersistedProperty.Queue, newQueue);
|
|
1831
|
+
queue = newQueue;
|
|
1832
|
+
};
|
|
1833
|
+
const data = {
|
|
1834
|
+
api_key: this.apiKey,
|
|
1835
|
+
batch: batchMessages,
|
|
1836
|
+
sent_at: currentISOTime(),
|
|
1837
|
+
};
|
|
1838
|
+
if (this.historicalMigration) {
|
|
1839
|
+
data.historical_migration = true;
|
|
1733
1840
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1841
|
+
const payload = JSON.stringify(data);
|
|
1842
|
+
const url = this.captureMode === 'form'
|
|
1843
|
+
? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
|
|
1844
|
+
: `${this.host}/batch/`;
|
|
1845
|
+
const fetchOptions = this.captureMode === 'form'
|
|
1846
|
+
? {
|
|
1847
|
+
method: 'POST',
|
|
1848
|
+
mode: 'no-cors',
|
|
1849
|
+
credentials: 'omit',
|
|
1850
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1851
|
+
body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
|
|
1852
|
+
}
|
|
1853
|
+
: {
|
|
1854
|
+
method: 'POST',
|
|
1855
|
+
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
1856
|
+
body: payload,
|
|
1857
|
+
};
|
|
1858
|
+
const retryOptions = {
|
|
1859
|
+
retryCheck: (err) => {
|
|
1860
|
+
// don't automatically retry on 413 errors, we want to reduce the batch size first
|
|
1861
|
+
if (isPostHogFetchContentTooLargeError(err)) {
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
// otherwise, retry on network errors
|
|
1865
|
+
return isPostHogFetchError(err);
|
|
1866
|
+
},
|
|
1738
1867
|
};
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
}
|
|
1742
|
-
catch (err) {
|
|
1743
|
-
// depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
|
|
1744
|
-
// and this will be an endless loop, in this case, if the error isn't a network issue, we always remove the items from the queue
|
|
1745
|
-
if (!(err instanceof PostHogFetchNetworkError)) {
|
|
1746
|
-
persistQueueChange();
|
|
1868
|
+
try {
|
|
1869
|
+
await this.fetchWithRetry(url, fetchOptions, retryOptions);
|
|
1747
1870
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1871
|
+
catch (err) {
|
|
1872
|
+
if (isPostHogFetchContentTooLargeError(err) && batchMessages.length > 1) {
|
|
1873
|
+
// if we get a 413 error, we want to reduce the batch size and try again
|
|
1874
|
+
this.maxBatchSize = Math.max(1, Math.floor(batchMessages.length / 2));
|
|
1875
|
+
this.logMsgIfDebug(() => console.warn(`Received 413 when sending batch of size ${batchMessages.length}, reducing batch size to ${this.maxBatchSize}`));
|
|
1876
|
+
// do not persist the queue change, we want to retry the same batch
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
// depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
|
|
1880
|
+
// and this will be an endless loop, in this case, if the error isn't a network issue, we always remove the items from the queue
|
|
1881
|
+
if (!(err instanceof PostHogFetchNetworkError)) {
|
|
1882
|
+
persistQueueChange();
|
|
1883
|
+
}
|
|
1884
|
+
this._events.emit('error', err);
|
|
1885
|
+
throw err;
|
|
1886
|
+
}
|
|
1887
|
+
persistQueueChange();
|
|
1888
|
+
sentMessages.push(...batchMessages);
|
|
1750
1889
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
return messages;
|
|
1890
|
+
this._events.emit('flush', sentMessages);
|
|
1891
|
+
return sentMessages;
|
|
1754
1892
|
}
|
|
1755
1893
|
async fetchWithRetry(url, options, retryOptions, requestTimeout) {
|
|
1756
1894
|
var _a;
|
|
@@ -1759,6 +1897,8 @@ class PostHogCoreStateless {
|
|
|
1759
1897
|
setTimeout(() => ctrl.abort(), ms);
|
|
1760
1898
|
return ctrl.signal;
|
|
1761
1899
|
});
|
|
1900
|
+
const body = options.body ? options.body : '';
|
|
1901
|
+
const reqByteLength = Buffer.byteLength(body, STRING_FORMAT);
|
|
1762
1902
|
return await retriable(async () => {
|
|
1763
1903
|
let res = null;
|
|
1764
1904
|
try {
|
|
@@ -1776,12 +1916,12 @@ class PostHogCoreStateless {
|
|
|
1776
1916
|
// https://developer.mozilla.org/en-US/docs/Web/API/Request/mode#no-cors
|
|
1777
1917
|
const isNoCors = options.mode === 'no-cors';
|
|
1778
1918
|
if (!isNoCors && (res.status < 200 || res.status >= 400)) {
|
|
1779
|
-
throw new PostHogFetchHttpError(res);
|
|
1919
|
+
throw new PostHogFetchHttpError(res, reqByteLength);
|
|
1780
1920
|
}
|
|
1781
1921
|
return res;
|
|
1782
1922
|
}, { ...this._retryOptions, ...retryOptions });
|
|
1783
1923
|
}
|
|
1784
|
-
async
|
|
1924
|
+
async _shutdown(shutdownTimeoutMs = 30000) {
|
|
1785
1925
|
// A little tricky - we want to have a max shutdown time and enforce it, even if that means we have some
|
|
1786
1926
|
// dangling promises. We'll keep track of the timeout and resolve/reject based on that.
|
|
1787
1927
|
await this._initPromise;
|
|
@@ -1808,7 +1948,7 @@ class PostHogCoreStateless {
|
|
|
1808
1948
|
if (!isPostHogFetchError(e)) {
|
|
1809
1949
|
throw e;
|
|
1810
1950
|
}
|
|
1811
|
-
|
|
1951
|
+
await logFlushError(e);
|
|
1812
1952
|
}
|
|
1813
1953
|
};
|
|
1814
1954
|
return Promise.race([
|
|
@@ -1822,6 +1962,22 @@ class PostHogCoreStateless {
|
|
|
1822
1962
|
doShutdown(),
|
|
1823
1963
|
]);
|
|
1824
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Call shutdown() once before the node process exits, so ensure that all events have been sent and all promises
|
|
1967
|
+
* have resolved. Do not use this function if you intend to keep using this PostHog instance after calling it.
|
|
1968
|
+
* @param shutdownTimeoutMs
|
|
1969
|
+
*/
|
|
1970
|
+
async shutdown(shutdownTimeoutMs = 30000) {
|
|
1971
|
+
if (this.shutdownPromise) {
|
|
1972
|
+
this.logMsgIfDebug(() => console.warn('shutdown() called while already shutting down. shutdown() is meant to be called once before process exit - use flush() for per-request cleanup'));
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
this.shutdownPromise = this._shutdown(shutdownTimeoutMs).finally(() => {
|
|
1976
|
+
this.shutdownPromise = null;
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
return this.shutdownPromise;
|
|
1980
|
+
}
|
|
1825
1981
|
}
|
|
1826
1982
|
|
|
1827
1983
|
class PostHogMemoryStorage {
|
|
@@ -3689,6 +3845,79 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3689
3845
|
});
|
|
3690
3846
|
this.addPendingPromise(capturePromise);
|
|
3691
3847
|
}
|
|
3848
|
+
async captureImmediate(props) {
|
|
3849
|
+
if (typeof props === 'string') {
|
|
3850
|
+
this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
|
|
3851
|
+
}
|
|
3852
|
+
const {
|
|
3853
|
+
distinctId,
|
|
3854
|
+
event,
|
|
3855
|
+
properties,
|
|
3856
|
+
groups,
|
|
3857
|
+
sendFeatureFlags,
|
|
3858
|
+
timestamp,
|
|
3859
|
+
disableGeoip,
|
|
3860
|
+
uuid
|
|
3861
|
+
} = props;
|
|
3862
|
+
const _capture = props => {
|
|
3863
|
+
return super.captureStatelessImmediate(distinctId, event, props, {
|
|
3864
|
+
timestamp,
|
|
3865
|
+
disableGeoip,
|
|
3866
|
+
uuid
|
|
3867
|
+
});
|
|
3868
|
+
};
|
|
3869
|
+
const _getFlags = async (distinctId, groups, disableGeoip) => {
|
|
3870
|
+
return (await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)).flags;
|
|
3871
|
+
};
|
|
3872
|
+
const capturePromise = Promise.resolve().then(async () => {
|
|
3873
|
+
if (sendFeatureFlags) {
|
|
3874
|
+
// If we are sending feature flags, we need to make sure we have the latest flags
|
|
3875
|
+
// return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
|
|
3876
|
+
return await _getFlags(distinctId, groups, disableGeoip);
|
|
3877
|
+
}
|
|
3878
|
+
if (event === '$feature_flag_called') {
|
|
3879
|
+
// 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.
|
|
3880
|
+
return {};
|
|
3881
|
+
}
|
|
3882
|
+
if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
|
|
3883
|
+
// Otherwise we may as well check for the flags locally and include them if they are already loaded
|
|
3884
|
+
const groupsWithStringValues = {};
|
|
3885
|
+
for (const [key, value] of Object.entries(groups || {})) {
|
|
3886
|
+
groupsWithStringValues[key] = String(value);
|
|
3887
|
+
}
|
|
3888
|
+
return await this.getAllFlags(distinctId, {
|
|
3889
|
+
groups: groupsWithStringValues,
|
|
3890
|
+
disableGeoip,
|
|
3891
|
+
onlyEvaluateLocally: true
|
|
3892
|
+
});
|
|
3893
|
+
}
|
|
3894
|
+
return {};
|
|
3895
|
+
}).then(flags => {
|
|
3896
|
+
// Derive the relevant flag properties to add
|
|
3897
|
+
const additionalProperties = {};
|
|
3898
|
+
if (flags) {
|
|
3899
|
+
for (const [feature, variant] of Object.entries(flags)) {
|
|
3900
|
+
additionalProperties[`$feature/${feature}`] = variant;
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
|
|
3904
|
+
if (activeFlags.length > 0) {
|
|
3905
|
+
additionalProperties['$active_feature_flags'] = activeFlags;
|
|
3906
|
+
}
|
|
3907
|
+
return additionalProperties;
|
|
3908
|
+
}).catch(() => {
|
|
3909
|
+
// Something went wrong getting the flag info - we should capture the event anyways
|
|
3910
|
+
return {};
|
|
3911
|
+
}).then(additionalProperties => {
|
|
3912
|
+
// No matter what - capture the event
|
|
3913
|
+
_capture({
|
|
3914
|
+
...additionalProperties,
|
|
3915
|
+
...properties,
|
|
3916
|
+
$groups: groups
|
|
3917
|
+
});
|
|
3918
|
+
});
|
|
3919
|
+
await capturePromise;
|
|
3920
|
+
}
|
|
3692
3921
|
identify({
|
|
3693
3922
|
distinctId,
|
|
3694
3923
|
properties,
|
|
@@ -3707,11 +3936,33 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3707
3936
|
disableGeoip
|
|
3708
3937
|
});
|
|
3709
3938
|
}
|
|
3939
|
+
async identifyImmediate({
|
|
3940
|
+
distinctId,
|
|
3941
|
+
properties,
|
|
3942
|
+
disableGeoip
|
|
3943
|
+
}) {
|
|
3944
|
+
// promote $set and $set_once to top level
|
|
3945
|
+
const userPropsOnce = properties?.$set_once;
|
|
3946
|
+
delete properties?.$set_once;
|
|
3947
|
+
// if no $set is provided we assume all properties are $set
|
|
3948
|
+
const userProps = properties?.$set || properties;
|
|
3949
|
+
await super.identifyStatelessImmediate(distinctId, {
|
|
3950
|
+
$set: userProps,
|
|
3951
|
+
$set_once: userPropsOnce
|
|
3952
|
+
}, {
|
|
3953
|
+
disableGeoip
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3710
3956
|
alias(data) {
|
|
3711
3957
|
super.aliasStateless(data.alias, data.distinctId, undefined, {
|
|
3712
3958
|
disableGeoip: data.disableGeoip
|
|
3713
3959
|
});
|
|
3714
3960
|
}
|
|
3961
|
+
async aliasImmediate(data) {
|
|
3962
|
+
await super.aliasStatelessImmediate(data.alias, data.distinctId, undefined, {
|
|
3963
|
+
disableGeoip: data.disableGeoip
|
|
3964
|
+
});
|
|
3965
|
+
}
|
|
3715
3966
|
isLocalEvaluationReady() {
|
|
3716
3967
|
return this.featureFlagsPoller?.isLocalEvaluationReady() ?? false;
|
|
3717
3968
|
}
|
|
@@ -3918,9 +4169,9 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3918
4169
|
async reloadFeatureFlags() {
|
|
3919
4170
|
await this.featureFlagsPoller?.loadFeatureFlags(true);
|
|
3920
4171
|
}
|
|
3921
|
-
async
|
|
4172
|
+
async _shutdown(shutdownTimeoutMs) {
|
|
3922
4173
|
this.featureFlagsPoller?.stopPoller();
|
|
3923
|
-
return super.
|
|
4174
|
+
return super._shutdown(shutdownTimeoutMs);
|
|
3924
4175
|
}
|
|
3925
4176
|
addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties) {
|
|
3926
4177
|
const allPersonProperties = {
|