@analyticscli/sdk 0.1.0-preview.6 → 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
@@ -237,7 +237,28 @@ var randomId = () => {
237
237
  if (globalThis.crypto?.randomUUID) {
238
238
  return globalThis.crypto.randomUUID();
239
239
  }
240
- return `${Date.now()}-${Math.random().toString(16).slice(2, 12)}`;
240
+ const bytes = new Uint8Array(16);
241
+ if (globalThis.crypto?.getRandomValues) {
242
+ globalThis.crypto.getRandomValues(bytes);
243
+ } else {
244
+ for (let index = 0; index < bytes.length; index += 1) {
245
+ bytes[index] = Math.floor(Math.random() * 256);
246
+ }
247
+ }
248
+ const byte6 = bytes[6] ?? 0;
249
+ const byte8 = bytes[8] ?? 0;
250
+ bytes[6] = byte6 & 15 | 64;
251
+ bytes[8] = byte8 & 63 | 128;
252
+ let output = "";
253
+ for (let index = 0; index < bytes.length; index += 1) {
254
+ const byte = bytes[index] ?? 0;
255
+ const hex = byte.toString(16).padStart(2, "0");
256
+ output += hex;
257
+ if (index === 3 || index === 5 || index === 7 || index === 9) {
258
+ output += "-";
259
+ }
260
+ }
261
+ return output;
241
262
  };
242
263
  var readStorageSync = (storage, key) => {
243
264
  if (!storage) {
@@ -566,6 +587,28 @@ var sanitizeSurveyResponseInput = (input) => {
566
587
 
567
588
  // src/analytics-client.ts
568
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
+ };
569
612
  var AnalyticsClient = class {
570
613
  apiKey;
571
614
  hasIngestConfig;
@@ -574,6 +617,7 @@ var AnalyticsClient = class {
574
617
  flushIntervalMs;
575
618
  maxRetries;
576
619
  debug;
620
+ onIngestError;
577
621
  platform;
578
622
  projectSurface;
579
623
  appVersion;
@@ -606,6 +650,7 @@ var AnalyticsClient = class {
606
650
  deferredEventsBeforeHydration = [];
607
651
  onboardingStepViewStateSessionId = null;
608
652
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
653
+ flushPausedUntilMs = 0;
609
654
  constructor(options) {
610
655
  const normalizedOptions = this.normalizeOptions(options);
611
656
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -618,6 +663,7 @@ var AnalyticsClient = class {
618
663
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
619
664
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
620
665
  this.debug = normalizedOptions.debug ?? false;
666
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
621
667
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
622
668
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
623
669
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
@@ -1081,6 +1127,9 @@ var AnalyticsClient = class {
1081
1127
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1082
1128
  return;
1083
1129
  }
1130
+ if (Date.now() < this.flushPausedUntilMs) {
1131
+ return;
1132
+ }
1084
1133
  this.isFlushing = true;
1085
1134
  const batch = this.queue.splice(0, this.batchSize);
1086
1135
  const payload = {
@@ -1095,9 +1144,20 @@ var AnalyticsClient = class {
1095
1144
  }
1096
1145
  try {
1097
1146
  await this.sendWithRetry(payload);
1147
+ this.flushPausedUntilMs = 0;
1098
1148
  } catch (error) {
1099
- this.log("Send failed permanently, requeueing batch", error);
1100
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);
1101
1161
  } finally {
1102
1162
  this.isFlushing = false;
1103
1163
  }
@@ -1126,9 +1186,8 @@ var AnalyticsClient = class {
1126
1186
  });
1127
1187
  }
1128
1188
  async sendWithRetry(payload) {
1129
- let attempt = 0;
1130
1189
  let delay = 250;
1131
- while (attempt <= this.maxRetries) {
1190
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1132
1191
  try {
1133
1192
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1134
1193
  method: "POST",
@@ -1140,19 +1199,110 @@ var AnalyticsClient = class {
1140
1199
  keepalive: true
1141
1200
  });
1142
1201
  if (!response.ok) {
1143
- throw new Error(`ingest status=${response.status}`);
1202
+ throw await this.createHttpIngestSendError(response, attempt);
1144
1203
  }
1145
1204
  return;
1146
1205
  } catch (error) {
1147
- attempt += 1;
1148
- if (attempt > this.maxRetries) {
1149
- 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;
1150
1219
  }
1151
1220
  await new Promise((resolve) => setTimeout(resolve, delay));
1152
1221
  delay *= 2;
1153
1222
  }
1154
1223
  }
1155
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
+ }
1156
1306
  parsePersistedConsent(raw) {
1157
1307
  if (raw === "granted") {
1158
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-RGEYDN6A.js";
18
+ } from "./chunk-S3MLRL5U.js";
19
19
  export {
20
20
  AnalyticsClient,
21
21
  ONBOARDING_EVENTS,
@@ -196,7 +196,28 @@ var randomId = () => {
196
196
  if (globalThis.crypto?.randomUUID) {
197
197
  return globalThis.crypto.randomUUID();
198
198
  }
199
- return `${Date.now()}-${Math.random().toString(16).slice(2, 12)}`;
199
+ const bytes = new Uint8Array(16);
200
+ if (globalThis.crypto?.getRandomValues) {
201
+ globalThis.crypto.getRandomValues(bytes);
202
+ } else {
203
+ for (let index = 0; index < bytes.length; index += 1) {
204
+ bytes[index] = Math.floor(Math.random() * 256);
205
+ }
206
+ }
207
+ const byte6 = bytes[6] ?? 0;
208
+ const byte8 = bytes[8] ?? 0;
209
+ bytes[6] = byte6 & 15 | 64;
210
+ bytes[8] = byte8 & 63 | 128;
211
+ let output = "";
212
+ for (let index = 0; index < bytes.length; index += 1) {
213
+ const byte = bytes[index] ?? 0;
214
+ const hex = byte.toString(16).padStart(2, "0");
215
+ output += hex;
216
+ if (index === 3 || index === 5 || index === 7 || index === 9) {
217
+ output += "-";
218
+ }
219
+ }
220
+ return output;
200
221
  };
201
222
  var readStorageSync = (storage, key) => {
202
223
  if (!storage) {
@@ -525,6 +546,28 @@ var sanitizeSurveyResponseInput = (input) => {
525
546
 
526
547
  // src/analytics-client.ts
527
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
+ };
528
571
  var AnalyticsClient = class {
529
572
  apiKey;
530
573
  hasIngestConfig;
@@ -533,6 +576,7 @@ var AnalyticsClient = class {
533
576
  flushIntervalMs;
534
577
  maxRetries;
535
578
  debug;
579
+ onIngestError;
536
580
  platform;
537
581
  projectSurface;
538
582
  appVersion;
@@ -565,6 +609,7 @@ var AnalyticsClient = class {
565
609
  deferredEventsBeforeHydration = [];
566
610
  onboardingStepViewStateSessionId = null;
567
611
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
612
+ flushPausedUntilMs = 0;
568
613
  constructor(options) {
569
614
  const normalizedOptions = this.normalizeOptions(options);
570
615
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -577,6 +622,7 @@ var AnalyticsClient = class {
577
622
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
578
623
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
579
624
  this.debug = normalizedOptions.debug ?? false;
625
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
580
626
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
581
627
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
582
628
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
@@ -1040,6 +1086,9 @@ var AnalyticsClient = class {
1040
1086
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1041
1087
  return;
1042
1088
  }
1089
+ if (Date.now() < this.flushPausedUntilMs) {
1090
+ return;
1091
+ }
1043
1092
  this.isFlushing = true;
1044
1093
  const batch = this.queue.splice(0, this.batchSize);
1045
1094
  const payload = {
@@ -1054,9 +1103,20 @@ var AnalyticsClient = class {
1054
1103
  }
1055
1104
  try {
1056
1105
  await this.sendWithRetry(payload);
1106
+ this.flushPausedUntilMs = 0;
1057
1107
  } catch (error) {
1058
- this.log("Send failed permanently, requeueing batch", error);
1059
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);
1060
1120
  } finally {
1061
1121
  this.isFlushing = false;
1062
1122
  }
@@ -1085,9 +1145,8 @@ var AnalyticsClient = class {
1085
1145
  });
1086
1146
  }
1087
1147
  async sendWithRetry(payload) {
1088
- let attempt = 0;
1089
1148
  let delay = 250;
1090
- while (attempt <= this.maxRetries) {
1149
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1091
1150
  try {
1092
1151
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1093
1152
  method: "POST",
@@ -1099,19 +1158,110 @@ var AnalyticsClient = class {
1099
1158
  keepalive: true
1100
1159
  });
1101
1160
  if (!response.ok) {
1102
- throw new Error(`ingest status=${response.status}`);
1161
+ throw await this.createHttpIngestSendError(response, attempt);
1103
1162
  }
1104
1163
  return;
1105
1164
  } catch (error) {
1106
- attempt += 1;
1107
- if (attempt > this.maxRetries) {
1108
- 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;
1109
1178
  }
1110
1179
  await new Promise((resolve) => setTimeout(resolve, delay));
1111
1180
  delay *= 2;
1112
1181
  }
1113
1182
  }
1114
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
+ }
1115
1265
  parsePersistedConsent(raw) {
1116
1266
  if (raw === "granted") {
1117
1267
  return true;
package/dist/index.cjs CHANGED
@@ -237,7 +237,28 @@ var randomId = () => {
237
237
  if (globalThis.crypto?.randomUUID) {
238
238
  return globalThis.crypto.randomUUID();
239
239
  }
240
- return `${Date.now()}-${Math.random().toString(16).slice(2, 12)}`;
240
+ const bytes = new Uint8Array(16);
241
+ if (globalThis.crypto?.getRandomValues) {
242
+ globalThis.crypto.getRandomValues(bytes);
243
+ } else {
244
+ for (let index = 0; index < bytes.length; index += 1) {
245
+ bytes[index] = Math.floor(Math.random() * 256);
246
+ }
247
+ }
248
+ const byte6 = bytes[6] ?? 0;
249
+ const byte8 = bytes[8] ?? 0;
250
+ bytes[6] = byte6 & 15 | 64;
251
+ bytes[8] = byte8 & 63 | 128;
252
+ let output = "";
253
+ for (let index = 0; index < bytes.length; index += 1) {
254
+ const byte = bytes[index] ?? 0;
255
+ const hex = byte.toString(16).padStart(2, "0");
256
+ output += hex;
257
+ if (index === 3 || index === 5 || index === 7 || index === 9) {
258
+ output += "-";
259
+ }
260
+ }
261
+ return output;
241
262
  };
242
263
  var readStorageSync = (storage, key) => {
243
264
  if (!storage) {
@@ -566,6 +587,28 @@ var sanitizeSurveyResponseInput = (input) => {
566
587
 
567
588
  // src/analytics-client.ts
568
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
+ };
569
612
  var AnalyticsClient = class {
570
613
  apiKey;
571
614
  hasIngestConfig;
@@ -574,6 +617,7 @@ var AnalyticsClient = class {
574
617
  flushIntervalMs;
575
618
  maxRetries;
576
619
  debug;
620
+ onIngestError;
577
621
  platform;
578
622
  projectSurface;
579
623
  appVersion;
@@ -606,6 +650,7 @@ var AnalyticsClient = class {
606
650
  deferredEventsBeforeHydration = [];
607
651
  onboardingStepViewStateSessionId = null;
608
652
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
653
+ flushPausedUntilMs = 0;
609
654
  constructor(options) {
610
655
  const normalizedOptions = this.normalizeOptions(options);
611
656
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -618,6 +663,7 @@ var AnalyticsClient = class {
618
663
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
619
664
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
620
665
  this.debug = normalizedOptions.debug ?? false;
666
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
621
667
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
622
668
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
623
669
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
@@ -1081,6 +1127,9 @@ var AnalyticsClient = class {
1081
1127
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1082
1128
  return;
1083
1129
  }
1130
+ if (Date.now() < this.flushPausedUntilMs) {
1131
+ return;
1132
+ }
1084
1133
  this.isFlushing = true;
1085
1134
  const batch = this.queue.splice(0, this.batchSize);
1086
1135
  const payload = {
@@ -1095,9 +1144,20 @@ var AnalyticsClient = class {
1095
1144
  }
1096
1145
  try {
1097
1146
  await this.sendWithRetry(payload);
1147
+ this.flushPausedUntilMs = 0;
1098
1148
  } catch (error) {
1099
- this.log("Send failed permanently, requeueing batch", error);
1100
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);
1101
1161
  } finally {
1102
1162
  this.isFlushing = false;
1103
1163
  }
@@ -1126,9 +1186,8 @@ var AnalyticsClient = class {
1126
1186
  });
1127
1187
  }
1128
1188
  async sendWithRetry(payload) {
1129
- let attempt = 0;
1130
1189
  let delay = 250;
1131
- while (attempt <= this.maxRetries) {
1190
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1132
1191
  try {
1133
1192
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1134
1193
  method: "POST",
@@ -1140,19 +1199,110 @@ var AnalyticsClient = class {
1140
1199
  keepalive: true
1141
1200
  });
1142
1201
  if (!response.ok) {
1143
- throw new Error(`ingest status=${response.status}`);
1202
+ throw await this.createHttpIngestSendError(response, attempt);
1144
1203
  }
1145
1204
  return;
1146
1205
  } catch (error) {
1147
- attempt += 1;
1148
- if (attempt > this.maxRetries) {
1149
- 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;
1150
1219
  }
1151
1220
  await new Promise((resolve) => setTimeout(resolve, delay));
1152
1221
  delay *= 2;
1153
1222
  }
1154
1223
  }
1155
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
+ }
1156
1306
  parsePersistedConsent(raw) {
1157
1307
  if (raw === "granted") {
1158
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-RGEYDN6A.js";
18
+ } from "./chunk-S3MLRL5U.js";
19
19
  export {
20
20
  AnalyticsClient,
21
21
  ONBOARDING_EVENTS,
@@ -237,7 +237,28 @@ var randomId = () => {
237
237
  if (globalThis.crypto?.randomUUID) {
238
238
  return globalThis.crypto.randomUUID();
239
239
  }
240
- return `${Date.now()}-${Math.random().toString(16).slice(2, 12)}`;
240
+ const bytes = new Uint8Array(16);
241
+ if (globalThis.crypto?.getRandomValues) {
242
+ globalThis.crypto.getRandomValues(bytes);
243
+ } else {
244
+ for (let index = 0; index < bytes.length; index += 1) {
245
+ bytes[index] = Math.floor(Math.random() * 256);
246
+ }
247
+ }
248
+ const byte6 = bytes[6] ?? 0;
249
+ const byte8 = bytes[8] ?? 0;
250
+ bytes[6] = byte6 & 15 | 64;
251
+ bytes[8] = byte8 & 63 | 128;
252
+ let output = "";
253
+ for (let index = 0; index < bytes.length; index += 1) {
254
+ const byte = bytes[index] ?? 0;
255
+ const hex = byte.toString(16).padStart(2, "0");
256
+ output += hex;
257
+ if (index === 3 || index === 5 || index === 7 || index === 9) {
258
+ output += "-";
259
+ }
260
+ }
261
+ return output;
241
262
  };
242
263
  var readStorageSync = (storage, key) => {
243
264
  if (!storage) {
@@ -566,6 +587,28 @@ var sanitizeSurveyResponseInput = (input) => {
566
587
 
567
588
  // src/analytics-client.ts
568
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
+ };
569
612
  var AnalyticsClient = class {
570
613
  apiKey;
571
614
  hasIngestConfig;
@@ -574,6 +617,7 @@ var AnalyticsClient = class {
574
617
  flushIntervalMs;
575
618
  maxRetries;
576
619
  debug;
620
+ onIngestError;
577
621
  platform;
578
622
  projectSurface;
579
623
  appVersion;
@@ -606,6 +650,7 @@ var AnalyticsClient = class {
606
650
  deferredEventsBeforeHydration = [];
607
651
  onboardingStepViewStateSessionId = null;
608
652
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
653
+ flushPausedUntilMs = 0;
609
654
  constructor(options) {
610
655
  const normalizedOptions = this.normalizeOptions(options);
611
656
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -618,6 +663,7 @@ var AnalyticsClient = class {
618
663
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
619
664
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
620
665
  this.debug = normalizedOptions.debug ?? false;
666
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
621
667
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
622
668
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
623
669
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
@@ -1081,6 +1127,9 @@ var AnalyticsClient = class {
1081
1127
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1082
1128
  return;
1083
1129
  }
1130
+ if (Date.now() < this.flushPausedUntilMs) {
1131
+ return;
1132
+ }
1084
1133
  this.isFlushing = true;
1085
1134
  const batch = this.queue.splice(0, this.batchSize);
1086
1135
  const payload = {
@@ -1095,9 +1144,20 @@ var AnalyticsClient = class {
1095
1144
  }
1096
1145
  try {
1097
1146
  await this.sendWithRetry(payload);
1147
+ this.flushPausedUntilMs = 0;
1098
1148
  } catch (error) {
1099
- this.log("Send failed permanently, requeueing batch", error);
1100
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);
1101
1161
  } finally {
1102
1162
  this.isFlushing = false;
1103
1163
  }
@@ -1126,9 +1186,8 @@ var AnalyticsClient = class {
1126
1186
  });
1127
1187
  }
1128
1188
  async sendWithRetry(payload) {
1129
- let attempt = 0;
1130
1189
  let delay = 250;
1131
- while (attempt <= this.maxRetries) {
1190
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1132
1191
  try {
1133
1192
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1134
1193
  method: "POST",
@@ -1140,19 +1199,110 @@ var AnalyticsClient = class {
1140
1199
  keepalive: true
1141
1200
  });
1142
1201
  if (!response.ok) {
1143
- throw new Error(`ingest status=${response.status}`);
1202
+ throw await this.createHttpIngestSendError(response, attempt);
1144
1203
  }
1145
1204
  return;
1146
1205
  } catch (error) {
1147
- attempt += 1;
1148
- if (attempt > this.maxRetries) {
1149
- 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;
1150
1219
  }
1151
1220
  await new Promise((resolve) => setTimeout(resolve, delay));
1152
1221
  delay *= 2;
1153
1222
  }
1154
1223
  }
1155
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
+ }
1156
1306
  parsePersistedConsent(raw) {
1157
1307
  if (raw === "granted") {
1158
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-RGEYDN6A.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.6",
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",