@analyticscli/sdk 0.1.0-preview.7 → 0.1.0-preview.8

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/dist/browser.cjs CHANGED
@@ -587,6 +587,28 @@ var sanitizeSurveyResponseInput = (input) => {
587
587
 
588
588
  // src/analytics-client.ts
589
589
  var DEFAULT_CONSENT_STORAGE_KEY = "analyticscli:consent:v1";
590
+ var AUTH_FAILURE_FLUSH_PAUSE_MS = 6e4;
591
+ var IngestSendError = class extends Error {
592
+ retryable;
593
+ attempts;
594
+ status;
595
+ errorCode;
596
+ serverMessage;
597
+ requestId;
598
+ constructor(input) {
599
+ super(input.message);
600
+ this.name = "IngestSendError";
601
+ this.retryable = input.retryable;
602
+ this.attempts = input.attempts;
603
+ this.status = input.status;
604
+ this.errorCode = input.errorCode;
605
+ this.serverMessage = input.serverMessage;
606
+ this.requestId = input.requestId;
607
+ if (input.cause !== void 0) {
608
+ this.cause = input.cause;
609
+ }
610
+ }
611
+ };
590
612
  var AnalyticsClient = class {
591
613
  apiKey;
592
614
  hasIngestConfig;
@@ -595,6 +617,7 @@ var AnalyticsClient = class {
595
617
  flushIntervalMs;
596
618
  maxRetries;
597
619
  debug;
620
+ onIngestError;
598
621
  platform;
599
622
  projectSurface;
600
623
  appVersion;
@@ -627,6 +650,7 @@ var AnalyticsClient = class {
627
650
  deferredEventsBeforeHydration = [];
628
651
  onboardingStepViewStateSessionId = null;
629
652
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
653
+ flushPausedUntilMs = 0;
630
654
  constructor(options) {
631
655
  const normalizedOptions = this.normalizeOptions(options);
632
656
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -639,6 +663,7 @@ var AnalyticsClient = class {
639
663
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
640
664
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
641
665
  this.debug = normalizedOptions.debug ?? false;
666
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
642
667
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
643
668
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
644
669
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
@@ -1102,6 +1127,9 @@ var AnalyticsClient = class {
1102
1127
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1103
1128
  return;
1104
1129
  }
1130
+ if (Date.now() < this.flushPausedUntilMs) {
1131
+ return;
1132
+ }
1105
1133
  this.isFlushing = true;
1106
1134
  const batch = this.queue.splice(0, this.batchSize);
1107
1135
  const payload = {
@@ -1116,9 +1144,20 @@ var AnalyticsClient = class {
1116
1144
  }
1117
1145
  try {
1118
1146
  await this.sendWithRetry(payload);
1147
+ this.flushPausedUntilMs = 0;
1119
1148
  } catch (error) {
1120
- this.log("Send failed permanently, requeueing batch", error);
1121
1149
  this.queue = [...batch, ...this.queue];
1150
+ const ingestError = this.toIngestSendError(error);
1151
+ const diagnostics = this.createIngestDiagnostics(ingestError, batch.length, this.queue.length);
1152
+ if (ingestError.status === 401 || ingestError.status === 403) {
1153
+ this.flushPausedUntilMs = Date.now() + AUTH_FAILURE_FLUSH_PAUSE_MS;
1154
+ this.log("Pausing ingest flush after auth failure", {
1155
+ status: ingestError.status,
1156
+ retryAfterMs: AUTH_FAILURE_FLUSH_PAUSE_MS
1157
+ });
1158
+ }
1159
+ this.log("Send failed permanently, requeueing batch", diagnostics);
1160
+ this.reportIngestError(diagnostics);
1122
1161
  } finally {
1123
1162
  this.isFlushing = false;
1124
1163
  }
@@ -1147,9 +1186,8 @@ var AnalyticsClient = class {
1147
1186
  });
1148
1187
  }
1149
1188
  async sendWithRetry(payload) {
1150
- let attempt = 0;
1151
1189
  let delay = 250;
1152
- while (attempt <= this.maxRetries) {
1190
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1153
1191
  try {
1154
1192
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1155
1193
  method: "POST",
@@ -1161,19 +1199,110 @@ var AnalyticsClient = class {
1161
1199
  keepalive: true
1162
1200
  });
1163
1201
  if (!response.ok) {
1164
- throw new Error(`ingest status=${response.status}`);
1202
+ throw await this.createHttpIngestSendError(response, attempt);
1165
1203
  }
1166
1204
  return;
1167
1205
  } catch (error) {
1168
- attempt += 1;
1169
- if (attempt > this.maxRetries) {
1170
- throw error;
1206
+ const normalized = this.toIngestSendError(error, attempt);
1207
+ const finalAttempt = attempt >= this.maxRetries + 1;
1208
+ this.log("Ingest attempt failed", {
1209
+ attempt: normalized.attempts,
1210
+ maxRetries: this.maxRetries,
1211
+ retryable: normalized.retryable,
1212
+ status: normalized.status,
1213
+ errorCode: normalized.errorCode,
1214
+ requestId: normalized.requestId,
1215
+ nextRetryInMs: !finalAttempt && normalized.retryable ? delay : null
1216
+ });
1217
+ if (finalAttempt || !normalized.retryable) {
1218
+ throw normalized;
1171
1219
  }
1172
1220
  await new Promise((resolve) => setTimeout(resolve, delay));
1173
1221
  delay *= 2;
1174
1222
  }
1175
1223
  }
1176
1224
  }
1225
+ async createHttpIngestSendError(response, attempts) {
1226
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("cf-ray") ?? void 0;
1227
+ let errorCode;
1228
+ let serverMessage;
1229
+ try {
1230
+ const parsed = await response.json();
1231
+ const errorBody = parsed && typeof parsed === "object" && parsed.error && typeof parsed.error === "object" ? parsed.error : void 0;
1232
+ if (typeof errorBody?.code === "string") {
1233
+ errorCode = errorBody.code;
1234
+ }
1235
+ if (typeof errorBody?.message === "string") {
1236
+ serverMessage = errorBody.message;
1237
+ }
1238
+ } catch {
1239
+ }
1240
+ const retryable = this.shouldRetryHttpStatus(response.status);
1241
+ const statusSuffix = errorCode ? ` ${errorCode}` : "";
1242
+ const message = `ingest status=${response.status}${statusSuffix}`;
1243
+ return new IngestSendError({
1244
+ message,
1245
+ retryable,
1246
+ attempts,
1247
+ status: response.status,
1248
+ errorCode,
1249
+ serverMessage,
1250
+ requestId
1251
+ });
1252
+ }
1253
+ shouldRetryHttpStatus(status) {
1254
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1255
+ }
1256
+ toIngestSendError(error, attempts) {
1257
+ if (error instanceof IngestSendError) {
1258
+ const resolvedAttempts = attempts ?? error.attempts;
1259
+ return new IngestSendError({
1260
+ message: error.message,
1261
+ retryable: error.retryable,
1262
+ attempts: resolvedAttempts,
1263
+ status: error.status,
1264
+ errorCode: error.errorCode,
1265
+ serverMessage: error.serverMessage,
1266
+ requestId: error.requestId,
1267
+ cause: error.cause
1268
+ });
1269
+ }
1270
+ const fallbackMessage = error instanceof Error ? error.message : "ingest request failed";
1271
+ return new IngestSendError({
1272
+ message: fallbackMessage,
1273
+ retryable: true,
1274
+ attempts: attempts ?? 1,
1275
+ cause: error
1276
+ });
1277
+ }
1278
+ createIngestDiagnostics(error, batchSize, queueSize) {
1279
+ return {
1280
+ name: "AnalyticsIngestError",
1281
+ message: error.message,
1282
+ endpoint: this.endpoint,
1283
+ path: "/v1/collect",
1284
+ status: error.status,
1285
+ errorCode: error.errorCode,
1286
+ serverMessage: error.serverMessage,
1287
+ requestId: error.requestId,
1288
+ retryable: error.retryable,
1289
+ attempts: error.attempts,
1290
+ maxRetries: this.maxRetries,
1291
+ batchSize,
1292
+ queueSize,
1293
+ timestamp: nowIso()
1294
+ };
1295
+ }
1296
+ reportIngestError(error) {
1297
+ if (!this.onIngestError) {
1298
+ return;
1299
+ }
1300
+ try {
1301
+ this.onIngestError(error);
1302
+ } catch (callbackError) {
1303
+ this.log("onIngestError callback threw", callbackError);
1304
+ }
1305
+ }
1177
1306
  parsePersistedConsent(raw) {
1178
1307
  if (raw === "granted") {
1179
1308
  return true;
@@ -1 +1 @@
1
- export { AnalyticsClient, AnalyticsClientOptions, AnalyticsConsentState, AnalyticsContext, AnalyticsContextConsentControls, AnalyticsContextUserControls, AnalyticsStorageAdapter, CreateAnalyticsContextOptions, EventContext, EventProperties, IdentityTrackingMode, InitInput, InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, OnboardingEventName, OnboardingEventProperties, OnboardingStepTracker, OnboardingSurveyAnswerType, OnboardingSurveyEventName, OnboardingSurveyResponseInput, OnboardingTracker, OnboardingTrackerDefaults, OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, PaywallEventName, PaywallEventProperties, PaywallJourneyEventName, PaywallTracker, PaywallTrackerDefaults, PaywallTrackerProperties, PurchaseEventName, SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync } from './index.cjs';
1
+ export { AnalyticsClient, AnalyticsClientOptions, AnalyticsConsentState, AnalyticsContext, AnalyticsContextConsentControls, AnalyticsContextUserControls, AnalyticsIngestError, AnalyticsIngestErrorHandler, AnalyticsStorageAdapter, CreateAnalyticsContextOptions, EventContext, EventProperties, IdentityTrackingMode, InitInput, InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, OnboardingEventName, OnboardingEventProperties, OnboardingStepTracker, OnboardingSurveyAnswerType, OnboardingSurveyEventName, OnboardingSurveyResponseInput, OnboardingTracker, OnboardingTrackerDefaults, OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, PaywallEventName, PaywallEventProperties, PaywallJourneyEventName, PaywallTracker, PaywallTrackerDefaults, PaywallTrackerProperties, PurchaseEventName, SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync } from './index.cjs';
package/dist/browser.d.ts CHANGED
@@ -1 +1 @@
1
- export { AnalyticsClient, AnalyticsClientOptions, AnalyticsConsentState, AnalyticsContext, AnalyticsContextConsentControls, AnalyticsContextUserControls, AnalyticsStorageAdapter, CreateAnalyticsContextOptions, EventContext, EventProperties, IdentityTrackingMode, InitInput, InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, OnboardingEventName, OnboardingEventProperties, OnboardingStepTracker, OnboardingSurveyAnswerType, OnboardingSurveyEventName, OnboardingSurveyResponseInput, OnboardingTracker, OnboardingTrackerDefaults, OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, PaywallEventName, PaywallEventProperties, PaywallJourneyEventName, PaywallTracker, PaywallTrackerDefaults, PaywallTrackerProperties, PurchaseEventName, SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync } from './index.js';
1
+ export { AnalyticsClient, AnalyticsClientOptions, AnalyticsConsentState, AnalyticsContext, AnalyticsContextConsentControls, AnalyticsContextUserControls, AnalyticsIngestError, AnalyticsIngestErrorHandler, AnalyticsStorageAdapter, CreateAnalyticsContextOptions, EventContext, EventProperties, IdentityTrackingMode, InitInput, InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, OnboardingEventName, OnboardingEventProperties, OnboardingStepTracker, OnboardingSurveyAnswerType, OnboardingSurveyEventName, OnboardingSurveyResponseInput, OnboardingTracker, OnboardingTrackerDefaults, OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, PaywallEventName, PaywallEventProperties, PaywallJourneyEventName, PaywallTracker, PaywallTrackerDefaults, PaywallTrackerProperties, PurchaseEventName, SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync } from './index.js';
package/dist/browser.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  initAsync,
16
16
  initConsentFirst,
17
17
  initConsentFirstAsync
18
- } from "./chunk-O4FIH647.js";
18
+ } from "./chunk-S3MLRL5U.js";
19
19
  export {
20
20
  AnalyticsClient,
21
21
  ONBOARDING_EVENTS,
@@ -546,6 +546,28 @@ var sanitizeSurveyResponseInput = (input) => {
546
546
 
547
547
  // src/analytics-client.ts
548
548
  var DEFAULT_CONSENT_STORAGE_KEY = "analyticscli:consent:v1";
549
+ var AUTH_FAILURE_FLUSH_PAUSE_MS = 6e4;
550
+ var IngestSendError = class extends Error {
551
+ retryable;
552
+ attempts;
553
+ status;
554
+ errorCode;
555
+ serverMessage;
556
+ requestId;
557
+ constructor(input) {
558
+ super(input.message);
559
+ this.name = "IngestSendError";
560
+ this.retryable = input.retryable;
561
+ this.attempts = input.attempts;
562
+ this.status = input.status;
563
+ this.errorCode = input.errorCode;
564
+ this.serverMessage = input.serverMessage;
565
+ this.requestId = input.requestId;
566
+ if (input.cause !== void 0) {
567
+ this.cause = input.cause;
568
+ }
569
+ }
570
+ };
549
571
  var AnalyticsClient = class {
550
572
  apiKey;
551
573
  hasIngestConfig;
@@ -554,6 +576,7 @@ var AnalyticsClient = class {
554
576
  flushIntervalMs;
555
577
  maxRetries;
556
578
  debug;
579
+ onIngestError;
557
580
  platform;
558
581
  projectSurface;
559
582
  appVersion;
@@ -586,6 +609,7 @@ var AnalyticsClient = class {
586
609
  deferredEventsBeforeHydration = [];
587
610
  onboardingStepViewStateSessionId = null;
588
611
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
612
+ flushPausedUntilMs = 0;
589
613
  constructor(options) {
590
614
  const normalizedOptions = this.normalizeOptions(options);
591
615
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -598,6 +622,7 @@ var AnalyticsClient = class {
598
622
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
599
623
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
600
624
  this.debug = normalizedOptions.debug ?? false;
625
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
601
626
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
602
627
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
603
628
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
@@ -1061,6 +1086,9 @@ var AnalyticsClient = class {
1061
1086
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1062
1087
  return;
1063
1088
  }
1089
+ if (Date.now() < this.flushPausedUntilMs) {
1090
+ return;
1091
+ }
1064
1092
  this.isFlushing = true;
1065
1093
  const batch = this.queue.splice(0, this.batchSize);
1066
1094
  const payload = {
@@ -1075,9 +1103,20 @@ var AnalyticsClient = class {
1075
1103
  }
1076
1104
  try {
1077
1105
  await this.sendWithRetry(payload);
1106
+ this.flushPausedUntilMs = 0;
1078
1107
  } catch (error) {
1079
- this.log("Send failed permanently, requeueing batch", error);
1080
1108
  this.queue = [...batch, ...this.queue];
1109
+ const ingestError = this.toIngestSendError(error);
1110
+ const diagnostics = this.createIngestDiagnostics(ingestError, batch.length, this.queue.length);
1111
+ if (ingestError.status === 401 || ingestError.status === 403) {
1112
+ this.flushPausedUntilMs = Date.now() + AUTH_FAILURE_FLUSH_PAUSE_MS;
1113
+ this.log("Pausing ingest flush after auth failure", {
1114
+ status: ingestError.status,
1115
+ retryAfterMs: AUTH_FAILURE_FLUSH_PAUSE_MS
1116
+ });
1117
+ }
1118
+ this.log("Send failed permanently, requeueing batch", diagnostics);
1119
+ this.reportIngestError(diagnostics);
1081
1120
  } finally {
1082
1121
  this.isFlushing = false;
1083
1122
  }
@@ -1106,9 +1145,8 @@ var AnalyticsClient = class {
1106
1145
  });
1107
1146
  }
1108
1147
  async sendWithRetry(payload) {
1109
- let attempt = 0;
1110
1148
  let delay = 250;
1111
- while (attempt <= this.maxRetries) {
1149
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1112
1150
  try {
1113
1151
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1114
1152
  method: "POST",
@@ -1120,19 +1158,110 @@ var AnalyticsClient = class {
1120
1158
  keepalive: true
1121
1159
  });
1122
1160
  if (!response.ok) {
1123
- throw new Error(`ingest status=${response.status}`);
1161
+ throw await this.createHttpIngestSendError(response, attempt);
1124
1162
  }
1125
1163
  return;
1126
1164
  } catch (error) {
1127
- attempt += 1;
1128
- if (attempt > this.maxRetries) {
1129
- throw error;
1165
+ const normalized = this.toIngestSendError(error, attempt);
1166
+ const finalAttempt = attempt >= this.maxRetries + 1;
1167
+ this.log("Ingest attempt failed", {
1168
+ attempt: normalized.attempts,
1169
+ maxRetries: this.maxRetries,
1170
+ retryable: normalized.retryable,
1171
+ status: normalized.status,
1172
+ errorCode: normalized.errorCode,
1173
+ requestId: normalized.requestId,
1174
+ nextRetryInMs: !finalAttempt && normalized.retryable ? delay : null
1175
+ });
1176
+ if (finalAttempt || !normalized.retryable) {
1177
+ throw normalized;
1130
1178
  }
1131
1179
  await new Promise((resolve) => setTimeout(resolve, delay));
1132
1180
  delay *= 2;
1133
1181
  }
1134
1182
  }
1135
1183
  }
1184
+ async createHttpIngestSendError(response, attempts) {
1185
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("cf-ray") ?? void 0;
1186
+ let errorCode;
1187
+ let serverMessage;
1188
+ try {
1189
+ const parsed = await response.json();
1190
+ const errorBody = parsed && typeof parsed === "object" && parsed.error && typeof parsed.error === "object" ? parsed.error : void 0;
1191
+ if (typeof errorBody?.code === "string") {
1192
+ errorCode = errorBody.code;
1193
+ }
1194
+ if (typeof errorBody?.message === "string") {
1195
+ serverMessage = errorBody.message;
1196
+ }
1197
+ } catch {
1198
+ }
1199
+ const retryable = this.shouldRetryHttpStatus(response.status);
1200
+ const statusSuffix = errorCode ? ` ${errorCode}` : "";
1201
+ const message = `ingest status=${response.status}${statusSuffix}`;
1202
+ return new IngestSendError({
1203
+ message,
1204
+ retryable,
1205
+ attempts,
1206
+ status: response.status,
1207
+ errorCode,
1208
+ serverMessage,
1209
+ requestId
1210
+ });
1211
+ }
1212
+ shouldRetryHttpStatus(status) {
1213
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1214
+ }
1215
+ toIngestSendError(error, attempts) {
1216
+ if (error instanceof IngestSendError) {
1217
+ const resolvedAttempts = attempts ?? error.attempts;
1218
+ return new IngestSendError({
1219
+ message: error.message,
1220
+ retryable: error.retryable,
1221
+ attempts: resolvedAttempts,
1222
+ status: error.status,
1223
+ errorCode: error.errorCode,
1224
+ serverMessage: error.serverMessage,
1225
+ requestId: error.requestId,
1226
+ cause: error.cause
1227
+ });
1228
+ }
1229
+ const fallbackMessage = error instanceof Error ? error.message : "ingest request failed";
1230
+ return new IngestSendError({
1231
+ message: fallbackMessage,
1232
+ retryable: true,
1233
+ attempts: attempts ?? 1,
1234
+ cause: error
1235
+ });
1236
+ }
1237
+ createIngestDiagnostics(error, batchSize, queueSize) {
1238
+ return {
1239
+ name: "AnalyticsIngestError",
1240
+ message: error.message,
1241
+ endpoint: this.endpoint,
1242
+ path: "/v1/collect",
1243
+ status: error.status,
1244
+ errorCode: error.errorCode,
1245
+ serverMessage: error.serverMessage,
1246
+ requestId: error.requestId,
1247
+ retryable: error.retryable,
1248
+ attempts: error.attempts,
1249
+ maxRetries: this.maxRetries,
1250
+ batchSize,
1251
+ queueSize,
1252
+ timestamp: nowIso()
1253
+ };
1254
+ }
1255
+ reportIngestError(error) {
1256
+ if (!this.onIngestError) {
1257
+ return;
1258
+ }
1259
+ try {
1260
+ this.onIngestError(error);
1261
+ } catch (callbackError) {
1262
+ this.log("onIngestError callback threw", callbackError);
1263
+ }
1264
+ }
1136
1265
  parsePersistedConsent(raw) {
1137
1266
  if (raw === "granted") {
1138
1267
  return true;
package/dist/index.cjs CHANGED
@@ -587,6 +587,28 @@ var sanitizeSurveyResponseInput = (input) => {
587
587
 
588
588
  // src/analytics-client.ts
589
589
  var DEFAULT_CONSENT_STORAGE_KEY = "analyticscli:consent:v1";
590
+ var AUTH_FAILURE_FLUSH_PAUSE_MS = 6e4;
591
+ var IngestSendError = class extends Error {
592
+ retryable;
593
+ attempts;
594
+ status;
595
+ errorCode;
596
+ serverMessage;
597
+ requestId;
598
+ constructor(input) {
599
+ super(input.message);
600
+ this.name = "IngestSendError";
601
+ this.retryable = input.retryable;
602
+ this.attempts = input.attempts;
603
+ this.status = input.status;
604
+ this.errorCode = input.errorCode;
605
+ this.serverMessage = input.serverMessage;
606
+ this.requestId = input.requestId;
607
+ if (input.cause !== void 0) {
608
+ this.cause = input.cause;
609
+ }
610
+ }
611
+ };
590
612
  var AnalyticsClient = class {
591
613
  apiKey;
592
614
  hasIngestConfig;
@@ -595,6 +617,7 @@ var AnalyticsClient = class {
595
617
  flushIntervalMs;
596
618
  maxRetries;
597
619
  debug;
620
+ onIngestError;
598
621
  platform;
599
622
  projectSurface;
600
623
  appVersion;
@@ -627,6 +650,7 @@ var AnalyticsClient = class {
627
650
  deferredEventsBeforeHydration = [];
628
651
  onboardingStepViewStateSessionId = null;
629
652
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
653
+ flushPausedUntilMs = 0;
630
654
  constructor(options) {
631
655
  const normalizedOptions = this.normalizeOptions(options);
632
656
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -639,6 +663,7 @@ var AnalyticsClient = class {
639
663
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
640
664
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
641
665
  this.debug = normalizedOptions.debug ?? false;
666
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
642
667
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
643
668
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
644
669
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
@@ -1102,6 +1127,9 @@ var AnalyticsClient = class {
1102
1127
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1103
1128
  return;
1104
1129
  }
1130
+ if (Date.now() < this.flushPausedUntilMs) {
1131
+ return;
1132
+ }
1105
1133
  this.isFlushing = true;
1106
1134
  const batch = this.queue.splice(0, this.batchSize);
1107
1135
  const payload = {
@@ -1116,9 +1144,20 @@ var AnalyticsClient = class {
1116
1144
  }
1117
1145
  try {
1118
1146
  await this.sendWithRetry(payload);
1147
+ this.flushPausedUntilMs = 0;
1119
1148
  } catch (error) {
1120
- this.log("Send failed permanently, requeueing batch", error);
1121
1149
  this.queue = [...batch, ...this.queue];
1150
+ const ingestError = this.toIngestSendError(error);
1151
+ const diagnostics = this.createIngestDiagnostics(ingestError, batch.length, this.queue.length);
1152
+ if (ingestError.status === 401 || ingestError.status === 403) {
1153
+ this.flushPausedUntilMs = Date.now() + AUTH_FAILURE_FLUSH_PAUSE_MS;
1154
+ this.log("Pausing ingest flush after auth failure", {
1155
+ status: ingestError.status,
1156
+ retryAfterMs: AUTH_FAILURE_FLUSH_PAUSE_MS
1157
+ });
1158
+ }
1159
+ this.log("Send failed permanently, requeueing batch", diagnostics);
1160
+ this.reportIngestError(diagnostics);
1122
1161
  } finally {
1123
1162
  this.isFlushing = false;
1124
1163
  }
@@ -1147,9 +1186,8 @@ var AnalyticsClient = class {
1147
1186
  });
1148
1187
  }
1149
1188
  async sendWithRetry(payload) {
1150
- let attempt = 0;
1151
1189
  let delay = 250;
1152
- while (attempt <= this.maxRetries) {
1190
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1153
1191
  try {
1154
1192
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1155
1193
  method: "POST",
@@ -1161,19 +1199,110 @@ var AnalyticsClient = class {
1161
1199
  keepalive: true
1162
1200
  });
1163
1201
  if (!response.ok) {
1164
- throw new Error(`ingest status=${response.status}`);
1202
+ throw await this.createHttpIngestSendError(response, attempt);
1165
1203
  }
1166
1204
  return;
1167
1205
  } catch (error) {
1168
- attempt += 1;
1169
- if (attempt > this.maxRetries) {
1170
- throw error;
1206
+ const normalized = this.toIngestSendError(error, attempt);
1207
+ const finalAttempt = attempt >= this.maxRetries + 1;
1208
+ this.log("Ingest attempt failed", {
1209
+ attempt: normalized.attempts,
1210
+ maxRetries: this.maxRetries,
1211
+ retryable: normalized.retryable,
1212
+ status: normalized.status,
1213
+ errorCode: normalized.errorCode,
1214
+ requestId: normalized.requestId,
1215
+ nextRetryInMs: !finalAttempt && normalized.retryable ? delay : null
1216
+ });
1217
+ if (finalAttempt || !normalized.retryable) {
1218
+ throw normalized;
1171
1219
  }
1172
1220
  await new Promise((resolve) => setTimeout(resolve, delay));
1173
1221
  delay *= 2;
1174
1222
  }
1175
1223
  }
1176
1224
  }
1225
+ async createHttpIngestSendError(response, attempts) {
1226
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("cf-ray") ?? void 0;
1227
+ let errorCode;
1228
+ let serverMessage;
1229
+ try {
1230
+ const parsed = await response.json();
1231
+ const errorBody = parsed && typeof parsed === "object" && parsed.error && typeof parsed.error === "object" ? parsed.error : void 0;
1232
+ if (typeof errorBody?.code === "string") {
1233
+ errorCode = errorBody.code;
1234
+ }
1235
+ if (typeof errorBody?.message === "string") {
1236
+ serverMessage = errorBody.message;
1237
+ }
1238
+ } catch {
1239
+ }
1240
+ const retryable = this.shouldRetryHttpStatus(response.status);
1241
+ const statusSuffix = errorCode ? ` ${errorCode}` : "";
1242
+ const message = `ingest status=${response.status}${statusSuffix}`;
1243
+ return new IngestSendError({
1244
+ message,
1245
+ retryable,
1246
+ attempts,
1247
+ status: response.status,
1248
+ errorCode,
1249
+ serverMessage,
1250
+ requestId
1251
+ });
1252
+ }
1253
+ shouldRetryHttpStatus(status) {
1254
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1255
+ }
1256
+ toIngestSendError(error, attempts) {
1257
+ if (error instanceof IngestSendError) {
1258
+ const resolvedAttempts = attempts ?? error.attempts;
1259
+ return new IngestSendError({
1260
+ message: error.message,
1261
+ retryable: error.retryable,
1262
+ attempts: resolvedAttempts,
1263
+ status: error.status,
1264
+ errorCode: error.errorCode,
1265
+ serverMessage: error.serverMessage,
1266
+ requestId: error.requestId,
1267
+ cause: error.cause
1268
+ });
1269
+ }
1270
+ const fallbackMessage = error instanceof Error ? error.message : "ingest request failed";
1271
+ return new IngestSendError({
1272
+ message: fallbackMessage,
1273
+ retryable: true,
1274
+ attempts: attempts ?? 1,
1275
+ cause: error
1276
+ });
1277
+ }
1278
+ createIngestDiagnostics(error, batchSize, queueSize) {
1279
+ return {
1280
+ name: "AnalyticsIngestError",
1281
+ message: error.message,
1282
+ endpoint: this.endpoint,
1283
+ path: "/v1/collect",
1284
+ status: error.status,
1285
+ errorCode: error.errorCode,
1286
+ serverMessage: error.serverMessage,
1287
+ requestId: error.requestId,
1288
+ retryable: error.retryable,
1289
+ attempts: error.attempts,
1290
+ maxRetries: this.maxRetries,
1291
+ batchSize,
1292
+ queueSize,
1293
+ timestamp: nowIso()
1294
+ };
1295
+ }
1296
+ reportIngestError(error) {
1297
+ if (!this.onIngestError) {
1298
+ return;
1299
+ }
1300
+ try {
1301
+ this.onIngestError(error);
1302
+ } catch (callbackError) {
1303
+ this.log("onIngestError callback threw", callbackError);
1304
+ }
1305
+ }
1177
1306
  parsePersistedConsent(raw) {
1178
1307
  if (raw === "granted") {
1179
1308
  return true;
package/dist/index.d.cts CHANGED
@@ -130,6 +130,65 @@ type PaywallTracker = {
130
130
  };
131
131
  type AnalyticsConsentState = 'granted' | 'denied' | 'unknown';
132
132
  type IdentityTrackingMode = 'strict' | 'consent_gated' | 'always_on';
133
+ type AnalyticsIngestError = {
134
+ /**
135
+ * Stable error name for host-app monitoring.
136
+ */
137
+ name: 'AnalyticsIngestError';
138
+ /**
139
+ * Human-readable summary of the ingest failure.
140
+ */
141
+ message: string;
142
+ /**
143
+ * Collector endpoint base URL configured in the SDK client.
144
+ */
145
+ endpoint: string;
146
+ /**
147
+ * Collector path that failed.
148
+ */
149
+ path: '/v1/collect';
150
+ /**
151
+ * HTTP status when available.
152
+ */
153
+ status?: number;
154
+ /**
155
+ * Structured server error code when available.
156
+ */
157
+ errorCode?: string;
158
+ /**
159
+ * Structured server message when available.
160
+ */
161
+ serverMessage?: string;
162
+ /**
163
+ * Request correlation id when exposed by the collector response.
164
+ */
165
+ requestId?: string;
166
+ /**
167
+ * Whether retrying can help (`true` for network/5xx/429 class failures).
168
+ */
169
+ retryable: boolean;
170
+ /**
171
+ * Number of attempts that were made for this batch.
172
+ */
173
+ attempts: number;
174
+ /**
175
+ * SDK max retries configured on the client.
176
+ */
177
+ maxRetries: number;
178
+ /**
179
+ * Number of events in the failed batch.
180
+ */
181
+ batchSize: number;
182
+ /**
183
+ * Current queue size after requeue.
184
+ */
185
+ queueSize: number;
186
+ /**
187
+ * ISO timestamp when the failure was surfaced to host-app callbacks.
188
+ */
189
+ timestamp: string;
190
+ };
191
+ type AnalyticsIngestErrorHandler = (error: AnalyticsIngestError) => void;
133
192
  type SetConsentOptions = {
134
193
  /**
135
194
  * Whether consent state should be persisted to storage when enabled.
@@ -158,6 +217,14 @@ type AnalyticsClientOptions = {
158
217
  * `debug: __DEV__`
159
218
  */
160
219
  debug?: boolean | null;
220
+ /**
221
+ * Optional host-app hook for ingest delivery failures.
222
+ * Use this to forward operational diagnostics to your own monitoring stack.
223
+ *
224
+ * GDPR recommendation:
225
+ * forward this structured metadata only and avoid attaching event payloads or raw identifiers.
226
+ */
227
+ onIngestError?: AnalyticsIngestErrorHandler | null;
161
228
  /**
162
229
  * Optional platform hint.
163
230
  * React Native/Expo: passing `Platform.OS` directly is supported.
@@ -246,6 +313,7 @@ declare class AnalyticsClient {
246
313
  private readonly flushIntervalMs;
247
314
  private readonly maxRetries;
248
315
  private readonly debug;
316
+ private readonly onIngestError;
249
317
  private readonly platform;
250
318
  private readonly projectSurface;
251
319
  private readonly appVersion;
@@ -278,6 +346,7 @@ declare class AnalyticsClient {
278
346
  private deferredEventsBeforeHydration;
279
347
  private onboardingStepViewStateSessionId;
280
348
  private onboardingStepViewsSeen;
349
+ private flushPausedUntilMs;
281
350
  constructor(options: AnalyticsClientOptions);
282
351
  /**
283
352
  * Resolves once client initialization work completes.
@@ -373,6 +442,11 @@ declare class AnalyticsClient {
373
442
  private enqueue;
374
443
  private scheduleFlush;
375
444
  private sendWithRetry;
445
+ private createHttpIngestSendError;
446
+ private shouldRetryHttpStatus;
447
+ private toIngestSendError;
448
+ private createIngestDiagnostics;
449
+ private reportIngestError;
376
450
  private parsePersistedConsent;
377
451
  private readPersistedConsentSync;
378
452
  private readPersistedConsentAsync;
@@ -489,4 +563,4 @@ declare const initConsentFirst: (input?: InitInput) => AnalyticsClient;
489
563
  declare const initAsync: (input?: InitInput) => Promise<AnalyticsClient>;
490
564
  declare const initConsentFirstAsync: (input?: InitInput) => Promise<AnalyticsClient>;
491
565
 
492
- export { AnalyticsClient, type AnalyticsClientOptions, type AnalyticsConsentState, type AnalyticsContext, type AnalyticsContextConsentControls, type AnalyticsContextUserControls, type AnalyticsStorageAdapter, type CreateAnalyticsContextOptions, type EventContext, type EventProperties, type IdentityTrackingMode, type InitInput, type InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, type OnboardingEventName, type OnboardingEventProperties, type OnboardingStepTracker, type OnboardingSurveyAnswerType, type OnboardingSurveyEventName, type OnboardingSurveyResponseInput, type OnboardingTracker, type OnboardingTrackerDefaults, type OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, type PaywallEventName, type PaywallEventProperties, type PaywallJourneyEventName, type PaywallTracker, type PaywallTrackerDefaults, type PaywallTrackerProperties, type PurchaseEventName, type SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync };
566
+ export { AnalyticsClient, type AnalyticsClientOptions, type AnalyticsConsentState, type AnalyticsContext, type AnalyticsContextConsentControls, type AnalyticsContextUserControls, type AnalyticsIngestError, type AnalyticsIngestErrorHandler, type AnalyticsStorageAdapter, type CreateAnalyticsContextOptions, type EventContext, type EventProperties, type IdentityTrackingMode, type InitInput, type InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, type OnboardingEventName, type OnboardingEventProperties, type OnboardingStepTracker, type OnboardingSurveyAnswerType, type OnboardingSurveyEventName, type OnboardingSurveyResponseInput, type OnboardingTracker, type OnboardingTrackerDefaults, type OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, type PaywallEventName, type PaywallEventProperties, type PaywallJourneyEventName, type PaywallTracker, type PaywallTrackerDefaults, type PaywallTrackerProperties, type PurchaseEventName, type SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync };
package/dist/index.d.ts CHANGED
@@ -130,6 +130,65 @@ type PaywallTracker = {
130
130
  };
131
131
  type AnalyticsConsentState = 'granted' | 'denied' | 'unknown';
132
132
  type IdentityTrackingMode = 'strict' | 'consent_gated' | 'always_on';
133
+ type AnalyticsIngestError = {
134
+ /**
135
+ * Stable error name for host-app monitoring.
136
+ */
137
+ name: 'AnalyticsIngestError';
138
+ /**
139
+ * Human-readable summary of the ingest failure.
140
+ */
141
+ message: string;
142
+ /**
143
+ * Collector endpoint base URL configured in the SDK client.
144
+ */
145
+ endpoint: string;
146
+ /**
147
+ * Collector path that failed.
148
+ */
149
+ path: '/v1/collect';
150
+ /**
151
+ * HTTP status when available.
152
+ */
153
+ status?: number;
154
+ /**
155
+ * Structured server error code when available.
156
+ */
157
+ errorCode?: string;
158
+ /**
159
+ * Structured server message when available.
160
+ */
161
+ serverMessage?: string;
162
+ /**
163
+ * Request correlation id when exposed by the collector response.
164
+ */
165
+ requestId?: string;
166
+ /**
167
+ * Whether retrying can help (`true` for network/5xx/429 class failures).
168
+ */
169
+ retryable: boolean;
170
+ /**
171
+ * Number of attempts that were made for this batch.
172
+ */
173
+ attempts: number;
174
+ /**
175
+ * SDK max retries configured on the client.
176
+ */
177
+ maxRetries: number;
178
+ /**
179
+ * Number of events in the failed batch.
180
+ */
181
+ batchSize: number;
182
+ /**
183
+ * Current queue size after requeue.
184
+ */
185
+ queueSize: number;
186
+ /**
187
+ * ISO timestamp when the failure was surfaced to host-app callbacks.
188
+ */
189
+ timestamp: string;
190
+ };
191
+ type AnalyticsIngestErrorHandler = (error: AnalyticsIngestError) => void;
133
192
  type SetConsentOptions = {
134
193
  /**
135
194
  * Whether consent state should be persisted to storage when enabled.
@@ -158,6 +217,14 @@ type AnalyticsClientOptions = {
158
217
  * `debug: __DEV__`
159
218
  */
160
219
  debug?: boolean | null;
220
+ /**
221
+ * Optional host-app hook for ingest delivery failures.
222
+ * Use this to forward operational diagnostics to your own monitoring stack.
223
+ *
224
+ * GDPR recommendation:
225
+ * forward this structured metadata only and avoid attaching event payloads or raw identifiers.
226
+ */
227
+ onIngestError?: AnalyticsIngestErrorHandler | null;
161
228
  /**
162
229
  * Optional platform hint.
163
230
  * React Native/Expo: passing `Platform.OS` directly is supported.
@@ -246,6 +313,7 @@ declare class AnalyticsClient {
246
313
  private readonly flushIntervalMs;
247
314
  private readonly maxRetries;
248
315
  private readonly debug;
316
+ private readonly onIngestError;
249
317
  private readonly platform;
250
318
  private readonly projectSurface;
251
319
  private readonly appVersion;
@@ -278,6 +346,7 @@ declare class AnalyticsClient {
278
346
  private deferredEventsBeforeHydration;
279
347
  private onboardingStepViewStateSessionId;
280
348
  private onboardingStepViewsSeen;
349
+ private flushPausedUntilMs;
281
350
  constructor(options: AnalyticsClientOptions);
282
351
  /**
283
352
  * Resolves once client initialization work completes.
@@ -373,6 +442,11 @@ declare class AnalyticsClient {
373
442
  private enqueue;
374
443
  private scheduleFlush;
375
444
  private sendWithRetry;
445
+ private createHttpIngestSendError;
446
+ private shouldRetryHttpStatus;
447
+ private toIngestSendError;
448
+ private createIngestDiagnostics;
449
+ private reportIngestError;
376
450
  private parsePersistedConsent;
377
451
  private readPersistedConsentSync;
378
452
  private readPersistedConsentAsync;
@@ -489,4 +563,4 @@ declare const initConsentFirst: (input?: InitInput) => AnalyticsClient;
489
563
  declare const initAsync: (input?: InitInput) => Promise<AnalyticsClient>;
490
564
  declare const initConsentFirstAsync: (input?: InitInput) => Promise<AnalyticsClient>;
491
565
 
492
- export { AnalyticsClient, type AnalyticsClientOptions, type AnalyticsConsentState, type AnalyticsContext, type AnalyticsContextConsentControls, type AnalyticsContextUserControls, type AnalyticsStorageAdapter, type CreateAnalyticsContextOptions, type EventContext, type EventProperties, type IdentityTrackingMode, type InitInput, type InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, type OnboardingEventName, type OnboardingEventProperties, type OnboardingStepTracker, type OnboardingSurveyAnswerType, type OnboardingSurveyEventName, type OnboardingSurveyResponseInput, type OnboardingTracker, type OnboardingTrackerDefaults, type OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, type PaywallEventName, type PaywallEventProperties, type PaywallJourneyEventName, type PaywallTracker, type PaywallTrackerDefaults, type PaywallTrackerProperties, type PurchaseEventName, type SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync };
566
+ export { AnalyticsClient, type AnalyticsClientOptions, type AnalyticsConsentState, type AnalyticsContext, type AnalyticsContextConsentControls, type AnalyticsContextUserControls, type AnalyticsIngestError, type AnalyticsIngestErrorHandler, type AnalyticsStorageAdapter, type CreateAnalyticsContextOptions, type EventContext, type EventProperties, type IdentityTrackingMode, type InitInput, type InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, type OnboardingEventName, type OnboardingEventProperties, type OnboardingStepTracker, type OnboardingSurveyAnswerType, type OnboardingSurveyEventName, type OnboardingSurveyResponseInput, type OnboardingTracker, type OnboardingTrackerDefaults, type OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, type PaywallEventName, type PaywallEventProperties, type PaywallJourneyEventName, type PaywallTracker, type PaywallTrackerDefaults, type PaywallTrackerProperties, type PurchaseEventName, type SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync };
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  initAsync,
16
16
  initConsentFirst,
17
17
  initConsentFirstAsync
18
- } from "./chunk-O4FIH647.js";
18
+ } from "./chunk-S3MLRL5U.js";
19
19
  export {
20
20
  AnalyticsClient,
21
21
  ONBOARDING_EVENTS,
@@ -587,6 +587,28 @@ var sanitizeSurveyResponseInput = (input) => {
587
587
 
588
588
  // src/analytics-client.ts
589
589
  var DEFAULT_CONSENT_STORAGE_KEY = "analyticscli:consent:v1";
590
+ var AUTH_FAILURE_FLUSH_PAUSE_MS = 6e4;
591
+ var IngestSendError = class extends Error {
592
+ retryable;
593
+ attempts;
594
+ status;
595
+ errorCode;
596
+ serverMessage;
597
+ requestId;
598
+ constructor(input) {
599
+ super(input.message);
600
+ this.name = "IngestSendError";
601
+ this.retryable = input.retryable;
602
+ this.attempts = input.attempts;
603
+ this.status = input.status;
604
+ this.errorCode = input.errorCode;
605
+ this.serverMessage = input.serverMessage;
606
+ this.requestId = input.requestId;
607
+ if (input.cause !== void 0) {
608
+ this.cause = input.cause;
609
+ }
610
+ }
611
+ };
590
612
  var AnalyticsClient = class {
591
613
  apiKey;
592
614
  hasIngestConfig;
@@ -595,6 +617,7 @@ var AnalyticsClient = class {
595
617
  flushIntervalMs;
596
618
  maxRetries;
597
619
  debug;
620
+ onIngestError;
598
621
  platform;
599
622
  projectSurface;
600
623
  appVersion;
@@ -627,6 +650,7 @@ var AnalyticsClient = class {
627
650
  deferredEventsBeforeHydration = [];
628
651
  onboardingStepViewStateSessionId = null;
629
652
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
653
+ flushPausedUntilMs = 0;
630
654
  constructor(options) {
631
655
  const normalizedOptions = this.normalizeOptions(options);
632
656
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -639,6 +663,7 @@ var AnalyticsClient = class {
639
663
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
640
664
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
641
665
  this.debug = normalizedOptions.debug ?? false;
666
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
642
667
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
643
668
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
644
669
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
@@ -1102,6 +1127,9 @@ var AnalyticsClient = class {
1102
1127
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1103
1128
  return;
1104
1129
  }
1130
+ if (Date.now() < this.flushPausedUntilMs) {
1131
+ return;
1132
+ }
1105
1133
  this.isFlushing = true;
1106
1134
  const batch = this.queue.splice(0, this.batchSize);
1107
1135
  const payload = {
@@ -1116,9 +1144,20 @@ var AnalyticsClient = class {
1116
1144
  }
1117
1145
  try {
1118
1146
  await this.sendWithRetry(payload);
1147
+ this.flushPausedUntilMs = 0;
1119
1148
  } catch (error) {
1120
- this.log("Send failed permanently, requeueing batch", error);
1121
1149
  this.queue = [...batch, ...this.queue];
1150
+ const ingestError = this.toIngestSendError(error);
1151
+ const diagnostics = this.createIngestDiagnostics(ingestError, batch.length, this.queue.length);
1152
+ if (ingestError.status === 401 || ingestError.status === 403) {
1153
+ this.flushPausedUntilMs = Date.now() + AUTH_FAILURE_FLUSH_PAUSE_MS;
1154
+ this.log("Pausing ingest flush after auth failure", {
1155
+ status: ingestError.status,
1156
+ retryAfterMs: AUTH_FAILURE_FLUSH_PAUSE_MS
1157
+ });
1158
+ }
1159
+ this.log("Send failed permanently, requeueing batch", diagnostics);
1160
+ this.reportIngestError(diagnostics);
1122
1161
  } finally {
1123
1162
  this.isFlushing = false;
1124
1163
  }
@@ -1147,9 +1186,8 @@ var AnalyticsClient = class {
1147
1186
  });
1148
1187
  }
1149
1188
  async sendWithRetry(payload) {
1150
- let attempt = 0;
1151
1189
  let delay = 250;
1152
- while (attempt <= this.maxRetries) {
1190
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1153
1191
  try {
1154
1192
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1155
1193
  method: "POST",
@@ -1161,19 +1199,110 @@ var AnalyticsClient = class {
1161
1199
  keepalive: true
1162
1200
  });
1163
1201
  if (!response.ok) {
1164
- throw new Error(`ingest status=${response.status}`);
1202
+ throw await this.createHttpIngestSendError(response, attempt);
1165
1203
  }
1166
1204
  return;
1167
1205
  } catch (error) {
1168
- attempt += 1;
1169
- if (attempt > this.maxRetries) {
1170
- throw error;
1206
+ const normalized = this.toIngestSendError(error, attempt);
1207
+ const finalAttempt = attempt >= this.maxRetries + 1;
1208
+ this.log("Ingest attempt failed", {
1209
+ attempt: normalized.attempts,
1210
+ maxRetries: this.maxRetries,
1211
+ retryable: normalized.retryable,
1212
+ status: normalized.status,
1213
+ errorCode: normalized.errorCode,
1214
+ requestId: normalized.requestId,
1215
+ nextRetryInMs: !finalAttempt && normalized.retryable ? delay : null
1216
+ });
1217
+ if (finalAttempt || !normalized.retryable) {
1218
+ throw normalized;
1171
1219
  }
1172
1220
  await new Promise((resolve) => setTimeout(resolve, delay));
1173
1221
  delay *= 2;
1174
1222
  }
1175
1223
  }
1176
1224
  }
1225
+ async createHttpIngestSendError(response, attempts) {
1226
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("cf-ray") ?? void 0;
1227
+ let errorCode;
1228
+ let serverMessage;
1229
+ try {
1230
+ const parsed = await response.json();
1231
+ const errorBody = parsed && typeof parsed === "object" && parsed.error && typeof parsed.error === "object" ? parsed.error : void 0;
1232
+ if (typeof errorBody?.code === "string") {
1233
+ errorCode = errorBody.code;
1234
+ }
1235
+ if (typeof errorBody?.message === "string") {
1236
+ serverMessage = errorBody.message;
1237
+ }
1238
+ } catch {
1239
+ }
1240
+ const retryable = this.shouldRetryHttpStatus(response.status);
1241
+ const statusSuffix = errorCode ? ` ${errorCode}` : "";
1242
+ const message = `ingest status=${response.status}${statusSuffix}`;
1243
+ return new IngestSendError({
1244
+ message,
1245
+ retryable,
1246
+ attempts,
1247
+ status: response.status,
1248
+ errorCode,
1249
+ serverMessage,
1250
+ requestId
1251
+ });
1252
+ }
1253
+ shouldRetryHttpStatus(status) {
1254
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1255
+ }
1256
+ toIngestSendError(error, attempts) {
1257
+ if (error instanceof IngestSendError) {
1258
+ const resolvedAttempts = attempts ?? error.attempts;
1259
+ return new IngestSendError({
1260
+ message: error.message,
1261
+ retryable: error.retryable,
1262
+ attempts: resolvedAttempts,
1263
+ status: error.status,
1264
+ errorCode: error.errorCode,
1265
+ serverMessage: error.serverMessage,
1266
+ requestId: error.requestId,
1267
+ cause: error.cause
1268
+ });
1269
+ }
1270
+ const fallbackMessage = error instanceof Error ? error.message : "ingest request failed";
1271
+ return new IngestSendError({
1272
+ message: fallbackMessage,
1273
+ retryable: true,
1274
+ attempts: attempts ?? 1,
1275
+ cause: error
1276
+ });
1277
+ }
1278
+ createIngestDiagnostics(error, batchSize, queueSize) {
1279
+ return {
1280
+ name: "AnalyticsIngestError",
1281
+ message: error.message,
1282
+ endpoint: this.endpoint,
1283
+ path: "/v1/collect",
1284
+ status: error.status,
1285
+ errorCode: error.errorCode,
1286
+ serverMessage: error.serverMessage,
1287
+ requestId: error.requestId,
1288
+ retryable: error.retryable,
1289
+ attempts: error.attempts,
1290
+ maxRetries: this.maxRetries,
1291
+ batchSize,
1292
+ queueSize,
1293
+ timestamp: nowIso()
1294
+ };
1295
+ }
1296
+ reportIngestError(error) {
1297
+ if (!this.onIngestError) {
1298
+ return;
1299
+ }
1300
+ try {
1301
+ this.onIngestError(error);
1302
+ } catch (callbackError) {
1303
+ this.log("onIngestError callback threw", callbackError);
1304
+ }
1305
+ }
1177
1306
  parsePersistedConsent(raw) {
1178
1307
  if (raw === "granted") {
1179
1308
  return true;
@@ -1 +1 @@
1
- export { AnalyticsClient, AnalyticsClientOptions, AnalyticsConsentState, AnalyticsContext, AnalyticsContextConsentControls, AnalyticsContextUserControls, AnalyticsStorageAdapter, CreateAnalyticsContextOptions, EventContext, EventProperties, IdentityTrackingMode, InitInput, InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, OnboardingEventName, OnboardingEventProperties, OnboardingStepTracker, OnboardingSurveyAnswerType, OnboardingSurveyEventName, OnboardingSurveyResponseInput, OnboardingTracker, OnboardingTrackerDefaults, OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, PaywallEventName, PaywallEventProperties, PaywallJourneyEventName, PaywallTracker, PaywallTrackerDefaults, PaywallTrackerProperties, PurchaseEventName, SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync } from './index.cjs';
1
+ export { AnalyticsClient, AnalyticsClientOptions, AnalyticsConsentState, AnalyticsContext, AnalyticsContextConsentControls, AnalyticsContextUserControls, AnalyticsIngestError, AnalyticsIngestErrorHandler, AnalyticsStorageAdapter, CreateAnalyticsContextOptions, EventContext, EventProperties, IdentityTrackingMode, InitInput, InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, OnboardingEventName, OnboardingEventProperties, OnboardingStepTracker, OnboardingSurveyAnswerType, OnboardingSurveyEventName, OnboardingSurveyResponseInput, OnboardingTracker, OnboardingTrackerDefaults, OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, PaywallEventName, PaywallEventProperties, PaywallJourneyEventName, PaywallTracker, PaywallTrackerDefaults, PaywallTrackerProperties, PurchaseEventName, SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync } from './index.cjs';
@@ -1 +1 @@
1
- export { AnalyticsClient, AnalyticsClientOptions, AnalyticsConsentState, AnalyticsContext, AnalyticsContextConsentControls, AnalyticsContextUserControls, AnalyticsStorageAdapter, CreateAnalyticsContextOptions, EventContext, EventProperties, IdentityTrackingMode, InitInput, InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, OnboardingEventName, OnboardingEventProperties, OnboardingStepTracker, OnboardingSurveyAnswerType, OnboardingSurveyEventName, OnboardingSurveyResponseInput, OnboardingTracker, OnboardingTrackerDefaults, OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, PaywallEventName, PaywallEventProperties, PaywallJourneyEventName, PaywallTracker, PaywallTrackerDefaults, PaywallTrackerProperties, PurchaseEventName, SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync } from './index.js';
1
+ export { AnalyticsClient, AnalyticsClientOptions, AnalyticsConsentState, AnalyticsContext, AnalyticsContextConsentControls, AnalyticsContextUserControls, AnalyticsIngestError, AnalyticsIngestErrorHandler, AnalyticsStorageAdapter, CreateAnalyticsContextOptions, EventContext, EventProperties, IdentityTrackingMode, InitInput, InitOptions, ONBOARDING_EVENTS, ONBOARDING_PROGRESS_EVENT_ORDER, ONBOARDING_SCREEN_EVENT_PREFIXES, ONBOARDING_SURVEY_EVENTS, OnboardingEventName, OnboardingEventProperties, OnboardingStepTracker, OnboardingSurveyAnswerType, OnboardingSurveyEventName, OnboardingSurveyResponseInput, OnboardingTracker, OnboardingTrackerDefaults, OnboardingTrackerSurveyInput, PAYWALL_ANCHOR_EVENT_CANDIDATES, PAYWALL_EVENTS, PAYWALL_JOURNEY_EVENT_ORDER, PAYWALL_SKIP_EVENT_CANDIDATES, PURCHASE_EVENTS, PURCHASE_SUCCESS_EVENT_CANDIDATES, PaywallEventName, PaywallEventProperties, PaywallJourneyEventName, PaywallTracker, PaywallTrackerDefaults, PaywallTrackerProperties, PurchaseEventName, SetConsentOptions, createAnalyticsContext, init, initAsync, initConsentFirst, initConsentFirstAsync } from './index.js';
@@ -15,7 +15,7 @@ import {
15
15
  initAsync,
16
16
  initConsentFirst,
17
17
  initConsentFirstAsync
18
- } from "./chunk-O4FIH647.js";
18
+ } from "./chunk-S3MLRL5U.js";
19
19
  export {
20
20
  AnalyticsClient,
21
21
  ONBOARDING_EVENTS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@analyticscli/sdk",
3
- "version": "0.1.0-preview.7",
3
+ "version": "0.1.0-preview.8",
4
4
  "description": "TypeScript SDK for sending onboarding, paywall, purchase and survey analytics events to AnalyticsCLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",