posthog-js-lite 3.0.0-beta.1 → 3.0.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
@@ -5,6 +5,12 @@ declare type PostHogCoreOptions = {
5
5
  flushAt?: number;
6
6
  /** The interval in milliseconds between periodic flushes */
7
7
  flushInterval?: number;
8
+ /** The maximum number of queued messages to be flushed as part of a single batch (must be higher than `flushAt`) */
9
+ maxBatchSize?: number;
10
+ /** The maximum number of cached messages either in memory or on the local storage.
11
+ * Defaults to 1000, (must be higher than `flushAt`)
12
+ */
13
+ maxQueueSize?: number;
8
14
  /** If set to true the SDK is essentially disabled (useful for local environments where you don't want to track anything) */
9
15
  disabled?: boolean;
10
16
  /** If set to false the SDK will not track until the `optIn` function is called. */
@@ -127,7 +133,10 @@ declare abstract class PostHogCoreStateless {
127
133
  private apiKey;
128
134
  host: string;
129
135
  private flushAt;
136
+ private maxBatchSize;
137
+ private maxQueueSize;
130
138
  private flushInterval;
139
+ private flushPromise;
131
140
  private requestTimeout;
132
141
  private featureFlagsRequestTimeoutMs;
133
142
  private captureMode;
@@ -157,7 +166,7 @@ declare abstract class PostHogCoreStateless {
157
166
  debug(enabled?: boolean): void;
158
167
  get isDebug(): boolean;
159
168
  private buildPayload;
160
- protected addPendingPromise(promise: Promise<any>): void;
169
+ protected addPendingPromise<T>(promise: Promise<T>): Promise<T>;
161
170
  /***
162
171
  *** TRACKING
163
172
  ***/
@@ -189,11 +198,19 @@ declare abstract class PostHogCoreStateless {
189
198
  *** QUEUEING AND FLUSHING
190
199
  ***/
191
200
  protected enqueue(type: string, _message: any, options?: PostHogCaptureOptions): void;
192
- flushAsync(): Promise<any>;
193
- flush(callback?: (err?: any, data?: any) => void): void;
201
+ private clearFlushTimer;
202
+ /**
203
+ * Helper for flushing the queue in the background
204
+ * Avoids unnecessary promise errors
205
+ */
206
+ private flushBackground;
207
+ flush(): Promise<any[]>;
208
+ protected getCustomHeaders(): {
209
+ [key: string]: string;
210
+ };
211
+ private _flush;
194
212
  private fetchWithRetry;
195
- shutdownAsync(shutdownTimeoutMs?: number): Promise<void>;
196
- shutdown(shutdownTimeoutMs?: number): void;
213
+ shutdown(shutdownTimeoutMs?: number): Promise<void>;
197
214
  }
198
215
  declare abstract class PostHogCore extends PostHogCoreStateless {
199
216
  private sendFeatureFlagEvent;
package/lib/index.esm.js CHANGED
@@ -942,6 +942,7 @@ function isPostHogFetchError(err) {
942
942
  }
943
943
  class PostHogCoreStateless {
944
944
  constructor(apiKey, options) {
945
+ this.flushPromise = null;
945
946
  this.disableGeoip = true;
946
947
  this.disabled = false;
947
948
  this.defaultOptIn = true;
@@ -953,6 +954,8 @@ class PostHogCoreStateless {
953
954
  this.apiKey = apiKey;
954
955
  this.host = removeTrailingSlash(options?.host || 'https://app.posthog.com');
955
956
  this.flushAt = options?.flushAt ? Math.max(options?.flushAt, 1) : 20;
957
+ this.maxBatchSize = Math.max(this.flushAt, options?.maxBatchSize ?? 100);
958
+ this.maxQueueSize = Math.max(this.flushAt, options?.maxQueueSize ?? 1000);
956
959
  this.flushInterval = options?.flushInterval ?? 10000;
957
960
  this.captureMode = options?.captureMode || 'form';
958
961
  // If enable is explicitly set to false we override the optout
@@ -1031,9 +1034,12 @@ class PostHogCoreStateless {
1031
1034
  addPendingPromise(promise) {
1032
1035
  const promiseUUID = uuidv7();
1033
1036
  this.pendingPromises[promiseUUID] = promise;
1034
- promise.finally(() => {
1037
+ promise
1038
+ .catch(() => { })
1039
+ .finally(() => {
1035
1040
  delete this.pendingPromises[promiseUUID];
1036
1041
  });
1042
+ return promise;
1037
1043
  }
1038
1044
  /***
1039
1045
  *** TRACKING
@@ -1098,7 +1104,7 @@ class PostHogCoreStateless {
1098
1104
  const url = `${this.host}/decide/?v=3`;
1099
1105
  const fetchOptions = {
1100
1106
  method: 'POST',
1101
- headers: { 'Content-Type': 'application/json' },
1107
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1102
1108
  body: JSON.stringify({
1103
1109
  token: this.apiKey,
1104
1110
  distinct_id: distinctId,
@@ -1208,80 +1214,106 @@ class PostHogCoreStateless {
1208
1214
  delete message.distinctId;
1209
1215
  }
1210
1216
  const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1217
+ if (queue.length >= this.maxQueueSize) {
1218
+ queue.shift();
1219
+ console.info('Queue is full, the oldest event is dropped.');
1220
+ }
1211
1221
  queue.push({ message });
1212
1222
  this.setPersistedProperty(PostHogPersistedProperty.Queue, queue);
1213
1223
  this._events.emit(type, message);
1214
1224
  // Flush queued events if we meet the flushAt length
1215
1225
  if (queue.length >= this.flushAt) {
1216
- this.flush();
1226
+ this.flushBackground();
1217
1227
  }
1218
1228
  if (this.flushInterval && !this._flushTimer) {
1219
- this._flushTimer = safeSetTimeout(() => this.flush(), this.flushInterval);
1229
+ this._flushTimer = safeSetTimeout(() => this.flushBackground(), this.flushInterval);
1220
1230
  }
1221
1231
  });
1222
1232
  }
1223
- async flushAsync() {
1224
- await this._initPromise;
1225
- return new Promise((resolve, reject) => {
1226
- this.flush((err, data) => {
1227
- return err ? reject(err) : resolve(data);
1233
+ clearFlushTimer() {
1234
+ if (this._flushTimer) {
1235
+ clearTimeout(this._flushTimer);
1236
+ this._flushTimer = undefined;
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Helper for flushing the queue in the background
1241
+ * Avoids unnecessary promise errors
1242
+ */
1243
+ flushBackground() {
1244
+ void this.flush().catch(() => { });
1245
+ }
1246
+ async flush() {
1247
+ if (!this.flushPromise) {
1248
+ this.flushPromise = this._flush().finally(() => {
1249
+ this.flushPromise = null;
1228
1250
  });
1229
- });
1251
+ this.addPendingPromise(this.flushPromise);
1252
+ }
1253
+ return this.flushPromise;
1230
1254
  }
1231
- flush(callback) {
1232
- this.wrap(() => {
1233
- if (this._flushTimer) {
1234
- clearTimeout(this._flushTimer);
1235
- this._flushTimer = null;
1236
- }
1237
- const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1238
- if (!queue.length) {
1239
- return callback?.();
1255
+ getCustomHeaders() {
1256
+ // Don't set the user agent if we're not on a browser. The latest spec allows
1257
+ // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers
1258
+ // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader),
1259
+ // but browsers such as Chrome and Safari have not caught up.
1260
+ const customUserAgent = this.getCustomUserAgent();
1261
+ const headers = {};
1262
+ if (customUserAgent && customUserAgent !== '') {
1263
+ headers['User-Agent'] = customUserAgent;
1264
+ }
1265
+ return headers;
1266
+ }
1267
+ async _flush() {
1268
+ this.clearFlushTimer();
1269
+ await this._initPromise;
1270
+ const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1271
+ if (!queue.length) {
1272
+ return [];
1273
+ }
1274
+ const items = queue.slice(0, this.maxBatchSize);
1275
+ const messages = items.map((item) => item.message);
1276
+ const persistQueueChange = () => {
1277
+ const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1278
+ this.setPersistedProperty(PostHogPersistedProperty.Queue, refreshedQueue.slice(items.length));
1279
+ };
1280
+ const data = {
1281
+ api_key: this.apiKey,
1282
+ batch: messages,
1283
+ sent_at: currentISOTime(),
1284
+ };
1285
+ const payload = JSON.stringify(data);
1286
+ const url = this.captureMode === 'form'
1287
+ ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1288
+ : `${this.host}/batch/`;
1289
+ const fetchOptions = this.captureMode === 'form'
1290
+ ? {
1291
+ method: 'POST',
1292
+ mode: 'no-cors',
1293
+ credentials: 'omit',
1294
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' },
1295
+ body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
1240
1296
  }
1241
- const items = queue.splice(0, this.flushAt);
1242
- this.setPersistedProperty(PostHogPersistedProperty.Queue, queue);
1243
- const messages = items.map((item) => item.message);
1244
- const data = {
1245
- api_key: this.apiKey,
1246
- batch: messages,
1247
- sent_at: currentISOTime(),
1248
- };
1249
- const done = (err) => {
1250
- if (err) {
1251
- this._events.emit('error', err);
1252
- }
1253
- callback?.(err, messages);
1254
- this._events.emit('flush', messages);
1297
+ : {
1298
+ method: 'POST',
1299
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1300
+ body: payload,
1255
1301
  };
1256
- // Don't set the user agent if we're not on a browser. The latest spec allows
1257
- // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers
1258
- // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader),
1259
- // but browsers such as Chrome and Safari have not caught up.
1260
- this.getCustomUserAgent();
1261
- const payload = JSON.stringify(data);
1262
- const url = this.captureMode === 'form'
1263
- ? `${this.host}/e/?ip=1&_=${currentTimestamp()}&v=${this.getLibraryVersion()}`
1264
- : `${this.host}/batch/`;
1265
- const fetchOptions = this.captureMode === 'form'
1266
- ? {
1267
- method: 'POST',
1268
- mode: 'no-cors',
1269
- credentials: 'omit',
1270
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1271
- body: `data=${encodeURIComponent(LZString.compressToBase64(payload))}&compression=lz64`,
1272
- }
1273
- : {
1274
- method: 'POST',
1275
- headers: { 'Content-Type': 'application/json' },
1276
- body: payload,
1277
- };
1278
- const requestPromise = this.fetchWithRetry(url, fetchOptions);
1279
- this.addPendingPromise(requestPromise
1280
- .then(() => done())
1281
- .catch((err) => {
1282
- done(err);
1283
- }));
1284
- });
1302
+ try {
1303
+ await this.fetchWithRetry(url, fetchOptions);
1304
+ }
1305
+ catch (err) {
1306
+ // depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
1307
+ // 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
1308
+ if (!(err instanceof PostHogFetchNetworkError)) {
1309
+ persistQueueChange();
1310
+ }
1311
+ this._events.emit('error', err);
1312
+ throw err;
1313
+ }
1314
+ persistQueueChange();
1315
+ this._events.emit('flush', messages);
1316
+ return messages;
1285
1317
  }
1286
1318
  async fetchWithRetry(url, options, retryOptions, requestTimeout) {
1287
1319
  var _a;
@@ -1312,15 +1344,12 @@ class PostHogCoreStateless {
1312
1344
  return res;
1313
1345
  }, { ...this._retryOptions, ...retryOptions });
1314
1346
  }
1315
- async shutdownAsync(shutdownTimeoutMs) {
1347
+ async shutdown(shutdownTimeoutMs = 30000) {
1316
1348
  await this._initPromise;
1317
- clearTimeout(this._flushTimer);
1349
+ this.clearFlushTimer();
1318
1350
  try {
1319
- await Promise.all(Object.values(this.pendingPromises).map((x) => x.catch(() => {
1320
- // ignore errors as we are shutting down and can't deal with them anyways.
1321
- })));
1322
- const timeout = shutdownTimeoutMs ?? 30000;
1323
- const startTimeWithDelay = Date.now() + timeout;
1351
+ await Promise.all(Object.values(this.pendingPromises));
1352
+ const startTimeWithDelay = Date.now() + shutdownTimeoutMs;
1324
1353
  while (true) {
1325
1354
  const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1326
1355
  if (queue.length === 0) {
@@ -1329,7 +1358,7 @@ class PostHogCoreStateless {
1329
1358
  // flush again to make sure we send all events, some of which might've been added
1330
1359
  // while we were waiting for the pending promises to resolve
1331
1360
  // For example, see sendFeatureFlags in posthog-node/src/posthog-node.ts::capture
1332
- await this.flushAsync();
1361
+ await this.flush();
1333
1362
  // If we've been waiting for more than the shutdownTimeoutMs, stop it
1334
1363
  const now = Date.now();
1335
1364
  if (startTimeWithDelay < now) {
@@ -1344,9 +1373,6 @@ class PostHogCoreStateless {
1344
1373
  console.error('Error while shutting down PostHog', e);
1345
1374
  }
1346
1375
  }
1347
- shutdown(shutdownTimeoutMs) {
1348
- void this.shutdownAsync(shutdownTimeoutMs);
1349
- }
1350
1376
  }
1351
1377
  class PostHogCore extends PostHogCoreStateless {
1352
1378
  constructor(apiKey, options) {
@@ -1812,7 +1838,7 @@ class PostHogCore extends PostHogCoreStateless {
1812
1838
  }
1813
1839
  }
1814
1840
 
1815
- var version = "3.0.0-beta.1";
1841
+ var version = "3.0.0";
1816
1842
 
1817
1843
  function getContext(window) {
1818
1844
  let context = {};