@clue-ai/browser-sdk 0.0.1
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/README.md +100 -0
- package/dist/authoring/overlay.d.ts +12 -0
- package/dist/authoring/overlay.js +468 -0
- package/dist/authoring/recording.d.ts +125 -0
- package/dist/authoring/recording.js +481 -0
- package/dist/authoring/service-logo.d.ts +1 -0
- package/dist/authoring/service-logo.generated.d.ts +1 -0
- package/dist/authoring/service-logo.generated.js +3 -0
- package/dist/authoring/service-logo.js +1 -0
- package/dist/authoring/session.d.ts +23 -0
- package/dist/authoring/session.js +127 -0
- package/dist/authoring/surface.d.ts +11 -0
- package/dist/authoring/surface.js +63 -0
- package/dist/authoring/toolbar-constants.d.ts +23 -0
- package/dist/authoring/toolbar-constants.js +42 -0
- package/dist/authoring/toolbar-drag.d.ts +29 -0
- package/dist/authoring/toolbar-drag.js +270 -0
- package/dist/authoring/toolbar-view.d.ts +21 -0
- package/dist/authoring/toolbar-view.js +2584 -0
- package/dist/capture/action.d.ts +2 -0
- package/dist/capture/action.js +62 -0
- package/dist/capture/dom.d.ts +23 -0
- package/dist/capture/dom.js +329 -0
- package/dist/capture/drag.d.ts +2 -0
- package/dist/capture/drag.js +75 -0
- package/dist/capture/error.d.ts +2 -0
- package/dist/capture/error.js +193 -0
- package/dist/capture/form.d.ts +2 -0
- package/dist/capture/form.js +137 -0
- package/dist/capture/frustration.d.ts +2 -0
- package/dist/capture/frustration.js +171 -0
- package/dist/capture/input.d.ts +2 -0
- package/dist/capture/input.js +109 -0
- package/dist/capture/location.d.ts +10 -0
- package/dist/capture/location.js +42 -0
- package/dist/capture/navigation.d.ts +2 -0
- package/dist/capture/navigation.js +100 -0
- package/dist/capture/network.d.ts +13 -0
- package/dist/capture/network.js +903 -0
- package/dist/capture/page.d.ts +2 -0
- package/dist/capture/page.js +78 -0
- package/dist/capture/performance.d.ts +2 -0
- package/dist/capture/performance.js +268 -0
- package/dist/context/account.d.ts +12 -0
- package/dist/context/account.js +129 -0
- package/dist/context/environment.d.ts +42 -0
- package/dist/context/environment.js +208 -0
- package/dist/context/identity.d.ts +14 -0
- package/dist/context/identity.js +123 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +155 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +142 -0
- package/dist/context/trace.d.ts +32 -0
- package/dist/context/trace.js +65 -0
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +199 -0
- package/dist/core/constants.d.ts +43 -0
- package/dist/core/constants.js +109 -0
- package/dist/core/contracts.d.ts +58 -0
- package/dist/core/contracts.js +53 -0
- package/dist/core/sdk.d.ts +2 -0
- package/dist/core/sdk.js +831 -0
- package/dist/core/types.d.ts +413 -0
- package/dist/core/types.js +1 -0
- package/dist/core/usage-governor.d.ts +7 -0
- package/dist/core/usage-governor.js +127 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +36 -0
- package/dist/integrations/next-router.d.ts +16 -0
- package/dist/integrations/next-router.js +18 -0
- package/dist/integrations/react-router.d.ts +7 -0
- package/dist/integrations/react-router.js +37 -0
- package/dist/internal/metrics.d.ts +9 -0
- package/dist/internal/metrics.js +38 -0
- package/dist/normalize/builders.d.ts +15 -0
- package/dist/normalize/builders.js +786 -0
- package/dist/normalize/canonical.d.ts +13 -0
- package/dist/normalize/canonical.js +77 -0
- package/dist/normalize/event-id.d.ts +8 -0
- package/dist/normalize/event-id.js +39 -0
- package/dist/normalize/path-template.d.ts +1 -0
- package/dist/normalize/path-template.js +33 -0
- package/dist/privacy/local-minimization.d.ts +29 -0
- package/dist/privacy/local-minimization.js +88 -0
- package/dist/privacy/mask.d.ts +7 -0
- package/dist/privacy/mask.js +60 -0
- package/dist/privacy/parameter-snapshot.d.ts +14 -0
- package/dist/privacy/parameter-snapshot.js +206 -0
- package/dist/privacy/sanitize.d.ts +11 -0
- package/dist/privacy/sanitize.js +145 -0
- package/dist/privacy/schema-evidence.d.ts +20 -0
- package/dist/privacy/schema-evidence.js +238 -0
- package/dist/transport/batch.d.ts +37 -0
- package/dist/transport/batch.js +182 -0
- package/dist/transport/client.d.ts +61 -0
- package/dist/transport/client.js +267 -0
- package/dist/transport/queue.d.ts +22 -0
- package/dist/transport/queue.js +56 -0
- package/dist/transport/retry.d.ts +14 -0
- package/dist/transport/retry.js +46 -0
- package/package.json +38 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { mergeIngestUsageGovernorFeedback, parseGovernorFeedbackEnvelope, } from "../core/usage-governor";
|
|
2
|
+
import { executeWithRetry, isRetryableStatus } from "./retry";
|
|
3
|
+
export const CLUE_SDK_REQUEST_HEADER = "x-clue-sdk-request";
|
|
4
|
+
export const CLUE_ENVIRONMENT_HEADER = "x-clue-environment";
|
|
5
|
+
export const CLUE_SDK_VERSION_HEADER = "x-clue-sdk-version";
|
|
6
|
+
export const CLUE_SOURCE_SCHEMA_VERSION_HEADER = "x-clue-source-schema-version";
|
|
7
|
+
export const CLUE_SERVICE_KEY_HEADER = "x-clue-service-key";
|
|
8
|
+
export const CLUE_BROWSER_TOKEN_HEADER = "x-clue-browser-token";
|
|
9
|
+
class HttpStatusError extends Error {
|
|
10
|
+
constructor(status) {
|
|
11
|
+
super(`HTTP ${status}`);
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
class IngestRejectedResponseError extends Error {
|
|
16
|
+
constructor(status, retryStrategy) {
|
|
17
|
+
super(`ingest rejected with retry strategy ${retryStrategy ?? "none"}`);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.retryStrategy = retryStrategy;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class IngestClient {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.endpoint = options.endpoint;
|
|
25
|
+
this.projectKey = options.projectKey;
|
|
26
|
+
this.environment = options.environment;
|
|
27
|
+
this.serviceKey = options.serviceKey ?? options.producerId;
|
|
28
|
+
this.producerId = options.producerId;
|
|
29
|
+
this.browserTokenProvider = options.browserTokenProvider ?? null;
|
|
30
|
+
this.sdkType = options.sdkType;
|
|
31
|
+
this.sdkVersion = options.sdkVersion;
|
|
32
|
+
this.schemaVersion = options.schemaVersion;
|
|
33
|
+
this.metrics = options.metrics ?? null;
|
|
34
|
+
}
|
|
35
|
+
async send(events, options = {}) {
|
|
36
|
+
if (events.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
status: null,
|
|
40
|
+
attempts: 0,
|
|
41
|
+
governor: null,
|
|
42
|
+
retryStrategy: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const maxPayloadBytes = Math.max(1024, options.maxPayloadBytes ?? Number.MAX_SAFE_INTEGER);
|
|
46
|
+
const metadata = options.metadata ?? this.createBatchEnvelopeMetadata();
|
|
47
|
+
const payload = this.stringifyPayload(events, metadata);
|
|
48
|
+
const browserToken = await this.resolveBrowserToken();
|
|
49
|
+
const beaconPayload = options.useBeacon && browserToken === null
|
|
50
|
+
? this.stringifyPayload(events, metadata, {
|
|
51
|
+
includeCompatibilityRouting: true,
|
|
52
|
+
})
|
|
53
|
+
: null;
|
|
54
|
+
const payloadSize = new TextEncoder().encode(payload).byteLength;
|
|
55
|
+
this.metrics?.increment("payload_size_bytes", payloadSize);
|
|
56
|
+
if (payloadSize > maxPayloadBytes ||
|
|
57
|
+
(beaconPayload !== null &&
|
|
58
|
+
new TextEncoder().encode(beaconPayload).byteLength > maxPayloadBytes)) {
|
|
59
|
+
this.metrics?.increment("dropped_count", events.length);
|
|
60
|
+
this.metrics?.increment("send_failure_count");
|
|
61
|
+
this.metrics?.increment("transport_failed_count");
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
status: null,
|
|
65
|
+
attempts: 0,
|
|
66
|
+
governor: null,
|
|
67
|
+
retryStrategy: null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
let attempts = 0;
|
|
71
|
+
let lastStatus = null;
|
|
72
|
+
let governor = null;
|
|
73
|
+
let retryStrategy = null;
|
|
74
|
+
if (beaconPayload !== null && this.trySendBeacon(beaconPayload)) {
|
|
75
|
+
this.metrics?.increment("sent_count", events.length);
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
status: 202,
|
|
79
|
+
attempts: 1,
|
|
80
|
+
governor: null,
|
|
81
|
+
retryStrategy: null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const result = await executeWithRetry(async () => {
|
|
85
|
+
const response = await fetch(this.endpoint, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"content-type": "application/json",
|
|
89
|
+
"x-clue-project-key": this.projectKey,
|
|
90
|
+
[CLUE_SERVICE_KEY_HEADER]: this.serviceKey,
|
|
91
|
+
...(browserToken
|
|
92
|
+
? { [CLUE_BROWSER_TOKEN_HEADER]: browserToken }
|
|
93
|
+
: {}),
|
|
94
|
+
[CLUE_SDK_REQUEST_HEADER]: this.sdkType,
|
|
95
|
+
[CLUE_ENVIRONMENT_HEADER]: this.environment,
|
|
96
|
+
[CLUE_SDK_VERSION_HEADER]: this.sdkVersion,
|
|
97
|
+
[CLUE_SOURCE_SCHEMA_VERSION_HEADER]: String(this.schemaVersion),
|
|
98
|
+
},
|
|
99
|
+
body: payload,
|
|
100
|
+
keepalive: options.keepalive ?? options.useBeacon ?? false,
|
|
101
|
+
credentials: "omit",
|
|
102
|
+
});
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
throw new HttpStatusError(response.status);
|
|
105
|
+
}
|
|
106
|
+
const ingestResponse = await this.readIngestResponse(response);
|
|
107
|
+
if (ingestResponse.accepted === false) {
|
|
108
|
+
throw new IngestRejectedResponseError(response.status, ingestResponse.retryStrategy);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
status: response.status,
|
|
112
|
+
governor: ingestResponse.governor,
|
|
113
|
+
retryStrategy: ingestResponse.retryStrategy,
|
|
114
|
+
};
|
|
115
|
+
}, (error) => {
|
|
116
|
+
if (error instanceof IngestRejectedResponseError) {
|
|
117
|
+
return error.retryStrategy === "retry_same_batch";
|
|
118
|
+
}
|
|
119
|
+
if (error instanceof HttpStatusError) {
|
|
120
|
+
return isRetryableStatus(error.status);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
attempts += result.attempts;
|
|
125
|
+
lastStatus =
|
|
126
|
+
typeof result.value?.status === "number"
|
|
127
|
+
? result.value.status
|
|
128
|
+
: result.error instanceof IngestRejectedResponseError
|
|
129
|
+
? result.error.status
|
|
130
|
+
: result.error instanceof HttpStatusError
|
|
131
|
+
? result.error.status
|
|
132
|
+
: null;
|
|
133
|
+
governor = mergeIngestUsageGovernorFeedback(governor, result.value?.governor ?? null);
|
|
134
|
+
retryStrategy =
|
|
135
|
+
result.value?.retryStrategy ??
|
|
136
|
+
(result.error instanceof IngestRejectedResponseError
|
|
137
|
+
? result.error.retryStrategy
|
|
138
|
+
: null);
|
|
139
|
+
if (result.attempts > 1) {
|
|
140
|
+
this.metrics?.increment("retry_count", result.attempts - 1);
|
|
141
|
+
this.metrics?.increment("transport_retry_count", result.attempts - 1);
|
|
142
|
+
}
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
this.metrics?.increment("send_failure_count");
|
|
145
|
+
this.metrics?.increment("transport_failed_count");
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
status: lastStatus,
|
|
149
|
+
attempts,
|
|
150
|
+
governor,
|
|
151
|
+
retryStrategy,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
this.metrics?.increment("sent_count", events.length);
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
status: lastStatus,
|
|
158
|
+
attempts,
|
|
159
|
+
governor,
|
|
160
|
+
retryStrategy,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
measurePayloadBytes(events, metadata = {
|
|
164
|
+
batch_id: "batch_00000000-0000-4000-8000-000000000000",
|
|
165
|
+
idempotency_key: "idem_00000000-0000-4000-8000-000000000000",
|
|
166
|
+
sent_at: "2026-01-01T00:00:00.000Z",
|
|
167
|
+
}) {
|
|
168
|
+
return new TextEncoder().encode(this.stringifyPayload(events, metadata))
|
|
169
|
+
.byteLength;
|
|
170
|
+
}
|
|
171
|
+
stringifyPayload(events, metadata, options = {}) {
|
|
172
|
+
return JSON.stringify({
|
|
173
|
+
...(options.includeCompatibilityRouting
|
|
174
|
+
? {
|
|
175
|
+
project_key: this.projectKey,
|
|
176
|
+
environment: this.environment,
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
179
|
+
batch_id: metadata.batch_id,
|
|
180
|
+
idempotency_key: metadata.idempotency_key,
|
|
181
|
+
sent_at: metadata.sent_at,
|
|
182
|
+
source_type: "browser_sdk",
|
|
183
|
+
source_schema_version: String(this.schemaVersion),
|
|
184
|
+
producer_metadata: {
|
|
185
|
+
producer_id: this.producerId,
|
|
186
|
+
sdk_type: this.sdkType,
|
|
187
|
+
sdk_version: this.sdkVersion,
|
|
188
|
+
},
|
|
189
|
+
events: events.map((event) => this.withSourceEvidence(event)),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
withSourceEvidence(event) {
|
|
193
|
+
return {
|
|
194
|
+
...event,
|
|
195
|
+
source_event_id: event.source_event_id ?? event.event_id,
|
|
196
|
+
source_schema_version: String(this.schemaVersion),
|
|
197
|
+
source_event_type: event.source_event_type ?? event.event_name,
|
|
198
|
+
source_event_kind: event.source_event_kind ?? event.event_category,
|
|
199
|
+
producer_id: event.producer_id ?? this.producerId,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
createBatchEnvelopeMetadata() {
|
|
203
|
+
const id = this.createId();
|
|
204
|
+
return {
|
|
205
|
+
batch_id: `batch_${id}`,
|
|
206
|
+
idempotency_key: `idem_${id}`,
|
|
207
|
+
sent_at: new Date().toISOString(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
createId() {
|
|
211
|
+
try {
|
|
212
|
+
if (typeof globalThis.crypto !== "undefined" &&
|
|
213
|
+
typeof globalThis.crypto.randomUUID === "function") {
|
|
214
|
+
return globalThis.crypto.randomUUID();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Fall through to a local random id when crypto is unavailable.
|
|
219
|
+
}
|
|
220
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
221
|
+
}
|
|
222
|
+
trySendBeacon(payload) {
|
|
223
|
+
try {
|
|
224
|
+
if (typeof navigator === "undefined" || !navigator.sendBeacon) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
228
|
+
return navigator.sendBeacon(this.endpoint, blob);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async resolveBrowserToken() {
|
|
235
|
+
if (!this.browserTokenProvider) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const token = await this.browserTokenProvider();
|
|
239
|
+
const trimmed = token.trim();
|
|
240
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
241
|
+
}
|
|
242
|
+
async readIngestResponse(response) {
|
|
243
|
+
try {
|
|
244
|
+
const body = await response.text();
|
|
245
|
+
if (!body.trim()) {
|
|
246
|
+
return {
|
|
247
|
+
accepted: null,
|
|
248
|
+
governor: null,
|
|
249
|
+
retryStrategy: null,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const parsedBody = JSON.parse(body);
|
|
253
|
+
return {
|
|
254
|
+
accepted: typeof parsedBody.accepted === "boolean" ? parsedBody.accepted : null,
|
|
255
|
+
governor: parseGovernorFeedbackEnvelope(parsedBody),
|
|
256
|
+
retryStrategy: parsedBody.retry_strategy ?? null,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return {
|
|
261
|
+
accepted: null,
|
|
262
|
+
governor: null,
|
|
263
|
+
retryStrategy: null,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CanonicalBaseEvent } from "../core/types";
|
|
2
|
+
import type { InternalMetricsStore } from "../internal/metrics";
|
|
3
|
+
export declare class EventQueue {
|
|
4
|
+
private readonly items;
|
|
5
|
+
private readonly maxItems;
|
|
6
|
+
private readonly maxBytes;
|
|
7
|
+
private readonly metrics;
|
|
8
|
+
private currentBytes;
|
|
9
|
+
constructor(options?: {
|
|
10
|
+
maxItems?: number;
|
|
11
|
+
maxBytes?: number;
|
|
12
|
+
metrics?: InternalMetricsStore;
|
|
13
|
+
});
|
|
14
|
+
enqueue(event: CanonicalBaseEvent): void;
|
|
15
|
+
dequeue(count: number): CanonicalBaseEvent[];
|
|
16
|
+
drain(): CanonicalBaseEvent[];
|
|
17
|
+
size(): number;
|
|
18
|
+
bytes(): number;
|
|
19
|
+
isEmpty(): boolean;
|
|
20
|
+
clear(): void;
|
|
21
|
+
private measureEventBytes;
|
|
22
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class EventQueue {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
this.items = [];
|
|
4
|
+
this.maxItems = Math.max(1, options?.maxItems ?? 1000);
|
|
5
|
+
this.maxBytes = Math.max(1024, options?.maxBytes ?? Number.MAX_SAFE_INTEGER);
|
|
6
|
+
this.metrics = options?.metrics ?? null;
|
|
7
|
+
this.currentBytes = 0;
|
|
8
|
+
}
|
|
9
|
+
enqueue(event) {
|
|
10
|
+
this.metrics?.increment("enqueue_count");
|
|
11
|
+
const eventBytes = this.measureEventBytes(event);
|
|
12
|
+
while (this.items.length > 0 &&
|
|
13
|
+
(this.items.length >= this.maxItems ||
|
|
14
|
+
this.currentBytes + eventBytes > this.maxBytes)) {
|
|
15
|
+
const dropped = this.items.shift();
|
|
16
|
+
this.currentBytes = Math.max(0, this.currentBytes - (dropped ? this.measureEventBytes(dropped) : 0));
|
|
17
|
+
this.metrics?.increment("dropped_count");
|
|
18
|
+
}
|
|
19
|
+
if (this.items.length >= this.maxItems) {
|
|
20
|
+
const dropped = this.items.shift();
|
|
21
|
+
this.currentBytes = Math.max(0, this.currentBytes - (dropped ? this.measureEventBytes(dropped) : 0));
|
|
22
|
+
this.metrics?.increment("dropped_count");
|
|
23
|
+
}
|
|
24
|
+
this.items.push(event);
|
|
25
|
+
this.currentBytes += eventBytes;
|
|
26
|
+
}
|
|
27
|
+
dequeue(count) {
|
|
28
|
+
if (count <= 0) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const events = this.items.splice(0, count);
|
|
32
|
+
for (const event of events) {
|
|
33
|
+
this.currentBytes = Math.max(0, this.currentBytes - this.measureEventBytes(event));
|
|
34
|
+
}
|
|
35
|
+
return events;
|
|
36
|
+
}
|
|
37
|
+
drain() {
|
|
38
|
+
return this.dequeue(this.items.length);
|
|
39
|
+
}
|
|
40
|
+
size() {
|
|
41
|
+
return this.items.length;
|
|
42
|
+
}
|
|
43
|
+
bytes() {
|
|
44
|
+
return this.currentBytes;
|
|
45
|
+
}
|
|
46
|
+
isEmpty() {
|
|
47
|
+
return this.items.length === 0;
|
|
48
|
+
}
|
|
49
|
+
clear() {
|
|
50
|
+
this.items.length = 0;
|
|
51
|
+
this.currentBytes = 0;
|
|
52
|
+
}
|
|
53
|
+
measureEventBytes(event) {
|
|
54
|
+
return new TextEncoder().encode(JSON.stringify(event)).byteLength;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type RetryPolicy = {
|
|
2
|
+
maxAttempts: number;
|
|
3
|
+
baseDelayMs: number;
|
|
4
|
+
maxDelayMs: number;
|
|
5
|
+
};
|
|
6
|
+
export type RetryResult<T> = {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
value?: T;
|
|
9
|
+
error?: unknown;
|
|
10
|
+
attempts: number;
|
|
11
|
+
};
|
|
12
|
+
export declare const isRetryableStatus: (status: number) => boolean;
|
|
13
|
+
export declare const computeRetryDelay: (attempt: number, policy: RetryPolicy) => number;
|
|
14
|
+
export declare const executeWithRetry: <T>(fn: () => Promise<T>, shouldRetry: (error: unknown) => boolean, policy?: RetryPolicy) => Promise<RetryResult<T>>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RETRY_CONFIG } from "../core/constants";
|
|
2
|
+
export const isRetryableStatus = (status) => {
|
|
3
|
+
return status === 429 || (status >= 500 && status < 600);
|
|
4
|
+
};
|
|
5
|
+
export const computeRetryDelay = (attempt, policy) => {
|
|
6
|
+
const exponent = Math.max(0, attempt - 1);
|
|
7
|
+
const raw = policy.baseDelayMs * 2 ** exponent;
|
|
8
|
+
const capped = Math.min(raw, policy.maxDelayMs);
|
|
9
|
+
const jitter = Math.floor(Math.random() * Math.min(200, capped));
|
|
10
|
+
return capped + jitter;
|
|
11
|
+
};
|
|
12
|
+
const sleep = (ms) => {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
setTimeout(resolve, ms);
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
export const executeWithRetry = async (fn, shouldRetry, policy = RETRY_CONFIG) => {
|
|
18
|
+
let lastError;
|
|
19
|
+
for (let attempt = 1; attempt <= policy.maxAttempts; attempt += 1) {
|
|
20
|
+
try {
|
|
21
|
+
const value = await fn();
|
|
22
|
+
return {
|
|
23
|
+
ok: true,
|
|
24
|
+
value,
|
|
25
|
+
attempts: attempt,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
lastError = error;
|
|
30
|
+
const retryable = shouldRetry(error);
|
|
31
|
+
if (!retryable || attempt >= policy.maxAttempts) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error,
|
|
35
|
+
attempts: attempt,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
await sleep(computeRetryDelay(attempt, policy));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: lastError,
|
|
44
|
+
attempts: policy.maxAttempts,
|
|
45
|
+
};
|
|
46
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clue-ai/browser-sdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Clue Browser SDK for raw telemetry capture and transport",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "node ./scripts/clean-dist.mjs && node ./scripts/generate-service-logo-module.mjs && tsc -p tsconfig.build.json",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:cov": "vitest run --coverage"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
26
|
+
"jsdom": "^26.1.0",
|
|
27
|
+
"typescript": "^5.9.0",
|
|
28
|
+
"vitest": "^3.2.4"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"anim-event": "^1.0.17",
|
|
32
|
+
"cssprefix": "^2.0.17",
|
|
33
|
+
"lucide": "^1.8.0",
|
|
34
|
+
"m-class-list": "^1.1.10",
|
|
35
|
+
"plain-draggable": "^2.5.15",
|
|
36
|
+
"pointer-event": "^1.3.1"
|
|
37
|
+
}
|
|
38
|
+
}
|