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

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,49 @@ 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 resolveDefaultOsNameFromPlatform = (platform) => {
592
+ if (!platform) {
593
+ return void 0;
594
+ }
595
+ if (platform === "ios") {
596
+ return "iOS";
597
+ }
598
+ if (platform === "android") {
599
+ return "Android";
600
+ }
601
+ if (platform === "web") {
602
+ return "Web";
603
+ }
604
+ if (platform === "mac") {
605
+ return "macOS";
606
+ }
607
+ if (platform === "windows") {
608
+ return "Windows";
609
+ }
610
+ return void 0;
611
+ };
612
+ var IngestSendError = class extends Error {
613
+ retryable;
614
+ attempts;
615
+ status;
616
+ errorCode;
617
+ serverMessage;
618
+ requestId;
619
+ constructor(input) {
620
+ super(input.message);
621
+ this.name = "IngestSendError";
622
+ this.retryable = input.retryable;
623
+ this.attempts = input.attempts;
624
+ this.status = input.status;
625
+ this.errorCode = input.errorCode;
626
+ this.serverMessage = input.serverMessage;
627
+ this.requestId = input.requestId;
628
+ if (input.cause !== void 0) {
629
+ this.cause = input.cause;
630
+ }
631
+ }
632
+ };
590
633
  var AnalyticsClient = class {
591
634
  apiKey;
592
635
  hasIngestConfig;
@@ -595,6 +638,7 @@ var AnalyticsClient = class {
595
638
  flushIntervalMs;
596
639
  maxRetries;
597
640
  debug;
641
+ onIngestError;
598
642
  platform;
599
643
  projectSurface;
600
644
  appVersion;
@@ -627,6 +671,7 @@ var AnalyticsClient = class {
627
671
  deferredEventsBeforeHydration = [];
628
672
  onboardingStepViewStateSessionId = null;
629
673
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
674
+ flushPausedUntilMs = 0;
630
675
  constructor(options) {
631
676
  const normalizedOptions = this.normalizeOptions(options);
632
677
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -639,11 +684,17 @@ var AnalyticsClient = class {
639
684
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
640
685
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
641
686
  this.debug = normalizedOptions.debug ?? false;
687
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
642
688
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
643
689
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
644
690
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
645
691
  this.identityTrackingMode = this.resolveIdentityTrackingModeOption(normalizedOptions);
646
- this.context = { ...normalizedOptions.context ?? {} };
692
+ const initialContext = { ...normalizedOptions.context ?? {} };
693
+ const hasExplicitOsName = this.readRequiredStringOption(initialContext.osName).length > 0;
694
+ this.context = {
695
+ ...initialContext,
696
+ osName: hasExplicitOsName ? initialContext.osName : resolveDefaultOsNameFromPlatform(this.platform) ?? initialContext.osName
697
+ };
647
698
  this.runtimeEnv = detectRuntimeEnv();
648
699
  this.persistConsentState = normalizedOptions.persistConsentState ?? false;
649
700
  this.consentStorageKey = this.readRequiredStringOption(normalizedOptions.consentStorageKey) || DEFAULT_CONSENT_STORAGE_KEY;
@@ -676,6 +727,7 @@ var AnalyticsClient = class {
676
727
  this.writePersistedConsent(this.configuredStorage, this.fullTrackingConsentGranted);
677
728
  }
678
729
  this.hydrationPromise = this.hydrateIdentityFromStorage();
730
+ this.enqueueInitialSessionStart();
679
731
  this.startAutoFlush();
680
732
  }
681
733
  /**
@@ -733,6 +785,32 @@ var AnalyticsClient = class {
733
785
  ...context
734
786
  };
735
787
  }
788
+ enqueueInitialSessionStart() {
789
+ if (!this.consentGranted) {
790
+ return;
791
+ }
792
+ if (this.shouldDeferEventsUntilHydrated()) {
793
+ this.deferEventUntilHydrated(() => {
794
+ this.enqueueInitialSessionStart();
795
+ });
796
+ return;
797
+ }
798
+ const sessionId = this.getSessionId();
799
+ this.enqueue({
800
+ eventId: randomId(),
801
+ eventName: "session_start",
802
+ ts: nowIso(),
803
+ sessionId,
804
+ anonId: this.anonId,
805
+ userId: this.getEventUserId(),
806
+ properties: this.withRuntimeMetadata({ source: "sdk_mount" }, sessionId),
807
+ platform: this.platform,
808
+ projectSurface: this.projectSurface,
809
+ appVersion: this.appVersion,
810
+ ...this.withEventContext(),
811
+ type: "track"
812
+ });
813
+ }
736
814
  /**
737
815
  * Associates following events with a known user id.
738
816
  * Anonymous history remains linked by anonId/sessionId.
@@ -1102,6 +1180,9 @@ var AnalyticsClient = class {
1102
1180
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1103
1181
  return;
1104
1182
  }
1183
+ if (Date.now() < this.flushPausedUntilMs) {
1184
+ return;
1185
+ }
1105
1186
  this.isFlushing = true;
1106
1187
  const batch = this.queue.splice(0, this.batchSize);
1107
1188
  const payload = {
@@ -1116,9 +1197,20 @@ var AnalyticsClient = class {
1116
1197
  }
1117
1198
  try {
1118
1199
  await this.sendWithRetry(payload);
1200
+ this.flushPausedUntilMs = 0;
1119
1201
  } catch (error) {
1120
- this.log("Send failed permanently, requeueing batch", error);
1121
1202
  this.queue = [...batch, ...this.queue];
1203
+ const ingestError = this.toIngestSendError(error);
1204
+ const diagnostics = this.createIngestDiagnostics(ingestError, batch.length, this.queue.length);
1205
+ if (ingestError.status === 401 || ingestError.status === 403) {
1206
+ this.flushPausedUntilMs = Date.now() + AUTH_FAILURE_FLUSH_PAUSE_MS;
1207
+ this.log("Pausing ingest flush after auth failure", {
1208
+ status: ingestError.status,
1209
+ retryAfterMs: AUTH_FAILURE_FLUSH_PAUSE_MS
1210
+ });
1211
+ }
1212
+ this.log("Send failed permanently, requeueing batch", diagnostics);
1213
+ this.reportIngestError(diagnostics);
1122
1214
  } finally {
1123
1215
  this.isFlushing = false;
1124
1216
  }
@@ -1147,9 +1239,8 @@ var AnalyticsClient = class {
1147
1239
  });
1148
1240
  }
1149
1241
  async sendWithRetry(payload) {
1150
- let attempt = 0;
1151
1242
  let delay = 250;
1152
- while (attempt <= this.maxRetries) {
1243
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1153
1244
  try {
1154
1245
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1155
1246
  method: "POST",
@@ -1161,19 +1252,110 @@ var AnalyticsClient = class {
1161
1252
  keepalive: true
1162
1253
  });
1163
1254
  if (!response.ok) {
1164
- throw new Error(`ingest status=${response.status}`);
1255
+ throw await this.createHttpIngestSendError(response, attempt);
1165
1256
  }
1166
1257
  return;
1167
1258
  } catch (error) {
1168
- attempt += 1;
1169
- if (attempt > this.maxRetries) {
1170
- throw error;
1259
+ const normalized = this.toIngestSendError(error, attempt);
1260
+ const finalAttempt = attempt >= this.maxRetries + 1;
1261
+ this.log("Ingest attempt failed", {
1262
+ attempt: normalized.attempts,
1263
+ maxRetries: this.maxRetries,
1264
+ retryable: normalized.retryable,
1265
+ status: normalized.status,
1266
+ errorCode: normalized.errorCode,
1267
+ requestId: normalized.requestId,
1268
+ nextRetryInMs: !finalAttempt && normalized.retryable ? delay : null
1269
+ });
1270
+ if (finalAttempt || !normalized.retryable) {
1271
+ throw normalized;
1171
1272
  }
1172
1273
  await new Promise((resolve) => setTimeout(resolve, delay));
1173
1274
  delay *= 2;
1174
1275
  }
1175
1276
  }
1176
1277
  }
1278
+ async createHttpIngestSendError(response, attempts) {
1279
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("cf-ray") ?? void 0;
1280
+ let errorCode;
1281
+ let serverMessage;
1282
+ try {
1283
+ const parsed = await response.json();
1284
+ const errorBody = parsed && typeof parsed === "object" && parsed.error && typeof parsed.error === "object" ? parsed.error : void 0;
1285
+ if (typeof errorBody?.code === "string") {
1286
+ errorCode = errorBody.code;
1287
+ }
1288
+ if (typeof errorBody?.message === "string") {
1289
+ serverMessage = errorBody.message;
1290
+ }
1291
+ } catch {
1292
+ }
1293
+ const retryable = this.shouldRetryHttpStatus(response.status);
1294
+ const statusSuffix = errorCode ? ` ${errorCode}` : "";
1295
+ const message = `ingest status=${response.status}${statusSuffix}`;
1296
+ return new IngestSendError({
1297
+ message,
1298
+ retryable,
1299
+ attempts,
1300
+ status: response.status,
1301
+ errorCode,
1302
+ serverMessage,
1303
+ requestId
1304
+ });
1305
+ }
1306
+ shouldRetryHttpStatus(status) {
1307
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1308
+ }
1309
+ toIngestSendError(error, attempts) {
1310
+ if (error instanceof IngestSendError) {
1311
+ const resolvedAttempts = attempts ?? error.attempts;
1312
+ return new IngestSendError({
1313
+ message: error.message,
1314
+ retryable: error.retryable,
1315
+ attempts: resolvedAttempts,
1316
+ status: error.status,
1317
+ errorCode: error.errorCode,
1318
+ serverMessage: error.serverMessage,
1319
+ requestId: error.requestId,
1320
+ cause: error.cause
1321
+ });
1322
+ }
1323
+ const fallbackMessage = error instanceof Error ? error.message : "ingest request failed";
1324
+ return new IngestSendError({
1325
+ message: fallbackMessage,
1326
+ retryable: true,
1327
+ attempts: attempts ?? 1,
1328
+ cause: error
1329
+ });
1330
+ }
1331
+ createIngestDiagnostics(error, batchSize, queueSize) {
1332
+ return {
1333
+ name: "AnalyticsIngestError",
1334
+ message: error.message,
1335
+ endpoint: this.endpoint,
1336
+ path: "/v1/collect",
1337
+ status: error.status,
1338
+ errorCode: error.errorCode,
1339
+ serverMessage: error.serverMessage,
1340
+ requestId: error.requestId,
1341
+ retryable: error.retryable,
1342
+ attempts: error.attempts,
1343
+ maxRetries: this.maxRetries,
1344
+ batchSize,
1345
+ queueSize,
1346
+ timestamp: nowIso()
1347
+ };
1348
+ }
1349
+ reportIngestError(error) {
1350
+ if (!this.onIngestError) {
1351
+ return;
1352
+ }
1353
+ try {
1354
+ this.onIngestError(error);
1355
+ } catch (callbackError) {
1356
+ this.log("onIngestError callback threw", callbackError);
1357
+ }
1358
+ }
1177
1359
  parsePersistedConsent(raw) {
1178
1360
  if (raw === "granted") {
1179
1361
  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-FRXYHOBX.js";
19
19
  export {
20
20
  AnalyticsClient,
21
21
  ONBOARDING_EVENTS,
@@ -546,6 +546,49 @@ 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 resolveDefaultOsNameFromPlatform = (platform) => {
551
+ if (!platform) {
552
+ return void 0;
553
+ }
554
+ if (platform === "ios") {
555
+ return "iOS";
556
+ }
557
+ if (platform === "android") {
558
+ return "Android";
559
+ }
560
+ if (platform === "web") {
561
+ return "Web";
562
+ }
563
+ if (platform === "mac") {
564
+ return "macOS";
565
+ }
566
+ if (platform === "windows") {
567
+ return "Windows";
568
+ }
569
+ return void 0;
570
+ };
571
+ var IngestSendError = class extends Error {
572
+ retryable;
573
+ attempts;
574
+ status;
575
+ errorCode;
576
+ serverMessage;
577
+ requestId;
578
+ constructor(input) {
579
+ super(input.message);
580
+ this.name = "IngestSendError";
581
+ this.retryable = input.retryable;
582
+ this.attempts = input.attempts;
583
+ this.status = input.status;
584
+ this.errorCode = input.errorCode;
585
+ this.serverMessage = input.serverMessage;
586
+ this.requestId = input.requestId;
587
+ if (input.cause !== void 0) {
588
+ this.cause = input.cause;
589
+ }
590
+ }
591
+ };
549
592
  var AnalyticsClient = class {
550
593
  apiKey;
551
594
  hasIngestConfig;
@@ -554,6 +597,7 @@ var AnalyticsClient = class {
554
597
  flushIntervalMs;
555
598
  maxRetries;
556
599
  debug;
600
+ onIngestError;
557
601
  platform;
558
602
  projectSurface;
559
603
  appVersion;
@@ -586,6 +630,7 @@ var AnalyticsClient = class {
586
630
  deferredEventsBeforeHydration = [];
587
631
  onboardingStepViewStateSessionId = null;
588
632
  onboardingStepViewsSeen = /* @__PURE__ */ new Set();
633
+ flushPausedUntilMs = 0;
589
634
  constructor(options) {
590
635
  const normalizedOptions = this.normalizeOptions(options);
591
636
  this.apiKey = this.readRequiredStringOption(normalizedOptions.apiKey);
@@ -598,11 +643,17 @@ var AnalyticsClient = class {
598
643
  this.flushIntervalMs = normalizedOptions.flushIntervalMs ?? 5e3;
599
644
  this.maxRetries = normalizedOptions.maxRetries ?? 4;
600
645
  this.debug = normalizedOptions.debug ?? false;
646
+ this.onIngestError = typeof normalizedOptions.onIngestError === "function" ? normalizedOptions.onIngestError : null;
601
647
  this.platform = this.normalizePlatformOption(normalizedOptions.platform) ?? detectDefaultPlatform();
602
648
  this.projectSurface = this.normalizeProjectSurfaceOption(normalizedOptions.projectSurface);
603
649
  this.appVersion = this.readRequiredStringOption(normalizedOptions.appVersion) || detectDefaultAppVersion();
604
650
  this.identityTrackingMode = this.resolveIdentityTrackingModeOption(normalizedOptions);
605
- this.context = { ...normalizedOptions.context ?? {} };
651
+ const initialContext = { ...normalizedOptions.context ?? {} };
652
+ const hasExplicitOsName = this.readRequiredStringOption(initialContext.osName).length > 0;
653
+ this.context = {
654
+ ...initialContext,
655
+ osName: hasExplicitOsName ? initialContext.osName : resolveDefaultOsNameFromPlatform(this.platform) ?? initialContext.osName
656
+ };
606
657
  this.runtimeEnv = detectRuntimeEnv();
607
658
  this.persistConsentState = normalizedOptions.persistConsentState ?? false;
608
659
  this.consentStorageKey = this.readRequiredStringOption(normalizedOptions.consentStorageKey) || DEFAULT_CONSENT_STORAGE_KEY;
@@ -635,6 +686,7 @@ var AnalyticsClient = class {
635
686
  this.writePersistedConsent(this.configuredStorage, this.fullTrackingConsentGranted);
636
687
  }
637
688
  this.hydrationPromise = this.hydrateIdentityFromStorage();
689
+ this.enqueueInitialSessionStart();
638
690
  this.startAutoFlush();
639
691
  }
640
692
  /**
@@ -692,6 +744,32 @@ var AnalyticsClient = class {
692
744
  ...context
693
745
  };
694
746
  }
747
+ enqueueInitialSessionStart() {
748
+ if (!this.consentGranted) {
749
+ return;
750
+ }
751
+ if (this.shouldDeferEventsUntilHydrated()) {
752
+ this.deferEventUntilHydrated(() => {
753
+ this.enqueueInitialSessionStart();
754
+ });
755
+ return;
756
+ }
757
+ const sessionId = this.getSessionId();
758
+ this.enqueue({
759
+ eventId: randomId(),
760
+ eventName: "session_start",
761
+ ts: nowIso(),
762
+ sessionId,
763
+ anonId: this.anonId,
764
+ userId: this.getEventUserId(),
765
+ properties: this.withRuntimeMetadata({ source: "sdk_mount" }, sessionId),
766
+ platform: this.platform,
767
+ projectSurface: this.projectSurface,
768
+ appVersion: this.appVersion,
769
+ ...this.withEventContext(),
770
+ type: "track"
771
+ });
772
+ }
695
773
  /**
696
774
  * Associates following events with a known user id.
697
775
  * Anonymous history remains linked by anonId/sessionId.
@@ -1061,6 +1139,9 @@ var AnalyticsClient = class {
1061
1139
  if (this.queue.length === 0 || this.isFlushing || !this.consentGranted) {
1062
1140
  return;
1063
1141
  }
1142
+ if (Date.now() < this.flushPausedUntilMs) {
1143
+ return;
1144
+ }
1064
1145
  this.isFlushing = true;
1065
1146
  const batch = this.queue.splice(0, this.batchSize);
1066
1147
  const payload = {
@@ -1075,9 +1156,20 @@ var AnalyticsClient = class {
1075
1156
  }
1076
1157
  try {
1077
1158
  await this.sendWithRetry(payload);
1159
+ this.flushPausedUntilMs = 0;
1078
1160
  } catch (error) {
1079
- this.log("Send failed permanently, requeueing batch", error);
1080
1161
  this.queue = [...batch, ...this.queue];
1162
+ const ingestError = this.toIngestSendError(error);
1163
+ const diagnostics = this.createIngestDiagnostics(ingestError, batch.length, this.queue.length);
1164
+ if (ingestError.status === 401 || ingestError.status === 403) {
1165
+ this.flushPausedUntilMs = Date.now() + AUTH_FAILURE_FLUSH_PAUSE_MS;
1166
+ this.log("Pausing ingest flush after auth failure", {
1167
+ status: ingestError.status,
1168
+ retryAfterMs: AUTH_FAILURE_FLUSH_PAUSE_MS
1169
+ });
1170
+ }
1171
+ this.log("Send failed permanently, requeueing batch", diagnostics);
1172
+ this.reportIngestError(diagnostics);
1081
1173
  } finally {
1082
1174
  this.isFlushing = false;
1083
1175
  }
@@ -1106,9 +1198,8 @@ var AnalyticsClient = class {
1106
1198
  });
1107
1199
  }
1108
1200
  async sendWithRetry(payload) {
1109
- let attempt = 0;
1110
1201
  let delay = 250;
1111
- while (attempt <= this.maxRetries) {
1202
+ for (let attempt = 1; attempt <= this.maxRetries + 1; attempt += 1) {
1112
1203
  try {
1113
1204
  const response = await fetch(`${this.endpoint}/v1/collect`, {
1114
1205
  method: "POST",
@@ -1120,19 +1211,110 @@ var AnalyticsClient = class {
1120
1211
  keepalive: true
1121
1212
  });
1122
1213
  if (!response.ok) {
1123
- throw new Error(`ingest status=${response.status}`);
1214
+ throw await this.createHttpIngestSendError(response, attempt);
1124
1215
  }
1125
1216
  return;
1126
1217
  } catch (error) {
1127
- attempt += 1;
1128
- if (attempt > this.maxRetries) {
1129
- throw error;
1218
+ const normalized = this.toIngestSendError(error, attempt);
1219
+ const finalAttempt = attempt >= this.maxRetries + 1;
1220
+ this.log("Ingest attempt failed", {
1221
+ attempt: normalized.attempts,
1222
+ maxRetries: this.maxRetries,
1223
+ retryable: normalized.retryable,
1224
+ status: normalized.status,
1225
+ errorCode: normalized.errorCode,
1226
+ requestId: normalized.requestId,
1227
+ nextRetryInMs: !finalAttempt && normalized.retryable ? delay : null
1228
+ });
1229
+ if (finalAttempt || !normalized.retryable) {
1230
+ throw normalized;
1130
1231
  }
1131
1232
  await new Promise((resolve) => setTimeout(resolve, delay));
1132
1233
  delay *= 2;
1133
1234
  }
1134
1235
  }
1135
1236
  }
1237
+ async createHttpIngestSendError(response, attempts) {
1238
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("cf-ray") ?? void 0;
1239
+ let errorCode;
1240
+ let serverMessage;
1241
+ try {
1242
+ const parsed = await response.json();
1243
+ const errorBody = parsed && typeof parsed === "object" && parsed.error && typeof parsed.error === "object" ? parsed.error : void 0;
1244
+ if (typeof errorBody?.code === "string") {
1245
+ errorCode = errorBody.code;
1246
+ }
1247
+ if (typeof errorBody?.message === "string") {
1248
+ serverMessage = errorBody.message;
1249
+ }
1250
+ } catch {
1251
+ }
1252
+ const retryable = this.shouldRetryHttpStatus(response.status);
1253
+ const statusSuffix = errorCode ? ` ${errorCode}` : "";
1254
+ const message = `ingest status=${response.status}${statusSuffix}`;
1255
+ return new IngestSendError({
1256
+ message,
1257
+ retryable,
1258
+ attempts,
1259
+ status: response.status,
1260
+ errorCode,
1261
+ serverMessage,
1262
+ requestId
1263
+ });
1264
+ }
1265
+ shouldRetryHttpStatus(status) {
1266
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1267
+ }
1268
+ toIngestSendError(error, attempts) {
1269
+ if (error instanceof IngestSendError) {
1270
+ const resolvedAttempts = attempts ?? error.attempts;
1271
+ return new IngestSendError({
1272
+ message: error.message,
1273
+ retryable: error.retryable,
1274
+ attempts: resolvedAttempts,
1275
+ status: error.status,
1276
+ errorCode: error.errorCode,
1277
+ serverMessage: error.serverMessage,
1278
+ requestId: error.requestId,
1279
+ cause: error.cause
1280
+ });
1281
+ }
1282
+ const fallbackMessage = error instanceof Error ? error.message : "ingest request failed";
1283
+ return new IngestSendError({
1284
+ message: fallbackMessage,
1285
+ retryable: true,
1286
+ attempts: attempts ?? 1,
1287
+ cause: error
1288
+ });
1289
+ }
1290
+ createIngestDiagnostics(error, batchSize, queueSize) {
1291
+ return {
1292
+ name: "AnalyticsIngestError",
1293
+ message: error.message,
1294
+ endpoint: this.endpoint,
1295
+ path: "/v1/collect",
1296
+ status: error.status,
1297
+ errorCode: error.errorCode,
1298
+ serverMessage: error.serverMessage,
1299
+ requestId: error.requestId,
1300
+ retryable: error.retryable,
1301
+ attempts: error.attempts,
1302
+ maxRetries: this.maxRetries,
1303
+ batchSize,
1304
+ queueSize,
1305
+ timestamp: nowIso()
1306
+ };
1307
+ }
1308
+ reportIngestError(error) {
1309
+ if (!this.onIngestError) {
1310
+ return;
1311
+ }
1312
+ try {
1313
+ this.onIngestError(error);
1314
+ } catch (callbackError) {
1315
+ this.log("onIngestError callback threw", callbackError);
1316
+ }
1317
+ }
1136
1318
  parsePersistedConsent(raw) {
1137
1319
  if (raw === "granted") {
1138
1320
  return true;