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