@statsig/client-core 3.32.4 → 3.32.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statsig/client-core",
3
- "version": "3.32.4",
3
+ "version": "3.32.6",
4
4
  "license": "ISC",
5
5
  "homepage": "https://github.com/statsig-io/js-client-monorepo",
6
6
  "repository": {
@@ -11,7 +11,7 @@ export declare class ErrorBoundary {
11
11
  wrap(instance: unknown, namePrefix?: string): void;
12
12
  logError(tag: string, error: unknown): void;
13
13
  logDroppedEvents(count: number, reason: string, metadata?: Record<string, unknown>): void;
14
- logEventRequestFailure(count: number, reason: string, flushType: string, statusCode: number, retries: number, failurePath?: string): void;
14
+ logEventRequestFailure(count: number, reason: string, flushType: string, statusCode: number, retries: number, failurePath?: string, failureErrorMessage?: string, failureDiagnosticBucket?: string, failureDiagnosticMetadata?: Record<string, string>): void;
15
15
  getLastSeenErrorAndReset(): Error | null;
16
16
  attachErrorIfNoneExists(error: unknown): void;
17
17
  private _capture;
@@ -56,7 +56,7 @@ class ErrorBoundary {
56
56
  }
57
57
  this._onError(`statsig::log_event_dropped_event_count`, new Error(reason), true, extra);
58
58
  }
59
- logEventRequestFailure(count, reason, flushType, statusCode, retries, failurePath) {
59
+ logEventRequestFailure(count, reason, flushType, statusCode, retries, failurePath, failureErrorMessage, failureDiagnosticBucket, failureDiagnosticMetadata) {
60
60
  const extra = {
61
61
  eventCount: String(count),
62
62
  flushType: flushType,
@@ -67,6 +67,22 @@ class ErrorBoundary {
67
67
  if (failurePath) {
68
68
  extra['failurePath'] = failurePath;
69
69
  }
70
+ if (typeof failureErrorMessage === 'string' &&
71
+ failureErrorMessage.length > 0) {
72
+ extra['failureErrorMessage'] = failureErrorMessage;
73
+ }
74
+ if (typeof failureDiagnosticBucket === 'string' &&
75
+ failureDiagnosticBucket.length > 0) {
76
+ extra['failureDiagnosticBucket'] = failureDiagnosticBucket;
77
+ }
78
+ if (failureDiagnosticMetadata) {
79
+ Object.keys(failureDiagnosticMetadata).forEach((key) => {
80
+ const value = failureDiagnosticMetadata[key];
81
+ if (value.length > 0) {
82
+ extra[`failureDiagnostic_${key}`] = value;
83
+ }
84
+ });
85
+ }
70
86
  this._onError(`statsig::log_event_failed`, new Error(reason), true, extra);
71
87
  }
72
88
  getLastSeenErrorAndReset() {
@@ -119,8 +135,9 @@ class ErrorBoundary {
119
135
  const statsigMetadata = StatsigMetadata_1.StatsigMetadataProvider.get();
120
136
  const info = isError ? unwrapped.stack : _getDescription(unwrapped);
121
137
  const body = Object.assign(Object.assign({ tag, exception: name, info, statsigOptions: _getStatsigOptionLoggingCopy(this._options) }, Object.assign(Object.assign({}, statsigMetadata), { sdkType })), (extra !== null && extra !== void 0 ? extra : {}));
122
- const func = (_f = (_e = (_d = this._options) === null || _d === void 0 ? void 0 : _d.networkConfig) === null || _e === void 0 ? void 0 : _e.networkOverrideFunc) !== null && _f !== void 0 ? _f : fetch;
123
- yield func(exports.EXCEPTION_ENDPOINT, {
138
+ const networkConfig = (_d = this._options) === null || _d === void 0 ? void 0 : _d.networkConfig;
139
+ const func = (_e = networkConfig === null || networkConfig === void 0 ? void 0 : networkConfig.networkOverrideFunc) !== null && _e !== void 0 ? _e : fetch;
140
+ yield func((_f = networkConfig === null || networkConfig === void 0 ? void 0 : networkConfig.sdkExceptionUrl) !== null && _f !== void 0 ? _f : exports.EXCEPTION_ENDPOINT, {
124
141
  method: 'POST',
125
142
  headers: {
126
143
  'STATSIG-API-KEY': this._sdkKey,
@@ -1,7 +1,7 @@
1
1
  export declare const EventRetryConstants: {
2
- readonly MAX_RETRY_ATTEMPTS: 5;
2
+ readonly MAX_RETRY_ATTEMPTS: 8;
3
3
  readonly DEFAULT_BATCH_SIZE: 100;
4
- readonly MAX_PENDING_BATCHES: 30;
4
+ readonly MAX_PENDING_BATCHES: 40;
5
5
  readonly TICK_INTERVAL_MS: 1000;
6
6
  readonly QUICK_FLUSH_WINDOW_MS: 200;
7
7
  readonly MAX_LOCAL_STORAGE: 500;
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EventRetryConstants = void 0;
4
4
  exports.EventRetryConstants = {
5
- MAX_RETRY_ATTEMPTS: 5,
5
+ MAX_RETRY_ATTEMPTS: 8,
6
6
  DEFAULT_BATCH_SIZE: 100,
7
- MAX_PENDING_BATCHES: 30,
7
+ MAX_PENDING_BATCHES: 40,
8
8
  TICK_INTERVAL_MS: 1000,
9
9
  QUICK_FLUSH_WINDOW_MS: 200,
10
10
  MAX_LOCAL_STORAGE: 500,
@@ -7,6 +7,9 @@ type EventSendResult = {
7
7
  success: boolean;
8
8
  statusCode: number;
9
9
  failurePath?: string;
10
+ failureErrorMessage?: string;
11
+ failureDiagnosticBucket?: string;
12
+ failureDiagnosticMetadata?: Record<string, string>;
10
13
  };
11
14
  export declare class EventSender {
12
15
  private _network;
@@ -55,19 +55,23 @@ class EventSender {
55
55
  });
56
56
  return response;
57
57
  }
58
- return {
59
- success: false,
60
- statusCode: response.statusCode,
61
- failurePath: response.failurePath,
62
- };
58
+ return Object.assign(Object.assign(Object.assign({ success: false, statusCode: response.statusCode, failurePath: response.failurePath }, (response.failureErrorMessage
59
+ ? { failureErrorMessage: response.failureErrorMessage }
60
+ : {})), (response.failureDiagnosticBucket
61
+ ? { failureDiagnosticBucket: response.failureDiagnosticBucket }
62
+ : {})), (response.failureDiagnosticMetadata
63
+ ? { failureDiagnosticMetadata: response.failureDiagnosticMetadata }
64
+ : {}));
63
65
  }
64
66
  catch (error) {
65
67
  Log_1.Log.warn('Failed to send batch:', error);
66
- return {
67
- success: false,
68
- statusCode: -1,
69
- failurePath: (_c = transportFailure.path) !== null && _c !== void 0 ? _c : failurePath,
70
- };
68
+ return Object.assign(Object.assign(Object.assign({ success: false, statusCode: -1, failurePath: (_c = transportFailure.path) !== null && _c !== void 0 ? _c : failurePath }, (transportFailure.errorMessage
69
+ ? { failureErrorMessage: transportFailure.errorMessage }
70
+ : {})), (transportFailure.diagnosticBucket
71
+ ? { failureDiagnosticBucket: transportFailure.diagnosticBucket }
72
+ : {})), (transportFailure.diagnosticMetadata
73
+ ? { failureDiagnosticMetadata: transportFailure.diagnosticMetadata }
74
+ : {}));
71
75
  }
72
76
  });
73
77
  }
@@ -77,13 +81,15 @@ class EventSender {
77
81
  const result = yield this._network.post(this._getRequestData(batch), failureInfo);
78
82
  const code = (_a = result === null || result === void 0 ? void 0 : result.code) !== null && _a !== void 0 ? _a : -1;
79
83
  if (code === -1) {
80
- return {
81
- success: false,
82
- statusCode: -1,
83
- failurePath: (_b = failureInfo.path) !== null && _b !== void 0 ? _b : (result === undefined
84
+ return Object.assign(Object.assign(Object.assign({ success: false, statusCode: -1, failurePath: (_b = failureInfo.path) !== null && _b !== void 0 ? _b : (result === undefined
84
85
  ? 'event_sender_post_returned_undefined'
85
- : 'event_sender_post_returned_null'),
86
- };
86
+ : 'event_sender_post_returned_null') }, (failureInfo.errorMessage
87
+ ? { failureErrorMessage: failureInfo.errorMessage }
88
+ : {})), (failureInfo.diagnosticBucket
89
+ ? { failureDiagnosticBucket: failureInfo.diagnosticBucket }
90
+ : {})), (failureInfo.diagnosticMetadata
91
+ ? { failureDiagnosticMetadata: failureInfo.diagnosticMetadata }
92
+ : {}));
87
93
  }
88
94
  return { success: code >= 200 && code < 300, statusCode: code };
89
95
  });
@@ -91,13 +97,15 @@ class EventSender {
91
97
  _sendEventsViaBeacon(batch, failureInfo) {
92
98
  var _a;
93
99
  const success = this._network.beacon(this._getRequestData(batch), failureInfo);
94
- return {
95
- success,
96
- statusCode: success ? 200 : -1,
97
- failurePath: success
100
+ return Object.assign(Object.assign(Object.assign({ success, statusCode: success ? 200 : -1, failurePath: success
98
101
  ? undefined
99
- : (_a = failureInfo.path) !== null && _a !== void 0 ? _a : 'beacon_send_false',
100
- };
102
+ : (_a = failureInfo.path) !== null && _a !== void 0 ? _a : 'beacon_send_false' }, (!success && failureInfo.errorMessage
103
+ ? { failureErrorMessage: failureInfo.errorMessage }
104
+ : {})), (!success && failureInfo.diagnosticBucket
105
+ ? { failureDiagnosticBucket: failureInfo.diagnosticBucket }
106
+ : {})), (!success && failureInfo.diagnosticMetadata
107
+ ? { failureDiagnosticMetadata: failureInfo.diagnosticMetadata }
108
+ : {}));
101
109
  }
102
110
  _getRequestData(batch) {
103
111
  return {
@@ -44,6 +44,7 @@ export declare class FlushCoordinator {
44
44
  private _prepareQueueForFlush;
45
45
  containsAtLeastOneFullBatch(): boolean;
46
46
  convertPendingEventsToBatches(): number;
47
+ private _isRetryableBatch;
47
48
  private _handleFailure;
48
49
  loadAndRetryShutdownFailedEvents(): Promise<void>;
49
50
  private _getStorageKey;
@@ -21,6 +21,13 @@ const SafeJs_1 = require("./SafeJs");
21
21
  const SessionID_1 = require("./SessionID");
22
22
  const StatsigOptionsCommon_1 = require("./StatsigOptionsCommon");
23
23
  const StorageProvider_1 = require("./StorageProvider");
24
+ const RETRYABLE_NO_RESPONSE_FAILURE_PATHS = new Set([
25
+ 'network_request_timed_out_no_response',
26
+ 'network_request_exception_no_response',
27
+ 'event_sender_post_returned_null',
28
+ 'event_sender_post_returned_undefined',
29
+ 'event_sender_post_exception',
30
+ ]);
24
31
  class FlushCoordinator {
25
32
  constructor(batchQueue, pendingEvents, onPrepareFlush,
26
33
  // For Event Sender
@@ -254,7 +261,7 @@ class FlushCoordinator {
254
261
  return true;
255
262
  }
256
263
  this._flushInterval.adjustForFailure();
257
- this._handleFailure(batch, flushType, result.statusCode, result.failurePath);
264
+ this._handleFailure(batch, flushType, result.statusCode, result.failurePath, result.failureErrorMessage, result.failureDiagnosticBucket, result.failureDiagnosticMetadata);
258
265
  return false;
259
266
  });
260
267
  }
@@ -283,23 +290,36 @@ class FlushCoordinator {
283
290
  const allEvents = this._pendingEvents.takeAll();
284
291
  return this._batchQueue.createBatches(allEvents);
285
292
  }
286
- _handleFailure(batch, flushType, statusCode, failurePath) {
293
+ _isRetryableBatch(statusCode, failurePath) {
294
+ if (NetworkCore_1.RETRYABLE_CODES.has(statusCode)) {
295
+ return true;
296
+ }
297
+ if (statusCode === -1 &&
298
+ failurePath &&
299
+ RETRYABLE_NO_RESPONSE_FAILURE_PATHS.has(failurePath)) {
300
+ return true;
301
+ }
302
+ return false;
303
+ }
304
+ _handleFailure(batch, flushType, statusCode, failurePath, failureErrorMessage, failureDiagnosticBucket, failureDiagnosticMetadata) {
287
305
  if (flushType === FlushTypes_1.FlushType.Shutdown) {
288
306
  Log_1.Log.warn(`${flushType} flush failed during shutdown. ` +
289
307
  `${batch.events.length} event(s) will be saved to storage for retry in next session.`);
290
308
  this._saveShutdownFailedEventsToStorage(batch.events);
291
309
  return;
292
310
  }
293
- if (!NetworkCore_1.RETRYABLE_CODES.has(statusCode)) {
311
+ if (!this._isRetryableBatch(statusCode, failurePath)) {
312
+ const reason = `non-retryable error`;
294
313
  Log_1.Log.warn(`${flushType} flush failed after ${batch.attempts} attempt(s). ` +
295
314
  `${batch.events.length} event(s) will be dropped. Non-retryable error: ${statusCode}`);
296
- this._errorBoundary.logEventRequestFailure(batch.events.length, `non-retryable error`, flushType, statusCode, batch.attempts, failurePath);
315
+ this._errorBoundary.logEventRequestFailure(batch.events.length, reason, flushType, statusCode, batch.attempts, failurePath, failureErrorMessage, failureDiagnosticBucket, failureDiagnosticMetadata);
297
316
  return;
298
317
  }
299
318
  if (batch.attempts >= EventRetryConstants_1.EventRetryConstants.MAX_RETRY_ATTEMPTS) {
319
+ const reason = `max retry attempts exceeded`;
300
320
  Log_1.Log.warn(`${flushType} flush failed after ${batch.attempts} attempt(s). ` +
301
321
  `${batch.events.length} event(s) will be dropped.`);
302
- this._errorBoundary.logEventRequestFailure(batch.events.length, `max retry attempts exceeded`, flushType, statusCode, batch.attempts, failurePath);
322
+ this._errorBoundary.logEventRequestFailure(batch.events.length, reason, flushType, statusCode, batch.attempts, failurePath, failureErrorMessage, failureDiagnosticBucket, failureDiagnosticMetadata);
303
323
  return;
304
324
  }
305
325
  batch.incrementAttempts();
@@ -26,6 +26,9 @@ export type RequestArgsWithData = Flatten<RequestArgs & {
26
26
  } & DataFlags>;
27
27
  export type RequestFailureInfo = {
28
28
  path?: string;
29
+ errorMessage?: string;
30
+ diagnosticBucket?: string;
31
+ diagnosticMetadata?: Record<string, string>;
29
32
  };
30
33
  type BeaconRequestArgs = Pick<RequestArgsWithData, 'data' | 'sdkKey' | 'urlConfig' | 'params' | 'isCompressable' | 'attempt'>;
31
34
  type NetworkResponse = {
@@ -138,6 +138,7 @@ class NetworkCore {
138
138
  const currentAttempt = attempt !== null && attempt !== void 0 ? attempt : 1;
139
139
  let reqTimedOut = false;
140
140
  const populatedUrl = this._getPopulatedURL(args);
141
+ const startTime = Date.now();
141
142
  let response = null;
142
143
  const keepalive = (0, VisibilityObserving_1._isUnloading)();
143
144
  try {
@@ -210,6 +211,17 @@ class NetworkCore {
210
211
  failureInfo.path = timedOut
211
212
  ? 'network_request_timed_out_no_response'
212
213
  : 'network_request_exception_no_response';
214
+ if (errorMessage) {
215
+ failureInfo.errorMessage = errorMessage;
216
+ }
217
+ try {
218
+ const diagnostics = _getNoResponseDiagnostics(args, populatedUrl, timedOut, Date.now() - startTime);
219
+ failureInfo.diagnosticBucket = diagnostics.bucket;
220
+ failureInfo.diagnosticMetadata = diagnostics.metadata;
221
+ }
222
+ catch (_e) {
223
+ // Diagnostics should not affect request failure handling.
224
+ }
213
225
  }
214
226
  }
215
227
  return null;
@@ -386,6 +398,89 @@ function _didTimeout(errorMsg, abortedByTimeout) {
386
398
  const timeout = errorMsg.includes('Timeout'); // probably not needed but just in case
387
399
  return timeout || abortedByTimeout;
388
400
  }
401
+ function _getNoResponseDiagnostics(args, populatedUrl, timedOut, elapsedMs) {
402
+ var _a, _b, _c;
403
+ const win = (0, SafeJs_1._getWindowSafe)();
404
+ const doc = win === null || win === void 0 ? void 0 : win.document;
405
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
406
+ const isUnloading = (0, VisibilityObserving_1._isUnloading)();
407
+ const online = nav && typeof nav.onLine === 'boolean' ? String(nav.onLine) : 'unknown';
408
+ const visibilityState = (_a = doc === null || doc === void 0 ? void 0 : doc.visibilityState) !== null && _a !== void 0 ? _a : 'unknown';
409
+ const hasCustomHeaders = Object.keys((_b = args.headers) !== null && _b !== void 0 ? _b : {}).length > 0;
410
+ const crossOrigin = _isCrossOrigin(populatedUrl, (_c = win === null || win === void 0 ? void 0 : win.location) === null || _c === void 0 ? void 0 : _c.origin);
411
+ const hasCustomUrl = args.urlConfig.customUrl != null;
412
+ const hasFallbackUrl = args.fallbackUrl != null;
413
+ const elapsedMsBucket = _bucketNumber(elapsedMs, [250, 1000, 5000, 10000]);
414
+ const bodySizeBucket = _bucketNumber(_getBodySize(args.body), [16384, 65536, 262144, 1048576]);
415
+ let bucket = 'unknown_no_response';
416
+ if (timedOut) {
417
+ bucket = 'timeout';
418
+ }
419
+ else if (online === 'false') {
420
+ bucket = 'browser_offline';
421
+ }
422
+ else if (isUnloading) {
423
+ bucket = 'page_unloading';
424
+ }
425
+ else if (visibilityState === 'hidden') {
426
+ bucket = 'page_hidden';
427
+ }
428
+ else if (crossOrigin && hasCustomHeaders) {
429
+ bucket = 'cross_origin_custom_headers_preflight_risk';
430
+ }
431
+ else if (hasCustomUrl || hasFallbackUrl) {
432
+ bucket = 'custom_url_no_response';
433
+ }
434
+ else if (elapsedMs < 250) {
435
+ bucket = 'immediate_network_rejection';
436
+ }
437
+ return {
438
+ bucket,
439
+ metadata: {
440
+ elapsedMsBucket,
441
+ bodySizeBucket,
442
+ online,
443
+ visibilityState,
444
+ isUnloading: String(isUnloading),
445
+ crossOrigin: String(crossOrigin),
446
+ hasCustomUrl: String(hasCustomUrl),
447
+ },
448
+ };
449
+ }
450
+ function _isCrossOrigin(url, currentOrigin) {
451
+ if (!currentOrigin) {
452
+ return true;
453
+ }
454
+ return (!url.startsWith(`${currentOrigin}/`) &&
455
+ !url.startsWith(`${currentOrigin}?`) &&
456
+ url !== currentOrigin);
457
+ }
458
+ function _getBodySize(body) {
459
+ if (body == null) {
460
+ return 0;
461
+ }
462
+ if (typeof body === 'string') {
463
+ return body.length;
464
+ }
465
+ if (body instanceof Uint8Array) {
466
+ return body.byteLength;
467
+ }
468
+ if (typeof Blob !== 'undefined' && body instanceof Blob) {
469
+ return body.size;
470
+ }
471
+ return -1;
472
+ }
473
+ function _bucketNumber(value, thresholds) {
474
+ if (value < 0) {
475
+ return 'unknown';
476
+ }
477
+ for (const threshold of thresholds) {
478
+ if (value < threshold) {
479
+ return `<${threshold}`;
480
+ }
481
+ }
482
+ return `>=${thresholds[thresholds.length - 1]}`;
483
+ }
389
484
  function _tryMarkInitStart(args, attempt) {
390
485
  if (args.urlConfig.endpoint !== NetworkConfig_1.Endpoint._initialize) {
391
486
  return;
@@ -1,4 +1,4 @@
1
- export declare const SDK_VERSION = "3.32.4";
1
+ export declare const SDK_VERSION = "3.32.6";
2
2
  export type StatsigMetadata = {
3
3
  readonly [key: string]: string | undefined | null;
4
4
  readonly appVersion?: string;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StatsigMetadataProvider = exports.SDK_VERSION = void 0;
4
- exports.SDK_VERSION = '3.32.4';
4
+ exports.SDK_VERSION = '3.32.6';
5
5
  let metadata = {
6
6
  sdkVersion: exports.SDK_VERSION,
7
7
  sdkType: 'js-mono', // js-mono is overwritten by Precomp and OnDevice clients
@@ -57,6 +57,12 @@ export type NetworkConfigCommon = {
57
57
  * default: `https://prodregistryv2.org/v1/rgstr`
58
58
  */
59
59
  logEventUrl?: string;
60
+ /**
61
+ * The URL used to report SDK exceptions via a POST request.
62
+ *
63
+ * default: `https://statsigapi.net/v1/sdk_exception`
64
+ */
65
+ sdkExceptionUrl?: string;
60
66
  /**
61
67
  * A list of URLs to try if the primary logEventUrl fails.
62
68
  */