@genn-inc/cluebase-frontend-sdk 0.0.2 → 0.0.6
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/{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/otel-span-conversion.js +1 -9
- package/dist/context/action-context.js +27 -7
- 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 +2 -1
- package/dist/exporter/batch.d.ts +3 -1
- package/dist/exporter/batch.js +6 -10
- package/dist/exporter/browser-outbox-records.d.ts +1 -1
- package/dist/exporter/browser-outbox-records.js +15 -6
- package/dist/exporter/browser-outbox.d.ts +5 -2
- package/dist/exporter/browser-outbox.js +48 -11
- package/dist/exporter/client.d.ts +2 -2
- package/dist/exporter/client.js +15 -30
- package/dist/exporter/delivery-response-contract.js +67 -17
- package/dist/exporter/delivery-response.d.ts +3 -1
- package/dist/exporter/delivery-response.js +31 -20
- package/dist/exporter/diagnostics.d.ts +1 -1
- package/dist/exporter/diagnostics.js +4 -1
- package/dist/exporter/event-id.js +1 -3
- package/dist/exporter/frontend-resource.js +5 -4
- package/dist/exporter/observation-source-event-exporter.d.ts +1 -0
- package/dist/exporter/observation-source-event-exporter.js +5 -2
- 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-wasm.d.ts +5 -0
- package/dist/exporter/sdk-core-wasm.js +18 -0
- package/dist/exporter/source-event.d.ts +1 -0
- package/dist/exporter/span-to-source-signal.d.ts +1 -1
- package/dist/exporter/span-to-source-signal.js +122 -82
- package/dist/index.d.ts +5 -4
- package/dist/index.js +23 -11
- 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.js +7 -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 +0 -1
- package/dist/provider/constants.d.ts +1 -1
- package/dist/provider/constants.js +0 -1
- package/dist/provider/sdk-error.d.ts +7 -0
- package/dist/provider/sdk-error.js +48 -0
- package/dist/provider/sdk-event-pipeline.d.ts +2 -1
- package/dist/provider/sdk-event-pipeline.js +6 -3
- package/dist/provider/sdk-input-normalization.js +2 -2
- package/dist/provider/sdk-otel-pipeline.js +5 -4
- package/dist/provider/sdk-runtime-surfaces.d.ts +15 -12
- package/dist/provider/sdk-runtime-surfaces.js +8 -7
- package/dist/provider/sdk.js +81 -12
- package/dist/provider/types.d.ts +6 -5
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +41 -0
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +267 -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 +12 -0
- package/dist/wasm/pkg-web/package.json +15 -0
- package/package.json +10 -3
|
@@ -188,7 +188,9 @@ export const withoutChecksum = (record) => {
|
|
|
188
188
|
export const parseRecord = (raw, projectHash) => {
|
|
189
189
|
try {
|
|
190
190
|
const value = JSON.parse(raw);
|
|
191
|
-
if (!isRecord(value) ||
|
|
191
|
+
if (!isRecord(value) ||
|
|
192
|
+
value.storageVersion !== 2 ||
|
|
193
|
+
value.projectHash !== projectHash)
|
|
192
194
|
return null;
|
|
193
195
|
const checksumSha256 = value.checksumSha256;
|
|
194
196
|
if (typeof checksumSha256 !== "string")
|
|
@@ -204,11 +206,14 @@ export const parseRecord = (raw, projectHash) => {
|
|
|
204
206
|
};
|
|
205
207
|
const isValidRecord = (value) => {
|
|
206
208
|
const base = typeof value.createdAt === "string" &&
|
|
207
|
-
(typeof value.recordVerifierSha256 === "string" ||
|
|
209
|
+
(typeof value.recordVerifierSha256 === "string" ||
|
|
210
|
+
value.recordVerifierSha256 === null);
|
|
208
211
|
if (!base)
|
|
209
212
|
return false;
|
|
210
213
|
if (value.kind === "consent") {
|
|
211
|
-
return ((value.state === "granted" ||
|
|
214
|
+
return ((value.state === "granted" ||
|
|
215
|
+
value.state === "denied" ||
|
|
216
|
+
value.state === "unknown") &&
|
|
212
217
|
typeof value.collectionGeneration === "string" &&
|
|
213
218
|
typeof value.legacyImportBlocked === "boolean" &&
|
|
214
219
|
typeof value.updatedAt === "string" &&
|
|
@@ -225,7 +230,8 @@ const isValidRecord = (value) => {
|
|
|
225
230
|
isBrowserSourceSignalEvent(value.event));
|
|
226
231
|
}
|
|
227
232
|
if (value.kind === "legacy_tombstone") {
|
|
228
|
-
return typeof value.eventKeyHash === "string" &&
|
|
233
|
+
return (typeof value.eventKeyHash === "string" &&
|
|
234
|
+
typeof value.importedAt === "string");
|
|
229
235
|
}
|
|
230
236
|
if (value.kind === "pending" || value.kind === "split_plan") {
|
|
231
237
|
if (typeof value.batchId !== "string" ||
|
|
@@ -291,7 +297,8 @@ export const draftRecordKey = (prefix, eventKeyHash) => `${prefix}draft:${eventK
|
|
|
291
297
|
export const pendingRecordKey = (prefix, batchId) => `${prefix}pending:${keyHash(batchId)}`;
|
|
292
298
|
export const tombstoneRecordKey = (prefix, eventKeyHash) => `${prefix}legacy-tombstone:${eventKeyHash}`;
|
|
293
299
|
export const browserEventsByteLength = (events) => events.reduce((total, event) => total + new TextEncoder().encode(JSON.stringify(event)).byteLength, 0);
|
|
294
|
-
export const outboxEventCount = (drafts, pending) => drafts.length +
|
|
300
|
+
export const outboxEventCount = (drafts, pending) => drafts.length +
|
|
301
|
+
pending.reduce((count, record) => count + record.events.length, 0);
|
|
295
302
|
export const hasLegacyTombstone = (draft, tombstones) => tombstones.some((record) => record.eventKeyHash === draft.eventKeyHash &&
|
|
296
303
|
record.recordVerifierSha256 === draft.recordVerifierSha256);
|
|
297
304
|
export const createGeneration = () => typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
@@ -299,7 +306,9 @@ export const createGeneration = () => typeof crypto !== "undefined" && typeof cr
|
|
|
299
306
|
: `${Date.now()}-${Math.random()}`;
|
|
300
307
|
export const readStorage = () => {
|
|
301
308
|
try {
|
|
302
|
-
return typeof localStorage === "undefined"
|
|
309
|
+
return typeof localStorage === "undefined"
|
|
310
|
+
? null
|
|
311
|
+
: localStorage;
|
|
303
312
|
}
|
|
304
313
|
catch {
|
|
305
314
|
return null;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { InternalMetricsStore } from "../internal/metrics.js";
|
|
2
2
|
import type { CluebaseConsentState } from "../provider/types.js";
|
|
3
|
+
import { type CollectionPermit, type PendingBrowserBatch, type StorageLike } from "./browser-outbox-records.js";
|
|
3
4
|
import type { BatchEnvelopeMetadata } from "./client.js";
|
|
4
5
|
import { DiagnosticStore } from "./diagnostics.js";
|
|
5
6
|
import type { BrowserSourceEvent } from "./source-event.js";
|
|
6
|
-
|
|
7
|
-
export type { CollectionPermit, OwnedBrowserEvent, PendingBrowserBatch } from "./browser-outbox-records.js";
|
|
7
|
+
export type { CollectionPermit, OwnedBrowserEvent, PendingBrowserBatch, } from "./browser-outbox-records.js";
|
|
8
8
|
export type ConsentTransition = {
|
|
9
9
|
readonly state: CluebaseConsentState;
|
|
10
10
|
readonly generation: string;
|
|
@@ -62,6 +62,7 @@ export declare class BrowserOutbox {
|
|
|
62
62
|
private persistCommittedConsent;
|
|
63
63
|
addDraft(event: BrowserSourceEvent, permit?: CollectionPermit): boolean;
|
|
64
64
|
estimatedDraftBytes(measure: (events: readonly BrowserSourceEvent[], metadata: BatchEnvelopeMetadata) => number, metadata: BatchEnvelopeMetadata): number;
|
|
65
|
+
draftCount(): number;
|
|
65
66
|
prepareFence(input: {
|
|
66
67
|
readonly maxPayloadBytes: number;
|
|
67
68
|
readonly createMetadata: () => BatchEnvelopeMetadata;
|
|
@@ -85,6 +86,8 @@ export declare class BrowserOutbox {
|
|
|
85
86
|
private resolvePermit;
|
|
86
87
|
private initializeConsentForDelivery;
|
|
87
88
|
private enforceCapacity;
|
|
89
|
+
private dropOldestPendingForCapacity;
|
|
90
|
+
private forciblyRemovePendingRecord;
|
|
88
91
|
private purgeAll;
|
|
89
92
|
clearMemory(): void;
|
|
90
93
|
private restore;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { canonicalize as canonicalJsonStringify } from "json-canonicalize";
|
|
2
|
+
import { addStorageListener, browserEventsByteLength, consentRecordKey, createConsentChannel, createConsentRecord, createDraft, createGeneration, createLegacyTombstone, createPending, draftRecordKey, hasLegacyTombstone, LEGACY_TOMBSTONE_LIMIT, largestFittingPrefix, nowIso, outboxEventCount, parseRecord, pendingRecordKey, readLocks, readStorage, recordBatchId, sha256Hex, splitPendingRecord, toHeadBatch, tombstoneRecordKey, UNANCHORED_COLLECTION_GENERATION, verifyHash, withChecksum, withoutChecksum, } from "./browser-outbox-records.js";
|
|
1
3
|
import { DiagnosticStore } from "./diagnostics.js";
|
|
2
4
|
import { isBrowserSourceSignalEvent } from "./source-event.js";
|
|
3
|
-
import { canonicalize as canonicalJsonStringify } from "json-canonicalize";
|
|
4
|
-
import { addStorageListener, browserEventsByteLength, createConsentChannel, createConsentRecord, createDraft, createGeneration, createLegacyTombstone, createPending, consentRecordKey, draftRecordKey, hasLegacyTombstone, largestFittingPrefix, nowIso, outboxEventCount, parseRecord, readLocks, readStorage, recordBatchId, sha256Hex, toHeadBatch, splitPendingRecord, pendingRecordKey, tombstoneRecordKey, UNANCHORED_COLLECTION_GENERATION, verifyHash, withChecksum, withoutChecksum, LEGACY_TOMBSTONE_LIMIT, } from "./browser-outbox-records.js";
|
|
5
5
|
export class BrowserOutbox {
|
|
6
6
|
constructor(options) {
|
|
7
7
|
this.drafts = [];
|
|
@@ -21,7 +21,8 @@ export class BrowserOutbox {
|
|
|
21
21
|
this.consentKey = consentRecordKey(this.prefix);
|
|
22
22
|
this.legacyKey = `cluebase:source_event_outbox:${options.projectKey}`;
|
|
23
23
|
this.lockName = `cluebase:source-event-outbox:v2:${this.projectHash}`;
|
|
24
|
-
this.storage =
|
|
24
|
+
this.storage =
|
|
25
|
+
"storage" in options ? (options.storage ?? null) : readStorage();
|
|
25
26
|
this.durableOutboxSupported = readLocks() !== null;
|
|
26
27
|
this.maxBytes = Math.max(1024, options.maxBytes);
|
|
27
28
|
this.maxEvents = Math.max(1, options.maxEvents);
|
|
@@ -78,7 +79,10 @@ export class BrowserOutbox {
|
|
|
78
79
|
this.generation = createGeneration();
|
|
79
80
|
this.legacyImportBlocked = true;
|
|
80
81
|
this.diagnostics.rotateForConsentPurge(purged);
|
|
81
|
-
this.consentChannel?.postMessage({
|
|
82
|
+
this.consentChannel?.postMessage({
|
|
83
|
+
type: "denied",
|
|
84
|
+
generation: this.generation,
|
|
85
|
+
});
|
|
82
86
|
this.clearMemory();
|
|
83
87
|
return Object.freeze({
|
|
84
88
|
state: this.consent,
|
|
@@ -214,6 +218,9 @@ export class BrowserOutbox {
|
|
|
214
218
|
estimatedDraftBytes(measure, metadata) {
|
|
215
219
|
return measure(this.drafts.map((draft) => draft.event), metadata);
|
|
216
220
|
}
|
|
221
|
+
draftCount() {
|
|
222
|
+
return this.drafts.length;
|
|
223
|
+
}
|
|
217
224
|
prepareFence(input) {
|
|
218
225
|
const claimedEventKeyHashes = new Set(this.pending.flatMap((record) => record.eventKeyHashes));
|
|
219
226
|
const candidates = this.drafts.filter((draft) => !claimedEventKeyHashes.has(draft.eventKeyHash) &&
|
|
@@ -438,16 +445,46 @@ export class BrowserOutbox {
|
|
|
438
445
|
while (this.pendingEventCount() > this.maxEvents ||
|
|
439
446
|
this.pendingBytes() > this.maxBytes) {
|
|
440
447
|
const dropped = this.drafts.shift();
|
|
441
|
-
if (
|
|
448
|
+
if (dropped) {
|
|
449
|
+
this.removeRecord(draftRecordKey(this.prefix, dropped.eventKeyHash));
|
|
450
|
+
this.metrics?.increment("dropped_count");
|
|
451
|
+
this.diagnostics.record({
|
|
452
|
+
code: "queue_overflow",
|
|
453
|
+
event_fingerprint: this.diagnostics.eventFingerprint(canonicalJsonStringify(dropped.event)),
|
|
454
|
+
});
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (!this.dropOldestPendingForCapacity())
|
|
442
458
|
return;
|
|
443
|
-
this.removeRecord(draftRecordKey(this.prefix, dropped.eventKeyHash));
|
|
444
|
-
this.metrics?.increment("dropped_count");
|
|
445
|
-
this.diagnostics.record({
|
|
446
|
-
code: "queue_overflow",
|
|
447
|
-
event_fingerprint: this.diagnostics.eventFingerprint(canonicalJsonStringify(dropped.event)),
|
|
448
|
-
});
|
|
449
459
|
}
|
|
450
460
|
}
|
|
461
|
+
dropOldestPendingForCapacity() {
|
|
462
|
+
if (this.pending.length === 0)
|
|
463
|
+
return false;
|
|
464
|
+
const oldest = this.pending.reduce((oldestSoFar, item) => item.ordinal < oldestSoFar.ordinal ? item : oldestSoFar);
|
|
465
|
+
const headBatch = toHeadBatch(oldest, pendingRecordKey(this.prefix, oldest.batchId));
|
|
466
|
+
const droppedCount = oldest.events.length;
|
|
467
|
+
this.forciblyRemovePendingRecord(oldest);
|
|
468
|
+
if (droppedCount > 0) {
|
|
469
|
+
this.metrics?.increment("dropped_count", droppedCount);
|
|
470
|
+
}
|
|
471
|
+
this.diagnostics.record({
|
|
472
|
+
code: "queue_overflow",
|
|
473
|
+
count: droppedCount,
|
|
474
|
+
batch_fingerprint: headBatch
|
|
475
|
+
? this.diagnostics.batchFingerprint(headBatch.diagnosticCanonicalBytes)
|
|
476
|
+
: undefined,
|
|
477
|
+
});
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
forciblyRemovePendingRecord(record) {
|
|
481
|
+
let removed = this.removeClaimedDraftRecords(record);
|
|
482
|
+
removed =
|
|
483
|
+
this.removeRecord(pendingRecordKey(this.prefix, record.batchId)) &&
|
|
484
|
+
removed;
|
|
485
|
+
this.pending = this.pending.filter((item) => item !== record);
|
|
486
|
+
return removed;
|
|
487
|
+
}
|
|
451
488
|
purgeAll() {
|
|
452
489
|
const count = this.pendingEventCount();
|
|
453
490
|
let removed = this.clear();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { InternalMetricsStore } from "../internal/metrics.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { IngestUsageGovernorFeedback } from "../provider/types.js";
|
|
3
3
|
import { type IngestRetryStrategy } from "./delivery-response.js";
|
|
4
4
|
import type { DiagnosticStore } from "./diagnostics.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type BrowserSourceEvent } from "./source-event.js";
|
|
6
6
|
export declare const CLUEBASE_SDK_REQUEST_HEADER = "x-cluebase-sdk-request";
|
|
7
7
|
export declare const CLUEBASE_SDK_VERSION_HEADER = "x-cluebase-sdk-version";
|
|
8
8
|
export declare const CLUEBASE_SOURCE_SCHEMA_VERSION_HEADER = "x-cluebase-source-schema-version";
|
package/dist/exporter/client.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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 { generateEventId } from "./event-id.js";
|
|
3
|
+
import { appliedPrivacyPolicyToken, applyDeliveredPrivacyPolicy, } from "../provider/privacy-policy-store.js";
|
|
5
4
|
import { classifyIngestHttpStatus, evaluateBrowserDeliveryResponse, } from "./delivery-response.js";
|
|
5
|
+
import { generateEventId } from "./event-id.js";
|
|
6
6
|
import { computeRetryDelay, RETRY_CONFIG } from "./retry.js";
|
|
7
|
-
import {
|
|
7
|
+
import { isBrowserSourceSignalEvent, SDK_SOURCE_SIGNAL_EVENT_NAME, SDK_SOURCE_SIGNAL_EVENT_TYPE, } 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";
|
|
@@ -32,8 +32,7 @@ export const resolveCanonicalIngestEndpoint = (endpoint) => {
|
|
|
32
32
|
return parsed.toString();
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
catch {
|
|
36
|
-
}
|
|
35
|
+
catch { }
|
|
37
36
|
return endpoint;
|
|
38
37
|
};
|
|
39
38
|
const BROWSER_TOKEN_REFRESH_LEEWAY_MS = 60000;
|
|
@@ -105,16 +104,7 @@ export class IngestClient {
|
|
|
105
104
|
const payloadSize = new TextEncoder().encode(payload).byteLength;
|
|
106
105
|
this.metrics?.increment("payload_size_bytes", payloadSize);
|
|
107
106
|
if (payloadSize > maxPayloadBytes) {
|
|
108
|
-
this.
|
|
109
|
-
this.metrics?.increment("send_failure_count");
|
|
110
|
-
this.metrics?.increment("transport_failed_count");
|
|
111
|
-
return {
|
|
112
|
-
ok: false,
|
|
113
|
-
status: null,
|
|
114
|
-
attempts: 0,
|
|
115
|
-
governor: null,
|
|
116
|
-
retryStrategy: null,
|
|
117
|
-
};
|
|
107
|
+
return this.failedSend(null, 0, "split_batch");
|
|
118
108
|
}
|
|
119
109
|
const browserTokenResult = await this.safeEnsureBrowserToken({
|
|
120
110
|
signal: options.signal,
|
|
@@ -269,23 +259,16 @@ export class IngestClient {
|
|
|
269
259
|
return { ok: false, status, attempts, governor, retryStrategy };
|
|
270
260
|
}
|
|
271
261
|
recordDecision(decision, status, attempt, payload) {
|
|
272
|
-
|
|
262
|
+
const diagnostics = this.diagnostics;
|
|
263
|
+
if (!diagnostics)
|
|
273
264
|
return;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
code
|
|
265
|
+
for (const code of decision.diagnosticCodes) {
|
|
266
|
+
diagnostics.record({
|
|
267
|
+
code,
|
|
268
|
+
count: code === "intentional_drop" ? decision.droppedEventCount : 1,
|
|
277
269
|
http_status: status ?? undefined,
|
|
278
270
|
attempt,
|
|
279
|
-
batch_fingerprint:
|
|
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)),
|
|
271
|
+
batch_fingerprint: diagnostics.batchFingerprint(diagnosticCanonicalPayload(payload)),
|
|
289
272
|
});
|
|
290
273
|
}
|
|
291
274
|
}
|
|
@@ -334,6 +317,7 @@ export class IngestClient {
|
|
|
334
317
|
reason: isConsentAbort(input.signal)
|
|
335
318
|
? "consent_denied"
|
|
336
319
|
: "transport_timeout",
|
|
320
|
+
diagnosticCodes: [],
|
|
337
321
|
},
|
|
338
322
|
};
|
|
339
323
|
}
|
|
@@ -416,7 +400,8 @@ export class IngestClient {
|
|
|
416
400
|
if (isConsentAbort(options.signal)) {
|
|
417
401
|
return { ok: false, retryStrategy: "drop_event" };
|
|
418
402
|
}
|
|
419
|
-
if (!(error instanceof BrowserTokenRequestError) ||
|
|
403
|
+
if (!(error instanceof BrowserTokenRequestError) ||
|
|
404
|
+
error.status === null) {
|
|
420
405
|
return { ok: false, retryStrategy: "retry_same_batch" };
|
|
421
406
|
}
|
|
422
407
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { COST_CAPABILITIES, LIMIT_KINDS } from "../_generated/usage-governor-tables.js";
|
|
2
1
|
import { z } from "zod";
|
|
2
|
+
import { COST_CAPABILITIES, LIMIT_KINDS, } from "../_generated/usage-governor-tables.js";
|
|
3
3
|
const MAX_INGEST_EVENTS_PER_BATCH = 10000;
|
|
4
4
|
const ingestRetryStrategySchema = z.enum([
|
|
5
5
|
"split_batch",
|
|
@@ -30,7 +30,11 @@ const ingestUsageGovernorFeedbackSchema = z.object({
|
|
|
30
30
|
})),
|
|
31
31
|
retryAfterAt: z.string().datetime({ offset: true }).nullable(),
|
|
32
32
|
});
|
|
33
|
-
const boundedCount = z
|
|
33
|
+
const boundedCount = z
|
|
34
|
+
.number()
|
|
35
|
+
.int()
|
|
36
|
+
.nonnegative()
|
|
37
|
+
.max(MAX_INGEST_EVENTS_PER_BATCH);
|
|
34
38
|
const positiveCount = boundedCount.refine((count) => count > 0);
|
|
35
39
|
export const ingestPrivacyPolicySchema = z
|
|
36
40
|
.object({
|
|
@@ -55,13 +59,16 @@ const observationSourceEventBatchAcceptedResponseSchema = z
|
|
|
55
59
|
submitted_event_count: positiveCount,
|
|
56
60
|
accepted_event_count: positiveCount,
|
|
57
61
|
dropped_event_count: boundedCount,
|
|
58
|
-
dropped_events: z
|
|
62
|
+
dropped_events: z
|
|
63
|
+
.array(ingestDroppedEventSchema)
|
|
64
|
+
.max(MAX_INGEST_EVENTS_PER_BATCH),
|
|
59
65
|
duplicate: z.boolean(),
|
|
60
66
|
governor: ingestUsageGovernorFeedbackSchema.optional(),
|
|
61
67
|
privacy_policy: ingestPrivacyPolicySchema.optional(),
|
|
62
68
|
})
|
|
63
69
|
.superRefine((value, ctx) => {
|
|
64
|
-
if (value.submitted_event_count !==
|
|
70
|
+
if (value.submitted_event_count !==
|
|
71
|
+
value.accepted_event_count + value.dropped_event_count) {
|
|
65
72
|
ctx.addIssue({
|
|
66
73
|
code: z.ZodIssueCode.custom,
|
|
67
74
|
path: ["submitted_event_count"],
|
|
@@ -81,7 +88,10 @@ const observationSourceEventBatchRejectedResponseSchema = z
|
|
|
81
88
|
submitted_event_count: positiveCount.optional(),
|
|
82
89
|
accepted_event_count: boundedCount.optional(),
|
|
83
90
|
dropped_event_count: boundedCount.optional(),
|
|
84
|
-
dropped_events: z
|
|
91
|
+
dropped_events: z
|
|
92
|
+
.array(ingestDroppedEventSchema)
|
|
93
|
+
.max(MAX_INGEST_EVENTS_PER_BATCH)
|
|
94
|
+
.optional(),
|
|
85
95
|
limits: z
|
|
86
96
|
.object({
|
|
87
97
|
hard_max_payload_bytes: z.number().int().positive(),
|
|
@@ -91,36 +101,67 @@ const observationSourceEventBatchRejectedResponseSchema = z
|
|
|
91
101
|
.optional(),
|
|
92
102
|
})
|
|
93
103
|
.superRefine((value, ctx) => {
|
|
94
|
-
const counts = [
|
|
104
|
+
const counts = [
|
|
105
|
+
value.submitted_event_count,
|
|
106
|
+
value.accepted_event_count,
|
|
107
|
+
value.dropped_event_count,
|
|
108
|
+
];
|
|
95
109
|
const hasAnyCount = counts.some((count) => count !== undefined);
|
|
96
110
|
const hasAllCounts = counts.every((count) => count !== undefined);
|
|
97
111
|
if (hasAnyCount !== hasAllCounts) {
|
|
98
|
-
ctx.addIssue({
|
|
112
|
+
ctx.addIssue({
|
|
113
|
+
code: z.ZodIssueCode.custom,
|
|
114
|
+
path: ["submitted_event_count"],
|
|
115
|
+
message: "rejected response counts must be all present or all absent",
|
|
116
|
+
});
|
|
99
117
|
return;
|
|
100
118
|
}
|
|
101
119
|
if (hasAllCounts) {
|
|
102
120
|
const [submitted, accepted, dropped] = counts;
|
|
103
|
-
if (accepted > submitted ||
|
|
104
|
-
|
|
121
|
+
if (accepted > submitted ||
|
|
122
|
+
dropped > submitted ||
|
|
123
|
+
accepted + dropped > submitted) {
|
|
124
|
+
ctx.addIssue({
|
|
125
|
+
code: z.ZodIssueCode.custom,
|
|
126
|
+
path: ["dropped_event_count"],
|
|
127
|
+
message: "rejected counts must not exceed submitted count",
|
|
128
|
+
});
|
|
105
129
|
}
|
|
106
130
|
}
|
|
107
131
|
if (value.retry_strategy === "drop_event") {
|
|
108
132
|
if (!hasAllCounts) {
|
|
109
|
-
ctx.addIssue({
|
|
133
|
+
ctx.addIssue({
|
|
134
|
+
code: z.ZodIssueCode.custom,
|
|
135
|
+
path: ["submitted_event_count"],
|
|
136
|
+
message: "drop rejection requires all counts",
|
|
137
|
+
});
|
|
110
138
|
}
|
|
111
139
|
else {
|
|
112
140
|
const [submitted, accepted, dropped] = counts;
|
|
113
141
|
if (accepted !== 0 || dropped !== submitted) {
|
|
114
|
-
ctx.addIssue({
|
|
142
|
+
ctx.addIssue({
|
|
143
|
+
code: z.ZodIssueCode.custom,
|
|
144
|
+
path: ["dropped_event_count"],
|
|
145
|
+
message: "drop rejection must reject every submitted event",
|
|
146
|
+
});
|
|
115
147
|
}
|
|
116
148
|
validateDroppedEventDetails(submitted, dropped, value.dropped_events ?? [], ctx);
|
|
117
149
|
}
|
|
118
150
|
}
|
|
119
151
|
else if ((value.dropped_events?.length ?? 0) > 0) {
|
|
120
|
-
ctx.addIssue({
|
|
152
|
+
ctx.addIssue({
|
|
153
|
+
code: z.ZodIssueCode.custom,
|
|
154
|
+
path: ["dropped_events"],
|
|
155
|
+
message: "non-drop rejection must not contain dropped event details",
|
|
156
|
+
});
|
|
121
157
|
}
|
|
122
|
-
if ((value.retry_strategy === "split_batch") !==
|
|
123
|
-
|
|
158
|
+
if ((value.retry_strategy === "split_batch") !==
|
|
159
|
+
(value.limits !== undefined)) {
|
|
160
|
+
ctx.addIssue({
|
|
161
|
+
code: z.ZodIssueCode.custom,
|
|
162
|
+
path: ["limits"],
|
|
163
|
+
message: "byte limits are valid only for split rejection",
|
|
164
|
+
});
|
|
124
165
|
}
|
|
125
166
|
});
|
|
126
167
|
export const observationSourceEventBatchResponseSchema = z.discriminatedUnion("accepted", [
|
|
@@ -129,12 +170,21 @@ export const observationSourceEventBatchResponseSchema = z.discriminatedUnion("a
|
|
|
129
170
|
]);
|
|
130
171
|
function validateDroppedEventDetails(submittedCount, droppedCount, droppedEvents, ctx) {
|
|
131
172
|
if (droppedEvents.length !== droppedCount) {
|
|
132
|
-
ctx.addIssue({
|
|
173
|
+
ctx.addIssue({
|
|
174
|
+
code: z.ZodIssueCode.custom,
|
|
175
|
+
path: ["dropped_events"],
|
|
176
|
+
message: "dropped event details must match dropped count",
|
|
177
|
+
});
|
|
133
178
|
}
|
|
134
179
|
const indexes = new Set();
|
|
135
180
|
for (const [detailIndex, detail] of droppedEvents.entries()) {
|
|
136
|
-
if (detail.event_index >= submittedCount ||
|
|
137
|
-
|
|
181
|
+
if (detail.event_index >= submittedCount ||
|
|
182
|
+
indexes.has(detail.event_index)) {
|
|
183
|
+
ctx.addIssue({
|
|
184
|
+
code: z.ZodIssueCode.custom,
|
|
185
|
+
path: ["dropped_events", detailIndex, "event_index"],
|
|
186
|
+
message: "dropped event indexes must be valid and unique",
|
|
187
|
+
});
|
|
138
188
|
}
|
|
139
189
|
indexes.add(detail.event_index);
|
|
140
190
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type IngestRetryStrategy } from "./delivery-response-contract.js";
|
|
2
1
|
import type { IngestUsageGovernorFeedback } from "../provider/types.js";
|
|
2
|
+
import { type IngestRetryStrategy } from "./delivery-response-contract.js";
|
|
3
|
+
import type { DiagnosticCode } from "./diagnostics.js";
|
|
3
4
|
export declare const BROWSER_REJECTION_STRATEGIES: Readonly<Record<string, IngestRetryStrategy>>;
|
|
4
5
|
export type { IngestRetryStrategy } from "./delivery-response-contract.js";
|
|
5
6
|
export type BrowserDeliveryDecision = {
|
|
@@ -12,6 +13,7 @@ export type BrowserDeliveryDecision = {
|
|
|
12
13
|
readonly acceptedEventCount: number;
|
|
13
14
|
readonly acceptedPartial: boolean;
|
|
14
15
|
readonly reason: string | null;
|
|
16
|
+
readonly diagnosticCodes: readonly DiagnosticCode[];
|
|
15
17
|
};
|
|
16
18
|
export declare const evaluateBrowserDeliveryResponse: (input: {
|
|
17
19
|
readonly status: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { observationSourceEventBatchResponseSchema, } from "./delivery-response-contract.js";
|
|
2
1
|
import { parseGovernorFeedbackEnvelope } from "../provider/usage-governor.js";
|
|
2
|
+
import { observationSourceEventBatchResponseSchema, } from "./delivery-response-contract.js";
|
|
3
3
|
export const BROWSER_REJECTION_STRATEGIES = {
|
|
4
4
|
invalid_request: "stop_and_report",
|
|
5
5
|
unsupported_source_schema_version: "stop_and_report",
|
|
@@ -21,6 +21,7 @@ export const BROWSER_REJECTION_STRATEGIES = {
|
|
|
21
21
|
raw_ingest_produce_failed: "retry_same_batch",
|
|
22
22
|
raw_ingest_ack_unknown: "retry_same_batch",
|
|
23
23
|
raw_ingest_receipt_metadata_failed: "retry_same_batch",
|
|
24
|
+
raw_ingest_receipt_unprocessable: "stop_and_report",
|
|
24
25
|
ingest_config_changed_retryable: "retry_same_batch",
|
|
25
26
|
usage_meter_unavailable: "retry_same_batch",
|
|
26
27
|
};
|
|
@@ -36,6 +37,15 @@ export const evaluateBrowserDeliveryResponse = (input) => {
|
|
|
36
37
|
return invalidResponse();
|
|
37
38
|
}
|
|
38
39
|
const partial = parsed.data.dropped_event_count > 0;
|
|
40
|
+
const diagnosticCodes = [];
|
|
41
|
+
if (partial) {
|
|
42
|
+
diagnosticCodes.push(parsed.data.dropped_events.length > 0
|
|
43
|
+
? "intentional_drop"
|
|
44
|
+
: "duplicate_drop_details_unavailable");
|
|
45
|
+
}
|
|
46
|
+
if (input.status !== 202) {
|
|
47
|
+
diagnosticCodes.push("non_canonical_success_status");
|
|
48
|
+
}
|
|
39
49
|
return {
|
|
40
50
|
ok: true,
|
|
41
51
|
retryStrategy: null,
|
|
@@ -46,6 +56,7 @@ export const evaluateBrowserDeliveryResponse = (input) => {
|
|
|
46
56
|
acceptedEventCount: parsed.data.accepted_event_count,
|
|
47
57
|
acceptedPartial: partial,
|
|
48
58
|
reason: partial ? "accepted_partial" : null,
|
|
59
|
+
diagnosticCodes,
|
|
49
60
|
};
|
|
50
61
|
}
|
|
51
62
|
if (BROWSER_REJECTION_STRATEGIES[parsed.data.reason] !==
|
|
@@ -60,6 +71,7 @@ export const evaluateBrowserDeliveryResponse = (input) => {
|
|
|
60
71
|
acceptedEventCount: parsed.data.accepted_event_count ?? 0,
|
|
61
72
|
acceptedPartial: false,
|
|
62
73
|
reason: parsed.data.reason,
|
|
74
|
+
diagnosticCodes: [],
|
|
63
75
|
};
|
|
64
76
|
}
|
|
65
77
|
return invalidResponse();
|
|
@@ -91,6 +103,7 @@ const invalidResponse = () => ({
|
|
|
91
103
|
acceptedEventCount: 0,
|
|
92
104
|
acceptedPartial: false,
|
|
93
105
|
reason: "invalid_response",
|
|
106
|
+
diagnosticCodes: ["invalid_response"],
|
|
94
107
|
});
|
|
95
108
|
export const classifyIngestHttpStatus = (status) => {
|
|
96
109
|
if (status >= 200 && status < 300)
|
|
@@ -104,29 +117,27 @@ export const classifyIngestHttpStatus = (status) => {
|
|
|
104
117
|
}
|
|
105
118
|
return "retry_same_batch";
|
|
106
119
|
};
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
if (status >= 200 && status < 300)
|
|
110
|
-
return invalidResponse();
|
|
111
|
-
if (retryStrategy === "retry_same_batch")
|
|
112
|
-
return retry(status);
|
|
113
|
-
return terminal(retryStrategy, status);
|
|
114
|
-
};
|
|
115
|
-
const retry = (status) => ({
|
|
116
|
-
ok: false,
|
|
117
|
-
retryStrategy: "retry_same_batch",
|
|
118
|
-
governor: null,
|
|
119
|
-
droppedEventCount: 0,
|
|
120
|
-
acceptedEventCount: 0,
|
|
121
|
-
acceptedPartial: false,
|
|
122
|
-
reason: `http_${status}`,
|
|
123
|
-
});
|
|
124
|
-
const terminal = (retryStrategy, status) => ({
|
|
120
|
+
const RETRYABLE_CLIENT_STATUSES = [408, 422, 425, 429];
|
|
121
|
+
const fallback = (status) => ({
|
|
125
122
|
ok: false,
|
|
126
|
-
retryStrategy,
|
|
123
|
+
retryStrategy: classifyIngestHttpStatus(status),
|
|
127
124
|
governor: null,
|
|
128
125
|
droppedEventCount: 0,
|
|
129
126
|
acceptedEventCount: 0,
|
|
130
127
|
acceptedPartial: false,
|
|
131
128
|
reason: `http_${status}`,
|
|
129
|
+
diagnosticCodes: fallbackDiagnosticCodes(status),
|
|
132
130
|
});
|
|
131
|
+
const fallbackDiagnosticCodes = (status) => {
|
|
132
|
+
if (classifyIngestHttpStatus(status) !== "retry_same_batch")
|
|
133
|
+
return [];
|
|
134
|
+
if (status >= 200 && status <= 299)
|
|
135
|
+
return ["invalid_response"];
|
|
136
|
+
if (status >= 300 && status <= 399)
|
|
137
|
+
return ["protocol_redirect"];
|
|
138
|
+
if (status >= 500 && status <= 599)
|
|
139
|
+
return [];
|
|
140
|
+
if (RETRYABLE_CLIENT_STATUSES.includes(status))
|
|
141
|
+
return ["invalid_response"];
|
|
142
|
+
return ["unknown_status"];
|
|
143
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IngestRetryStrategy } from "./delivery-response.js";
|
|
1
|
+
import type { IngestRetryStrategy } from "./delivery-response-contract.js";
|
|
2
2
|
export type DiagnosticCode = "batch_split" | "consent_persistence_failed" | "consent_purge" | "consent_purge_failed" | "consent_state_unavailable" | "durable_outbox_unavailable" | "duplicate_drop_details_unavailable" | "event_oversize" | "intentional_drop" | "invalid_response" | "legacy_pre_revocation_event_blocked" | "legacy_bridge_capacity_reached" | "legacy_writer_detected" | "non_canonical_success_status" | "outbox_key_collision" | "outbox_record_corrupt" | "protocol_redirect" | "queue_overflow" | "retry_exhausted" | "retry_scheduled" | "shutdown_failure" | "terminal_rejection" | "unknown_status";
|
|
3
3
|
export type DiagnosticLifecycle = "open" | "closing" | "closing_failed" | "closed";
|
|
4
4
|
export type DiagnosticFlushOutcome = "clean" | "failed" | null;
|
|
@@ -51,6 +51,7 @@ const SAFE_REASONS = new Set([
|
|
|
51
51
|
"raw_ingest_ack_unknown",
|
|
52
52
|
"raw_ingest_produce_failed",
|
|
53
53
|
"raw_ingest_receipt_metadata_failed",
|
|
54
|
+
"raw_ingest_receipt_unprocessable",
|
|
54
55
|
"route_throttled",
|
|
55
56
|
"stale_browser_token",
|
|
56
57
|
"unresolved_project_or_environment",
|
|
@@ -131,7 +132,9 @@ const normalize = (input) => {
|
|
|
131
132
|
if (input.reason && SAFE_REASONS.has(input.reason))
|
|
132
133
|
entry.reason = input.reason;
|
|
133
134
|
const attempt = input.attempt;
|
|
134
|
-
if (typeof attempt === "number" &&
|
|
135
|
+
if (typeof attempt === "number" &&
|
|
136
|
+
Number.isSafeInteger(attempt) &&
|
|
137
|
+
attempt > 0) {
|
|
135
138
|
entry.attempt = attempt;
|
|
136
139
|
}
|
|
137
140
|
const count = input.count;
|
|
@@ -20,9 +20,7 @@ const nextMonotonicParts = () => {
|
|
|
20
20
|
typeof globalThis.performance.now === "function"
|
|
21
21
|
? Math.floor(globalThis.performance.now() * 1000)
|
|
22
22
|
: Date.now();
|
|
23
|
-
const monotonicTime = performanceNow > lastMonotonicTime
|
|
24
|
-
? performanceNow
|
|
25
|
-
: lastMonotonicTime + 1;
|
|
23
|
+
const monotonicTime = performanceNow > lastMonotonicTime ? performanceNow : lastMonotonicTime + 1;
|
|
26
24
|
lastMonotonicTime = monotonicTime;
|
|
27
25
|
monotonicCounter = (monotonicCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
28
26
|
return { monotonicTime, counter: monotonicCounter };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CLUEBASE_OTEL_ALLOWED_RESOURCE_KEYS, CLUEBASE_OTEL_SDK_SOURCE_REQUIRED_RESOURCE_KEYS, } from "../_generated/otel-resource-keys.js";
|
|
2
2
|
import { SDK_VERSION } from "../provider/constants.js";
|
|
3
|
-
import {
|
|
4
|
-
const FRONTEND_RESOURCE_KEYS = new Set(
|
|
3
|
+
import { isDeniedKey } from "./sanitize.js";
|
|
4
|
+
const FRONTEND_RESOURCE_KEYS = new Set(CLUEBASE_OTEL_ALLOWED_RESOURCE_KEYS);
|
|
5
5
|
const FRONTEND_REQUIRED_RESOURCE_KEYS = new Set(CLUEBASE_OTEL_SDK_SOURCE_REQUIRED_RESOURCE_KEYS);
|
|
6
6
|
const MAX_RESOURCE_VALUE_BYTES = 512;
|
|
7
7
|
export const createFrontendResourceAttributes = (resolved) => ({
|
|
@@ -27,7 +27,8 @@ export const toFrontendSourceResource = (resource, deniedKeys = []) => {
|
|
|
27
27
|
}
|
|
28
28
|
for (const [key, value] of entries) {
|
|
29
29
|
if (!FRONTEND_RESOURCE_KEYS.has(key) ||
|
|
30
|
-
(isDeniedKey(key, deniedKeys) &&
|
|
30
|
+
(isDeniedKey(key, deniedKeys) &&
|
|
31
|
+
!FRONTEND_REQUIRED_RESOURCE_KEYS.has(key))) {
|
|
31
32
|
continue;
|
|
32
33
|
}
|
|
33
34
|
if (key === "cluebase.service_type" && value !== "frontend") {
|
|
@@ -30,6 +30,7 @@ export declare class ObservationSourceEventExporter implements SpanExporter {
|
|
|
30
30
|
private ignoreSpanReason;
|
|
31
31
|
enqueue(event: BrowserSourceSignalEvent): void;
|
|
32
32
|
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
|
|
33
|
+
private exportAsync;
|
|
33
34
|
forceFlush(): Promise<void>;
|
|
34
35
|
shutdown(): Promise<void>;
|
|
35
36
|
internalMetrics(): {
|
|
@@ -55,6 +55,9 @@ export class ObservationSourceEventExporter {
|
|
|
55
55
|
});
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
+
void this.exportAsync(spans, resultCallback);
|
|
59
|
+
}
|
|
60
|
+
async exportAsync(spans, resultCallback) {
|
|
58
61
|
try {
|
|
59
62
|
const context = this.getContext();
|
|
60
63
|
for (const span of spans) {
|
|
@@ -71,7 +74,7 @@ export class ObservationSourceEventExporter {
|
|
|
71
74
|
this.droppedSpanCount += 1;
|
|
72
75
|
continue;
|
|
73
76
|
}
|
|
74
|
-
const sourceSignal = spanToSourceSignalEvent(span, {
|
|
77
|
+
const sourceSignal = await spanToSourceSignalEvent(span, {
|
|
75
78
|
context,
|
|
76
79
|
deniedKeys: this.deniedKeys,
|
|
77
80
|
});
|
|
@@ -85,7 +88,7 @@ export class ObservationSourceEventExporter {
|
|
|
85
88
|
});
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
|
-
|
|
91
|
+
await this.flushImpl()
|
|
89
92
|
.then(() => resultCallback({ code: ExportResultCode.SUCCESS }))
|
|
90
93
|
.catch((cause) => resultCallback({
|
|
91
94
|
code: ExportResultCode.FAILED,
|