@genn-inc/cluebase-frontend-sdk 0.0.6 → 0.0.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/_generated/correlation-attribute-keys.d.ts +3 -0
- package/dist/_generated/correlation-attribute-keys.js +35 -0
- package/dist/_generated/diagnostic-codes.d.ts +4 -0
- package/dist/_generated/diagnostic-codes.js +48 -0
- package/dist/_generated/drop-reasons.d.ts +2 -0
- package/dist/_generated/drop-reasons.js +25 -0
- package/dist/_generated/ingest-routes.d.ts +5 -0
- package/dist/_generated/ingest-routes.js +12 -0
- package/dist/context/trace.js +2 -4
- package/dist/exporter/batch.d.ts +23 -26
- package/dist/exporter/batch.js +254 -289
- package/dist/exporter/browser-outbox-records.d.ts +7 -24
- package/dist/exporter/browser-outbox-records.js +6 -94
- package/dist/exporter/browser-outbox.d.ts +23 -28
- package/dist/exporter/browser-outbox.js +99 -211
- package/dist/exporter/client.d.ts +24 -34
- package/dist/exporter/client.js +107 -318
- package/dist/exporter/delivery-pipeline.d.ts +72 -0
- package/dist/exporter/delivery-pipeline.js +199 -0
- package/dist/exporter/delivery-response-contract.d.ts +5 -81
- package/dist/exporter/delivery-response-contract.js +4 -166
- package/dist/exporter/delivery-response.d.ts +8 -4
- package/dist/exporter/delivery-response.js +62 -133
- package/dist/exporter/diagnostics.d.ts +2 -1
- package/dist/exporter/diagnostics.js +1 -25
- package/dist/exporter/event-id.d.ts +1 -0
- package/dist/exporter/event-id.js +3 -0
- package/dist/exporter/observation-source-event-exporter.d.ts +1 -1
- package/dist/exporter/observation-source-event-exporter.js +2 -2
- package/dist/exporter/parameter-encryption.d.ts +10 -0
- package/dist/exporter/parameter-encryption.js +219 -0
- package/dist/exporter/parameter-snapshot.js +25 -3
- package/dist/exporter/sdk-core-delivery.d.ts +226 -0
- package/dist/exporter/sdk-core-delivery.js +97 -0
- package/dist/exporter/sdk-core-wasm.d.ts +24 -1
- package/dist/exporter/sdk-core-wasm.js +31 -3
- package/dist/exporter/source-event.d.ts +2 -0
- package/dist/exporter/source-event.js +17 -0
- package/dist/exporter/span-to-source-signal.d.ts +1 -1
- package/dist/exporter/span-to-source-signal.js +20 -140
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -3
- package/dist/instrumentation/network.d.ts +1 -1
- package/dist/instrumentation/network.js +17 -0
- package/dist/provider/_version.d.ts +1 -1
- package/dist/provider/_version.js +1 -1
- package/dist/provider/config.js +16 -3
- package/dist/provider/constants.js +0 -1
- package/dist/provider/contracts.d.ts +2 -0
- package/dist/provider/contracts.js +2 -0
- package/dist/provider/lifecycle-event-dedup.d.ts +7 -0
- package/dist/provider/lifecycle-event-dedup.js +31 -0
- package/dist/provider/sdk-encryption-store.d.ts +7 -0
- package/dist/provider/sdk-encryption-store.js +15 -0
- package/dist/provider/sdk-event-pipeline.d.ts +1 -3
- package/dist/provider/sdk-event-pipeline.js +0 -15
- package/dist/provider/sdk-otel-pipeline.d.ts +2 -1
- package/dist/provider/sdk-otel-pipeline.js +2 -11
- package/dist/provider/sdk-runtime-surfaces.d.ts +3 -2
- package/dist/provider/sdk-runtime-surfaces.js +7 -6
- package/dist/provider/sdk.js +88 -54
- package/dist/provider/types.d.ts +14 -19
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +18 -9
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +170 -29
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +10 -9
- package/package.json +1 -1
- package/dist/provider/usage-governor.d.ts +0 -7
- package/dist/provider/usage-governor.js +0 -114
package/dist/exporter/client.js
CHANGED
|
@@ -1,40 +1,15 @@
|
|
|
1
1
|
import { canonicalize as canonicalJsonStringify } from "json-canonicalize";
|
|
2
2
|
import { CONSENT_DENIED_ABORT_REASON, DEFAULT_FRONTEND_SOURCE_IDENTIFIER, } from "../provider/constants.js";
|
|
3
|
-
import { appliedPrivacyPolicyToken
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { appliedPrivacyPolicyToken } from "../provider/privacy-policy-store.js";
|
|
4
|
+
import { currentSdkEncryption } from "../provider/sdk-encryption-store.js";
|
|
5
|
+
import { classifyIngestHttpStatus, } from "./delivery-response.js";
|
|
6
|
+
import { generateBatchId } from "./event-id.js";
|
|
7
|
+
import { describeIngestEndpoint, } from "./sdk-core-wasm.js";
|
|
8
|
+
import { withSourceEvidence } from "./source-event.js";
|
|
8
9
|
export const CLUEBASE_SDK_REQUEST_HEADER = "x-cluebase-sdk-request";
|
|
9
10
|
export const CLUEBASE_SDK_VERSION_HEADER = "x-cluebase-sdk-version";
|
|
10
11
|
export const CLUEBASE_SOURCE_SCHEMA_VERSION_HEADER = "x-cluebase-source-schema-version";
|
|
11
12
|
export const CLUEBASE_BROWSER_TOKEN_HEADER = "x-cluebase-browser-token";
|
|
12
|
-
export const CANONICAL_BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
|
|
13
|
-
export const CANONICAL_BROWSER_TOKEN_PATH = "/api/v1/ingest/browser-tokens";
|
|
14
|
-
const CANONICAL_INGEST_PATH_SUFFIX = CANONICAL_BROWSER_INGEST_PATH.replace(/^\/api\/v1/, "");
|
|
15
|
-
export const resolveCanonicalIngestEndpoint = (endpoint) => {
|
|
16
|
-
if (!endpoint) {
|
|
17
|
-
return endpoint;
|
|
18
|
-
}
|
|
19
|
-
if (endpoint.endsWith(CANONICAL_INGEST_PATH_SUFFIX)) {
|
|
20
|
-
return endpoint;
|
|
21
|
-
}
|
|
22
|
-
const trimmed = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
|
|
23
|
-
if (trimmed.endsWith("/api/v1")) {
|
|
24
|
-
return `${trimmed}${CANONICAL_INGEST_PATH_SUFFIX}`;
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
const parsed = new URL(trimmed);
|
|
28
|
-
if (parsed.pathname === "" || parsed.pathname === "/") {
|
|
29
|
-
parsed.pathname = CANONICAL_BROWSER_INGEST_PATH;
|
|
30
|
-
parsed.search = "";
|
|
31
|
-
parsed.hash = "";
|
|
32
|
-
return parsed.toString();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch { }
|
|
36
|
-
return endpoint;
|
|
37
|
-
};
|
|
38
13
|
const BROWSER_TOKEN_REFRESH_LEEWAY_MS = 60000;
|
|
39
14
|
class BrowserTokenRequestError extends Error {
|
|
40
15
|
constructor(status) {
|
|
@@ -43,163 +18,85 @@ class BrowserTokenRequestError extends Error {
|
|
|
43
18
|
this.name = "BrowserTokenRequestError";
|
|
44
19
|
}
|
|
45
20
|
}
|
|
46
|
-
export const resolveBrowserTokenEndpoint = (endpoint) => {
|
|
47
|
-
if (!endpoint) {
|
|
48
|
-
return CANONICAL_BROWSER_TOKEN_PATH;
|
|
49
|
-
}
|
|
50
|
-
try {
|
|
51
|
-
const parsed = new URL(endpoint);
|
|
52
|
-
parsed.pathname = CANONICAL_BROWSER_TOKEN_PATH;
|
|
53
|
-
parsed.search = "";
|
|
54
|
-
parsed.hash = "";
|
|
55
|
-
return parsed.toString();
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
const trimmed = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
|
|
59
|
-
const apiV1Idx = trimmed.indexOf("/api/v1");
|
|
60
|
-
if (apiV1Idx >= 0) {
|
|
61
|
-
return `${trimmed.slice(0, apiV1Idx)}${CANONICAL_BROWSER_TOKEN_PATH}`;
|
|
62
|
-
}
|
|
63
|
-
return `${trimmed}${CANONICAL_BROWSER_TOKEN_PATH}`;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
const NORMAL_ATTEMPT_TIMEOUT_MS = 15000;
|
|
67
21
|
export class IngestClient {
|
|
68
22
|
constructor(options) {
|
|
69
|
-
this.
|
|
23
|
+
this.describedEndpoint = null;
|
|
24
|
+
this.describePromise = null;
|
|
70
25
|
this.producerId = DEFAULT_FRONTEND_SOURCE_IDENTIFIER;
|
|
71
26
|
this.cachedToken = null;
|
|
72
27
|
this.pendingTokenRequest = null;
|
|
73
|
-
this.
|
|
74
|
-
this.
|
|
28
|
+
this.inFlight = null;
|
|
29
|
+
this.configuredEndpoint = options.endpoint;
|
|
75
30
|
this.projectKey = options.projectKey;
|
|
76
31
|
this.sdkType = options.sdkType;
|
|
77
32
|
this.sdkVersion = options.sdkVersion;
|
|
78
33
|
this.schemaVersion = options.schemaVersion;
|
|
79
34
|
this.metrics = options.metrics ?? null;
|
|
80
35
|
this.diagnostics = options.diagnostics ?? null;
|
|
36
|
+
this.onEndpointsResolved = options.onEndpointsResolved ?? null;
|
|
81
37
|
}
|
|
82
|
-
async
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
38
|
+
async endpoints() {
|
|
39
|
+
this.describePromise ?? (this.describePromise = describeIngestEndpoint(this.configuredEndpoint).then((described) => {
|
|
40
|
+
this.describedEndpoint = described;
|
|
41
|
+
this.onEndpointsResolved?.(described);
|
|
42
|
+
return described;
|
|
43
|
+
}));
|
|
44
|
+
return await this.describePromise;
|
|
45
|
+
}
|
|
46
|
+
knownEndpoints() {
|
|
47
|
+
return this.describedEndpoint;
|
|
48
|
+
}
|
|
49
|
+
async perform(request) {
|
|
50
|
+
const active = {
|
|
51
|
+
controller: new AbortController(),
|
|
52
|
+
cause: null,
|
|
53
|
+
timedOut: false,
|
|
54
|
+
};
|
|
55
|
+
this.inFlight = active;
|
|
56
|
+
const timeout = setTimeout(() => {
|
|
57
|
+
active.timedOut = true;
|
|
58
|
+
active.controller.abort();
|
|
59
|
+
}, request.timeout_ms);
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(request.url, {
|
|
62
|
+
method: request.method,
|
|
63
|
+
headers: Object.fromEntries(request.headers.map((header) => [header.name, header.value])),
|
|
64
|
+
body: request.body,
|
|
65
|
+
redirect: request.follow_redirects ? "follow" : "error",
|
|
66
|
+
signal: active.controller.signal,
|
|
67
|
+
...(request.omit_credentials ? { credentials: "omit" } : {}),
|
|
68
|
+
...(request.keepalive ? { keepalive: true } : {}),
|
|
69
|
+
});
|
|
70
|
+
const body = await readBoundedBody(response, request.max_response_body_bytes);
|
|
92
71
|
return {
|
|
93
|
-
|
|
94
|
-
status:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
retryStrategy: null,
|
|
72
|
+
kind: "response",
|
|
73
|
+
status: response.status,
|
|
74
|
+
body: body.text,
|
|
75
|
+
body_over_limit: body.overLimit,
|
|
98
76
|
};
|
|
99
77
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (payloadSize > maxPayloadBytes) {
|
|
107
|
-
return this.failedSend(null, 0, "split_batch");
|
|
108
|
-
}
|
|
109
|
-
const browserTokenResult = await this.safeEnsureBrowserToken({
|
|
110
|
-
signal: options.signal,
|
|
111
|
-
requireCachedToken: options.requireCachedBrowserToken,
|
|
112
|
-
deadlineAt: options.deadlineAt,
|
|
113
|
-
});
|
|
114
|
-
if (!browserTokenResult.ok) {
|
|
115
|
-
this.metrics?.increment("send_failure_count");
|
|
116
|
-
this.metrics?.increment("transport_failed_count");
|
|
117
|
-
return {
|
|
118
|
-
ok: false,
|
|
119
|
-
status: null,
|
|
120
|
-
attempts: 0,
|
|
121
|
-
governor: null,
|
|
122
|
-
retryStrategy: browserTokenResult.retryStrategy ??
|
|
123
|
-
(options.requireCachedBrowserToken ? "drop_event" : null),
|
|
124
|
-
};
|
|
78
|
+
catch {
|
|
79
|
+
if (active.cause)
|
|
80
|
+
return { kind: "aborted", cause: active.cause };
|
|
81
|
+
if (active.timedOut)
|
|
82
|
+
return { kind: "timeout" };
|
|
83
|
+
return { kind: "network_error" };
|
|
125
84
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (options.signal?.aborted ||
|
|
131
|
-
!hasDeadlineRemaining(options.deadlineAt)) {
|
|
132
|
-
return this.failedSend(lastStatus, attempts - 1, isConsentAbort(options.signal) ? "drop_event" : "retry_same_batch");
|
|
133
|
-
}
|
|
134
|
-
const attempt = await this.postBatch({
|
|
135
|
-
payload,
|
|
136
|
-
browserToken,
|
|
137
|
-
keepalive,
|
|
138
|
-
signal: options.signal,
|
|
139
|
-
deadlineAt: options.deadlineAt,
|
|
140
|
-
localSubmittedCount: sourceSignalEvents.length,
|
|
141
|
-
});
|
|
142
|
-
lastStatus = attempt.status;
|
|
143
|
-
const decision = attempt.decision;
|
|
144
|
-
this.recordDecision(decision, lastStatus, attempts, payload);
|
|
145
|
-
if (decision.ok) {
|
|
146
|
-
if (decision.droppedEventCount > 0) {
|
|
147
|
-
this.metrics?.increment("ingest_dropped_event_count", decision.droppedEventCount);
|
|
148
|
-
}
|
|
149
|
-
this.metrics?.increment("sent_count", decision.acceptedEventCount);
|
|
150
|
-
return {
|
|
151
|
-
ok: true,
|
|
152
|
-
status: lastStatus,
|
|
153
|
-
attempts,
|
|
154
|
-
governor: decision.governor,
|
|
155
|
-
retryStrategy: null,
|
|
156
|
-
observationIngestBatchId: decision.observationIngestBatchId,
|
|
157
|
-
partial: decision.acceptedPartial,
|
|
158
|
-
droppedEventCount: decision.droppedEventCount,
|
|
159
|
-
reason: decision.reason,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
if (decision.droppedEventCount > 0) {
|
|
163
|
-
this.metrics?.increment("ingest_dropped_event_count", decision.droppedEventCount);
|
|
164
|
-
}
|
|
165
|
-
if (!options.requireCachedBrowserToken &&
|
|
166
|
-
!refreshedToken &&
|
|
167
|
-
(decision.reason === "expired_browser_token" ||
|
|
168
|
-
decision.reason === "stale_browser_token")) {
|
|
169
|
-
this.cachedToken = null;
|
|
170
|
-
const refreshed = await this.safeEnsureBrowserToken({
|
|
171
|
-
signal: options.signal,
|
|
172
|
-
deadlineAt: options.deadlineAt,
|
|
173
|
-
});
|
|
174
|
-
if (!refreshed.ok) {
|
|
175
|
-
return this.failedSend(lastStatus, attempts, refreshed.retryStrategy ?? "retry_same_batch");
|
|
176
|
-
}
|
|
177
|
-
browserToken = refreshed.token;
|
|
178
|
-
refreshedToken = true;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (decision.retryStrategy !== "retry_same_batch" ||
|
|
182
|
-
attempts === RETRY_CONFIG.maxAttempts) {
|
|
183
|
-
return this.failedSend(lastStatus, attempts, decision.retryStrategy, decision.governor);
|
|
184
|
-
}
|
|
185
|
-
this.metrics?.increment("retry_count");
|
|
186
|
-
this.metrics?.increment("transport_retry_count");
|
|
187
|
-
this.diagnostics?.record({
|
|
188
|
-
code: "retry_scheduled",
|
|
189
|
-
strategy: "retry_same_batch",
|
|
190
|
-
http_status: lastStatus ?? undefined,
|
|
191
|
-
attempt: attempts,
|
|
192
|
-
batch_fingerprint: this.diagnostics.batchFingerprint(diagnosticCanonicalPayload(payload)),
|
|
193
|
-
});
|
|
194
|
-
if (!(await sleep(computeRetryDelay(attempts, RETRY_CONFIG), options.signal, options.deadlineAt))) {
|
|
195
|
-
return this.failedSend(lastStatus, attempts, isConsentAbort(options.signal) ? "drop_event" : "retry_same_batch");
|
|
196
|
-
}
|
|
85
|
+
finally {
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
if (this.inFlight === active)
|
|
88
|
+
this.inFlight = null;
|
|
197
89
|
}
|
|
198
|
-
|
|
90
|
+
}
|
|
91
|
+
abortAttempt(cause) {
|
|
92
|
+
if (!this.inFlight)
|
|
93
|
+
return;
|
|
94
|
+
this.inFlight.cause = cause;
|
|
95
|
+
this.inFlight.controller.abort();
|
|
199
96
|
}
|
|
200
97
|
measurePayloadBytes(events, metadata = {
|
|
201
|
-
batch_id: "
|
|
202
|
-
idempotency_key: "
|
|
98
|
+
batch_id: `batch_${"0".repeat(32)}`,
|
|
99
|
+
idempotency_key: `idem_${"0".repeat(32)}`,
|
|
203
100
|
sent_at: "2026-01-01T00:00:00.000Z",
|
|
204
101
|
}) {
|
|
205
102
|
return new TextEncoder().encode(this.stringifyPayload(events, metadata))
|
|
@@ -213,39 +110,34 @@ export class IngestClient {
|
|
|
213
110
|
idempotencyKey: metadata.idempotency_key,
|
|
214
111
|
sentAt: metadata.sent_at,
|
|
215
112
|
privacyPolicyToken: appliedPrivacyPolicyToken(),
|
|
113
|
+
sdkEncryptionKeyVersion: currentSdkEncryption()?.keyVersion,
|
|
216
114
|
producerMetadata: {
|
|
217
115
|
producer_id: this.producerId,
|
|
218
116
|
sdk_type: this.sdkType,
|
|
219
117
|
sdk_version: this.sdkVersion,
|
|
220
118
|
},
|
|
221
|
-
events: events.map((event) =>
|
|
119
|
+
events: events.map((event) => withSourceEvidence(event)),
|
|
222
120
|
});
|
|
223
121
|
}
|
|
224
|
-
withSourceEvidence(event) {
|
|
225
|
-
return {
|
|
226
|
-
...event,
|
|
227
|
-
source_event_id: event.source_event_id ?? event.event_id,
|
|
228
|
-
source_schema_version: String(this.schemaVersion),
|
|
229
|
-
source_event_type: SDK_SOURCE_SIGNAL_EVENT_TYPE,
|
|
230
|
-
source_event_kind: SDK_SOURCE_SIGNAL_EVENT_TYPE,
|
|
231
|
-
event_name: SDK_SOURCE_SIGNAL_EVENT_NAME,
|
|
232
|
-
event_category: "context",
|
|
233
|
-
producer_id: this.producerId,
|
|
234
|
-
service_name: event.service_name ?? this.sourceIdentifier,
|
|
235
|
-
privacy_state: event.privacy_state ?? "masked",
|
|
236
|
-
privacy_level: event.privacy_level ?? "default_masked",
|
|
237
|
-
masking_state: event.masking_state ?? "masked",
|
|
238
|
-
allowed_value_keys: event.allowed_value_keys ?? [],
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
122
|
createBatchEnvelopeMetadata() {
|
|
242
|
-
const id =
|
|
123
|
+
const id = generateBatchId();
|
|
243
124
|
return {
|
|
244
125
|
batch_id: `batch_${id}`,
|
|
245
126
|
idempotency_key: `idem_${id}`,
|
|
246
127
|
sent_at: new Date().toISOString(),
|
|
247
128
|
};
|
|
248
129
|
}
|
|
130
|
+
diagnosticCanonicalPayload(payload) {
|
|
131
|
+
try {
|
|
132
|
+
return canonicalJsonStringify(JSON.parse(payload));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return "{}";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async ensureDeliveryToken(options = {}) {
|
|
139
|
+
return await this.safeEnsureBrowserToken(options);
|
|
140
|
+
}
|
|
249
141
|
hasUsableBrowserToken(nowMs = Date.now()) {
|
|
250
142
|
return this.getUsableCachedBrowserToken(nowMs) !== null;
|
|
251
143
|
}
|
|
@@ -253,78 +145,6 @@ export class IngestClient {
|
|
|
253
145
|
const result = await this.safeEnsureBrowserToken({ signal });
|
|
254
146
|
return result.ok;
|
|
255
147
|
}
|
|
256
|
-
failedSend(status, attempts, retryStrategy, governor = null) {
|
|
257
|
-
this.metrics?.increment("send_failure_count");
|
|
258
|
-
this.metrics?.increment("transport_failed_count");
|
|
259
|
-
return { ok: false, status, attempts, governor, retryStrategy };
|
|
260
|
-
}
|
|
261
|
-
recordDecision(decision, status, attempt, payload) {
|
|
262
|
-
const diagnostics = this.diagnostics;
|
|
263
|
-
if (!diagnostics)
|
|
264
|
-
return;
|
|
265
|
-
for (const code of decision.diagnosticCodes) {
|
|
266
|
-
diagnostics.record({
|
|
267
|
-
code,
|
|
268
|
-
count: code === "intentional_drop" ? decision.droppedEventCount : 1,
|
|
269
|
-
http_status: status ?? undefined,
|
|
270
|
-
attempt,
|
|
271
|
-
batch_fingerprint: diagnostics.batchFingerprint(diagnosticCanonicalPayload(payload)),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
async postBatch(input) {
|
|
276
|
-
try {
|
|
277
|
-
const response = await fetchWithDeadline(this.endpoint, {
|
|
278
|
-
method: "POST",
|
|
279
|
-
headers: {
|
|
280
|
-
"content-type": "application/json",
|
|
281
|
-
"x-cluebase-project-key": this.projectKey,
|
|
282
|
-
[CLUEBASE_BROWSER_TOKEN_HEADER]: input.browserToken,
|
|
283
|
-
[CLUEBASE_SDK_REQUEST_HEADER]: this.sdkType,
|
|
284
|
-
[CLUEBASE_SDK_VERSION_HEADER]: this.sdkVersion,
|
|
285
|
-
[CLUEBASE_SOURCE_SCHEMA_VERSION_HEADER]: String(this.schemaVersion),
|
|
286
|
-
},
|
|
287
|
-
body: input.payload,
|
|
288
|
-
keepalive: input.keepalive,
|
|
289
|
-
credentials: "omit",
|
|
290
|
-
redirect: "error",
|
|
291
|
-
}, input.signal, input.deadlineAt);
|
|
292
|
-
const decision = evaluateBrowserDeliveryResponse({
|
|
293
|
-
status: response.status,
|
|
294
|
-
bodyText: await response.text(),
|
|
295
|
-
localSubmittedCount: input.localSubmittedCount,
|
|
296
|
-
});
|
|
297
|
-
if (decision.privacyPolicy !== undefined) {
|
|
298
|
-
applyDeliveredPrivacyPolicy(decision.privacyPolicy);
|
|
299
|
-
}
|
|
300
|
-
return {
|
|
301
|
-
status: response.status,
|
|
302
|
-
decision,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
catch {
|
|
306
|
-
return {
|
|
307
|
-
status: null,
|
|
308
|
-
decision: {
|
|
309
|
-
ok: false,
|
|
310
|
-
retryStrategy: isConsentAbort(input.signal)
|
|
311
|
-
? "drop_event"
|
|
312
|
-
: "retry_same_batch",
|
|
313
|
-
governor: null,
|
|
314
|
-
droppedEventCount: 0,
|
|
315
|
-
acceptedEventCount: 0,
|
|
316
|
-
acceptedPartial: false,
|
|
317
|
-
reason: isConsentAbort(input.signal)
|
|
318
|
-
? "consent_denied"
|
|
319
|
-
: "transport_timeout",
|
|
320
|
-
diagnosticCodes: [],
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
createId() {
|
|
326
|
-
return generateEventId();
|
|
327
|
-
}
|
|
328
148
|
getUsableCachedBrowserToken(nowMs = Date.now()) {
|
|
329
149
|
if (this.cachedToken &&
|
|
330
150
|
this.cachedToken.expiresAtMs > nowMs + BROWSER_TOKEN_REFRESH_LEEWAY_MS) {
|
|
@@ -332,7 +152,7 @@ export class IngestClient {
|
|
|
332
152
|
}
|
|
333
153
|
return null;
|
|
334
154
|
}
|
|
335
|
-
async ensureBrowserToken(signal
|
|
155
|
+
async ensureBrowserToken(signal) {
|
|
336
156
|
const nowMs = Date.now();
|
|
337
157
|
const cachedToken = this.getUsableCachedBrowserToken(nowMs);
|
|
338
158
|
if (cachedToken) {
|
|
@@ -342,18 +162,17 @@ export class IngestClient {
|
|
|
342
162
|
return await awaitWithAbort(this.pendingTokenRequest, signal);
|
|
343
163
|
}
|
|
344
164
|
const request = (async () => {
|
|
345
|
-
const response = await
|
|
165
|
+
const response = await fetch((await this.endpoints()).browserTokenUrl, {
|
|
346
166
|
method: "POST",
|
|
347
167
|
headers: {
|
|
348
168
|
"content-type": "application/json",
|
|
349
169
|
"x-cluebase-project-key": this.projectKey,
|
|
350
170
|
},
|
|
351
|
-
body: JSON.stringify({
|
|
352
|
-
projectKey: this.projectKey,
|
|
353
|
-
}),
|
|
171
|
+
body: JSON.stringify({ projectKey: this.projectKey }),
|
|
354
172
|
credentials: "omit",
|
|
355
173
|
redirect: "error",
|
|
356
|
-
|
|
174
|
+
...(signal ? { signal } : {}),
|
|
175
|
+
});
|
|
357
176
|
if (!response.ok) {
|
|
358
177
|
throw new BrowserTokenRequestError(response.status);
|
|
359
178
|
}
|
|
@@ -392,7 +211,7 @@ export class IngestClient {
|
|
|
392
211
|
try {
|
|
393
212
|
return {
|
|
394
213
|
ok: true,
|
|
395
|
-
token: await this.ensureBrowserToken(options.signal
|
|
214
|
+
token: await this.ensureBrowserToken(options.signal),
|
|
396
215
|
};
|
|
397
216
|
}
|
|
398
217
|
catch (error) {
|
|
@@ -411,35 +230,25 @@ export class IngestClient {
|
|
|
411
230
|
}
|
|
412
231
|
}
|
|
413
232
|
}
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
233
|
+
const readBoundedBody = async (response, maxBytes) => {
|
|
234
|
+
if (!response.body)
|
|
235
|
+
return { text: "", overLimit: false };
|
|
236
|
+
const reader = response.body.getReader();
|
|
237
|
+
const decoder = new TextDecoder();
|
|
238
|
+
let bytes = 0;
|
|
239
|
+
let text = "";
|
|
240
|
+
while (true) {
|
|
241
|
+
const chunk = await reader.read();
|
|
242
|
+
if (chunk.done)
|
|
243
|
+
return { text: text + decoder.decode(), overLimit: false };
|
|
244
|
+
bytes += chunk.value.byteLength;
|
|
245
|
+
if (bytes > maxBytes) {
|
|
246
|
+
await reader.cancel();
|
|
247
|
+
return { text: "", overLimit: true };
|
|
248
|
+
}
|
|
249
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
423
250
|
}
|
|
424
251
|
};
|
|
425
|
-
const isConsentAbort = (signal) => signal?.aborted === true && signal.reason === CONSENT_DENIED_ABORT_REASON;
|
|
426
|
-
const sleep = (milliseconds, signal, deadlineAt) => new Promise((resolve) => {
|
|
427
|
-
const remaining = remainingDeadlineMs(deadlineAt);
|
|
428
|
-
if (signal?.aborted || (remaining !== null && remaining <= 0)) {
|
|
429
|
-
resolve(false);
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const abort = () => {
|
|
433
|
-
clearTimeout(timeout);
|
|
434
|
-
resolve(false);
|
|
435
|
-
};
|
|
436
|
-
const timeoutMs = Math.min(milliseconds, remaining ?? milliseconds);
|
|
437
|
-
const timeout = setTimeout(() => {
|
|
438
|
-
signal?.removeEventListener("abort", abort);
|
|
439
|
-
resolve(timeoutMs === milliseconds);
|
|
440
|
-
}, timeoutMs);
|
|
441
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
442
|
-
});
|
|
443
252
|
const awaitWithAbort = async (promise, signal) => {
|
|
444
253
|
if (!signal)
|
|
445
254
|
return await promise;
|
|
@@ -457,27 +266,7 @@ const awaitWithAbort = async (promise, signal) => {
|
|
|
457
266
|
});
|
|
458
267
|
});
|
|
459
268
|
};
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
abort();
|
|
465
|
-
externalSignal?.addEventListener("abort", abort, { once: true });
|
|
466
|
-
const remaining = remainingDeadlineMs(deadlineAt);
|
|
467
|
-
if (remaining !== null && remaining <= 0)
|
|
468
|
-
abort();
|
|
469
|
-
const timeout = setTimeout(abort, Math.min(NORMAL_ATTEMPT_TIMEOUT_MS, remaining ?? NORMAL_ATTEMPT_TIMEOUT_MS));
|
|
470
|
-
try {
|
|
471
|
-
return await fetch(input, { ...init, signal: controller.signal });
|
|
472
|
-
}
|
|
473
|
-
finally {
|
|
474
|
-
clearTimeout(timeout);
|
|
475
|
-
externalSignal?.removeEventListener("abort", abort);
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
const monotonicNow = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
479
|
-
const remainingDeadlineMs = (deadlineAt) => deadlineAt === undefined ? null : Math.max(0, deadlineAt - monotonicNow());
|
|
480
|
-
const hasDeadlineRemaining = (deadlineAt) => {
|
|
481
|
-
const remaining = remainingDeadlineMs(deadlineAt);
|
|
482
|
-
return remaining === null || remaining > 0;
|
|
483
|
-
};
|
|
269
|
+
const isConsentAbort = (signal) => signal?.aborted === true && signal.reason === CONSENT_DENIED_ABORT_REASON;
|
|
270
|
+
const asObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
271
|
+
? value
|
|
272
|
+
: null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DiagnosticCode } from "./diagnostics.js";
|
|
2
|
+
import { type CollectionPermit, type ConsentReport, type ConsentState, type DeliveryProgress, type DiagnosticEntry, type EnqueueReport, type FenceReport, type QueueDepthReport, type QueueLimitsInput, type RestoredEvent, type RestoreReport, type TransportOutcome, type TransportRequest } from "./sdk-core-delivery.js";
|
|
3
|
+
export type DeliveryTransport = (request: TransportRequest) => Promise<TransportOutcome>;
|
|
4
|
+
export type DeliveryClock = {
|
|
5
|
+
nowMs: () => number;
|
|
6
|
+
sleep: (delayMs: number) => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export type DeliveryPipelineOptions = {
|
|
9
|
+
readonly endpoint: string;
|
|
10
|
+
readonly projectKey: string;
|
|
11
|
+
readonly sdkVersion: string;
|
|
12
|
+
readonly sourceSchemaVersion: string;
|
|
13
|
+
readonly sdkRequest: string;
|
|
14
|
+
readonly queueLimits: QueueLimitsInput;
|
|
15
|
+
readonly requestTimeoutMs?: number;
|
|
16
|
+
readonly initialConsent: ConsentState;
|
|
17
|
+
readonly transport: DeliveryTransport;
|
|
18
|
+
readonly clock: DeliveryClock;
|
|
19
|
+
};
|
|
20
|
+
export type AdmittedEvent = {
|
|
21
|
+
readonly eventKey: string;
|
|
22
|
+
readonly byteLength: number;
|
|
23
|
+
readonly diagnosticCanonicalBytes?: string;
|
|
24
|
+
};
|
|
25
|
+
export type DeliveryObservation = {
|
|
26
|
+
readonly progress: DeliveryProgress;
|
|
27
|
+
readonly diagnostics: readonly DiagnosticEntry[];
|
|
28
|
+
readonly settledEventKeys: readonly string[];
|
|
29
|
+
readonly droppedEventKeys: readonly string[];
|
|
30
|
+
readonly droppedEventCount: number;
|
|
31
|
+
readonly httpStatus: number | null;
|
|
32
|
+
readonly reason: string | null;
|
|
33
|
+
readonly privacyPolicy?: unknown;
|
|
34
|
+
readonly sdkEncryption?: unknown;
|
|
35
|
+
readonly directiveUnreadable: boolean;
|
|
36
|
+
readonly diagnosticCodes: readonly DiagnosticCode[];
|
|
37
|
+
};
|
|
38
|
+
export type SerializedBatch = {
|
|
39
|
+
readonly body: string;
|
|
40
|
+
readonly diagnosticCanonicalBytes?: string;
|
|
41
|
+
};
|
|
42
|
+
export type DeliveryRunStop = "queue_drained" | "awaiting_result" | "consent_revoked" | "deadline_exceeded" | "retries_exhausted" | "retry_not_due" | "batch_unavailable";
|
|
43
|
+
export type DeliveryRunSummary = {
|
|
44
|
+
readonly observations: readonly DeliveryObservation[];
|
|
45
|
+
readonly attempts: number;
|
|
46
|
+
readonly stop: DeliveryRunStop;
|
|
47
|
+
};
|
|
48
|
+
export declare class DeliveryPipeline {
|
|
49
|
+
private readonly consentHandle;
|
|
50
|
+
private readonly driverOptions;
|
|
51
|
+
private handle;
|
|
52
|
+
private readonly transport;
|
|
53
|
+
private readonly clock;
|
|
54
|
+
private released;
|
|
55
|
+
constructor(options: DeliveryPipelineOptions);
|
|
56
|
+
reset(): void;
|
|
57
|
+
permit(): CollectionPermit;
|
|
58
|
+
admit(event: AdmittedEvent, permit: CollectionPermit): EnqueueReport;
|
|
59
|
+
adoptRestoredBatch(ordinal: number, events: readonly RestoredEvent[]): RestoreReport;
|
|
60
|
+
depth(envelopeOverheadBytes: number): QueueDepthReport;
|
|
61
|
+
pendingCount(): number;
|
|
62
|
+
fence(envelopeOverheadBytes: number): FenceReport;
|
|
63
|
+
rememberBrowserToken(token: string | null): void;
|
|
64
|
+
declareConsent(consent: ConsentState): ConsentReport;
|
|
65
|
+
beginClose(): void;
|
|
66
|
+
dispose(): boolean;
|
|
67
|
+
run(input: {
|
|
68
|
+
readonly body: (batchId: number, eventKeys: readonly string[]) => SerializedBatch | null;
|
|
69
|
+
readonly onObservation?: (observation: DeliveryObservation) => void;
|
|
70
|
+
readonly deadlineMs?: number | (() => number | undefined);
|
|
71
|
+
}): Promise<DeliveryRunSummary>;
|
|
72
|
+
}
|