@statsig/client-core 3.32.5 → 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.5",
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, failureErrorMessage?: 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, failureErrorMessage) {
59
+ logEventRequestFailure(count, reason, flushType, statusCode, retries, failurePath, failureErrorMessage, failureDiagnosticBucket, failureDiagnosticMetadata) {
60
60
  const extra = {
61
61
  eventCount: String(count),
62
62
  flushType: flushType,
@@ -71,6 +71,18 @@ class ErrorBoundary {
71
71
  failureErrorMessage.length > 0) {
72
72
  extra['failureErrorMessage'] = failureErrorMessage;
73
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
+ }
74
86
  this._onError(`statsig::log_event_failed`, new Error(reason), true, extra);
75
87
  }
76
88
  getLastSeenErrorAndReset() {
@@ -123,8 +135,9 @@ class ErrorBoundary {
123
135
  const statsigMetadata = StatsigMetadata_1.StatsigMetadataProvider.get();
124
136
  const info = isError ? unwrapped.stack : _getDescription(unwrapped);
125
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 : {}));
126
- 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;
127
- 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, {
128
141
  method: 'POST',
129
142
  headers: {
130
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,
@@ -8,6 +8,8 @@ type EventSendResult = {
8
8
  statusCode: number;
9
9
  failurePath?: string;
10
10
  failureErrorMessage?: string;
11
+ failureDiagnosticBucket?: string;
12
+ failureDiagnosticMetadata?: Record<string, string>;
11
13
  };
12
14
  export declare class EventSender {
13
15
  private _network;
@@ -55,14 +55,22 @@ class EventSender {
55
55
  });
56
56
  return response;
57
57
  }
58
- return Object.assign({ success: false, statusCode: response.statusCode, failurePath: response.failurePath }, (response.failureErrorMessage
58
+ return Object.assign(Object.assign(Object.assign({ success: false, statusCode: response.statusCode, failurePath: response.failurePath }, (response.failureErrorMessage
59
59
  ? { failureErrorMessage: response.failureErrorMessage }
60
+ : {})), (response.failureDiagnosticBucket
61
+ ? { failureDiagnosticBucket: response.failureDiagnosticBucket }
62
+ : {})), (response.failureDiagnosticMetadata
63
+ ? { failureDiagnosticMetadata: response.failureDiagnosticMetadata }
60
64
  : {}));
61
65
  }
62
66
  catch (error) {
63
67
  Log_1.Log.warn('Failed to send batch:', error);
64
- return Object.assign({ success: false, statusCode: -1, failurePath: (_c = transportFailure.path) !== null && _c !== void 0 ? _c : failurePath }, (transportFailure.errorMessage
68
+ return Object.assign(Object.assign(Object.assign({ success: false, statusCode: -1, failurePath: (_c = transportFailure.path) !== null && _c !== void 0 ? _c : failurePath }, (transportFailure.errorMessage
65
69
  ? { failureErrorMessage: transportFailure.errorMessage }
70
+ : {})), (transportFailure.diagnosticBucket
71
+ ? { failureDiagnosticBucket: transportFailure.diagnosticBucket }
72
+ : {})), (transportFailure.diagnosticMetadata
73
+ ? { failureDiagnosticMetadata: transportFailure.diagnosticMetadata }
66
74
  : {}));
67
75
  }
68
76
  });
@@ -73,10 +81,14 @@ class EventSender {
73
81
  const result = yield this._network.post(this._getRequestData(batch), failureInfo);
74
82
  const code = (_a = result === null || result === void 0 ? void 0 : result.code) !== null && _a !== void 0 ? _a : -1;
75
83
  if (code === -1) {
76
- return Object.assign({ success: false, statusCode: -1, 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
77
85
  ? 'event_sender_post_returned_undefined'
78
86
  : 'event_sender_post_returned_null') }, (failureInfo.errorMessage
79
87
  ? { failureErrorMessage: failureInfo.errorMessage }
88
+ : {})), (failureInfo.diagnosticBucket
89
+ ? { failureDiagnosticBucket: failureInfo.diagnosticBucket }
90
+ : {})), (failureInfo.diagnosticMetadata
91
+ ? { failureDiagnosticMetadata: failureInfo.diagnosticMetadata }
80
92
  : {}));
81
93
  }
82
94
  return { success: code >= 200 && code < 300, statusCode: code };
@@ -85,10 +97,14 @@ class EventSender {
85
97
  _sendEventsViaBeacon(batch, failureInfo) {
86
98
  var _a;
87
99
  const success = this._network.beacon(this._getRequestData(batch), failureInfo);
88
- return Object.assign({ success, statusCode: success ? 200 : -1, failurePath: success
100
+ return Object.assign(Object.assign(Object.assign({ success, statusCode: success ? 200 : -1, failurePath: success
89
101
  ? undefined
90
102
  : (_a = failureInfo.path) !== null && _a !== void 0 ? _a : 'beacon_send_false' }, (!success && failureInfo.errorMessage
91
103
  ? { failureErrorMessage: failureInfo.errorMessage }
104
+ : {})), (!success && failureInfo.diagnosticBucket
105
+ ? { failureDiagnosticBucket: failureInfo.diagnosticBucket }
106
+ : {})), (!success && failureInfo.diagnosticMetadata
107
+ ? { failureDiagnosticMetadata: failureInfo.diagnosticMetadata }
92
108
  : {}));
93
109
  }
94
110
  _getRequestData(batch) {
@@ -261,7 +261,7 @@ class FlushCoordinator {
261
261
  return true;
262
262
  }
263
263
  this._flushInterval.adjustForFailure();
264
- this._handleFailure(batch, flushType, result.statusCode, result.failurePath, result.failureErrorMessage);
264
+ this._handleFailure(batch, flushType, result.statusCode, result.failurePath, result.failureErrorMessage, result.failureDiagnosticBucket, result.failureDiagnosticMetadata);
265
265
  return false;
266
266
  });
267
267
  }
@@ -301,7 +301,7 @@ class FlushCoordinator {
301
301
  }
302
302
  return false;
303
303
  }
304
- _handleFailure(batch, flushType, statusCode, failurePath, failureErrorMessage) {
304
+ _handleFailure(batch, flushType, statusCode, failurePath, failureErrorMessage, failureDiagnosticBucket, failureDiagnosticMetadata) {
305
305
  if (flushType === FlushTypes_1.FlushType.Shutdown) {
306
306
  Log_1.Log.warn(`${flushType} flush failed during shutdown. ` +
307
307
  `${batch.events.length} event(s) will be saved to storage for retry in next session.`);
@@ -309,15 +309,17 @@ class FlushCoordinator {
309
309
  return;
310
310
  }
311
311
  if (!this._isRetryableBatch(statusCode, failurePath)) {
312
+ const reason = `non-retryable error`;
312
313
  Log_1.Log.warn(`${flushType} flush failed after ${batch.attempts} attempt(s). ` +
313
314
  `${batch.events.length} event(s) will be dropped. Non-retryable error: ${statusCode}`);
314
- this._errorBoundary.logEventRequestFailure(batch.events.length, `non-retryable error`, flushType, statusCode, batch.attempts, failurePath, failureErrorMessage);
315
+ this._errorBoundary.logEventRequestFailure(batch.events.length, reason, flushType, statusCode, batch.attempts, failurePath, failureErrorMessage, failureDiagnosticBucket, failureDiagnosticMetadata);
315
316
  return;
316
317
  }
317
318
  if (batch.attempts >= EventRetryConstants_1.EventRetryConstants.MAX_RETRY_ATTEMPTS) {
319
+ const reason = `max retry attempts exceeded`;
318
320
  Log_1.Log.warn(`${flushType} flush failed after ${batch.attempts} attempt(s). ` +
319
321
  `${batch.events.length} event(s) will be dropped.`);
320
- this._errorBoundary.logEventRequestFailure(batch.events.length, `max retry attempts exceeded`, flushType, statusCode, batch.attempts, failurePath, failureErrorMessage);
322
+ this._errorBoundary.logEventRequestFailure(batch.events.length, reason, flushType, statusCode, batch.attempts, failurePath, failureErrorMessage, failureDiagnosticBucket, failureDiagnosticMetadata);
321
323
  return;
322
324
  }
323
325
  batch.incrementAttempts();
@@ -27,6 +27,8 @@ export type RequestArgsWithData = Flatten<RequestArgs & {
27
27
  export type RequestFailureInfo = {
28
28
  path?: string;
29
29
  errorMessage?: string;
30
+ diagnosticBucket?: string;
31
+ diagnosticMetadata?: Record<string, string>;
30
32
  };
31
33
  type BeaconRequestArgs = Pick<RequestArgsWithData, 'data' | 'sdkKey' | 'urlConfig' | 'params' | 'isCompressable' | 'attempt'>;
32
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 {
@@ -213,6 +214,14 @@ class NetworkCore {
213
214
  if (errorMessage) {
214
215
  failureInfo.errorMessage = errorMessage;
215
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
+ }
216
225
  }
217
226
  }
218
227
  return null;
@@ -389,6 +398,89 @@ function _didTimeout(errorMsg, abortedByTimeout) {
389
398
  const timeout = errorMsg.includes('Timeout'); // probably not needed but just in case
390
399
  return timeout || abortedByTimeout;
391
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
+ }
392
484
  function _tryMarkInitStart(args, attempt) {
393
485
  if (args.urlConfig.endpoint !== NetworkConfig_1.Endpoint._initialize) {
394
486
  return;
@@ -1,4 +1,4 @@
1
- export declare const SDK_VERSION = "3.32.5";
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.5';
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
  */