@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 +158 -8
- package/dist/browser.d.cts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/{chunk-RGEYDN6A.js → chunk-S3MLRL5U.js} +158 -8
- package/dist/index.cjs +158 -8
- package/dist/index.d.cts +75 -1
- package/dist/index.d.ts +75 -1
- package/dist/index.js +1 -1
- package/dist/react-native.cjs +158 -8
- package/dist/react-native.d.cts +1 -1
- package/dist/react-native.d.ts +1 -1
- package/dist/react-native.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
1202
|
+
throw await this.createHttpIngestSendError(response, attempt);
|
|
1144
1203
|
}
|
|
1145
1204
|
return;
|
|
1146
1205
|
} catch (error) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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/browser.d.cts
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.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
|
@@ -196,7 +196,28 @@ var randomId = () => {
|
|
|
196
196
|
if (globalThis.crypto?.randomUUID) {
|
|
197
197
|
return globalThis.crypto.randomUUID();
|
|
198
198
|
}
|
|
199
|
-
|
|
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
|
-
|
|
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
|
|
1161
|
+
throw await this.createHttpIngestSendError(response, attempt);
|
|
1103
1162
|
}
|
|
1104
1163
|
return;
|
|
1105
1164
|
} catch (error) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1202
|
+
throw await this.createHttpIngestSendError(response, attempt);
|
|
1144
1203
|
}
|
|
1145
1204
|
return;
|
|
1146
1205
|
} catch (error) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
package/dist/react-native.cjs
CHANGED
|
@@ -237,7 +237,28 @@ var randomId = () => {
|
|
|
237
237
|
if (globalThis.crypto?.randomUUID) {
|
|
238
238
|
return globalThis.crypto.randomUUID();
|
|
239
239
|
}
|
|
240
|
-
|
|
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
|
-
|
|
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
|
|
1202
|
+
throw await this.createHttpIngestSendError(response, attempt);
|
|
1144
1203
|
}
|
|
1145
1204
|
return;
|
|
1146
1205
|
} catch (error) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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/react-native.d.cts
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.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/react-native.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/react-native.js
CHANGED
package/package.json
CHANGED