@genn-inc/cluebase-frontend-sdk 0.0.3 → 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/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 +16 -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.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-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 +67 -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 +62 -53
|
@@ -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,
|
|
@@ -56,7 +56,9 @@ export const toPathTemplate = (pathname) => {
|
|
|
56
56
|
}
|
|
57
57
|
const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
58
58
|
const [pathOnly] = normalizedPath.split("?");
|
|
59
|
-
const rawSegments = pathOnly
|
|
59
|
+
const rawSegments = pathOnly
|
|
60
|
+
.split("/")
|
|
61
|
+
.filter((segment) => segment.length > 0);
|
|
60
62
|
const segments = rawSegments.map((segment, index) => shouldTemplateSegment(segment, rawSegments[index - 1] ?? null)
|
|
61
63
|
? ":id"
|
|
62
64
|
: segment);
|
|
@@ -2,8 +2,8 @@ import { CLUEBASE_CORRELATION_ATTRIBUTE_KEYS } from "../_generated/correlation-a
|
|
|
2
2
|
import { cluebaseAuthoredPropertyAttribute } from "../instrumentation/cluebase-authored-attributes.js";
|
|
3
3
|
import { EVENT_SOURCE, EVENT_VERSION, SOURCE_SCHEMA_VERSION, } from "../provider/constants.js";
|
|
4
4
|
import { generateEventId, generateInteractionId, generateRequestId, generateRequestSpanId, generateTraceId, } from "./event-id.js";
|
|
5
|
-
import { isDeniedKey } from "./sanitize.js";
|
|
6
5
|
import { toFrontendSourceResource, } from "./frontend-resource.js";
|
|
6
|
+
import { isDeniedKey } from "./sanitize.js";
|
|
7
7
|
import { SDK_SOURCE_SIGNAL_EVENT_NAME, SDK_SOURCE_SIGNAL_EVENT_TYPE, } from "./source-event.js";
|
|
8
8
|
export const RAW_FRONTEND_SOURCE_SIGNAL_KIND = "frontend_raw_event";
|
|
9
9
|
const PUBLIC_CUSTOM_EVENT_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
@@ -236,7 +236,8 @@ export function rawEventToSourceSignalEvent(raw, options) {
|
|
|
236
236
|
if (viewId)
|
|
237
237
|
attributes["cluebase.view.id"] = viewId;
|
|
238
238
|
if (failureType) {
|
|
239
|
-
attributes[cluebaseAuthoredPropertyAttribute("failure_type")] =
|
|
239
|
+
attributes[cluebaseAuthoredPropertyAttribute("failure_type")] =
|
|
240
|
+
failureType;
|
|
240
241
|
}
|
|
241
242
|
if (outcomeClass)
|
|
242
243
|
attributes["cluebase.outcome_class"] = outcomeClass;
|
|
@@ -291,6 +292,7 @@ export function rawEventToSourceSignalEvent(raw, options) {
|
|
|
291
292
|
attributes,
|
|
292
293
|
events: [],
|
|
293
294
|
resource: toFrontendSourceResource(options.resource, options.deniedKeys),
|
|
295
|
+
raw_resource: toFrontendSourceResource(options.resource, options.deniedKeys),
|
|
294
296
|
context: contextRecord(context, {
|
|
295
297
|
session_id: sessionId,
|
|
296
298
|
trace_id: traceId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { detectTextContainsPII } from "../_generated/identity-pii-policy-text-contains.js";
|
|
2
|
+
import { detectWholeValuePII } from "../_generated/identity-pii-policy-whole-value.js";
|
|
2
3
|
export type SanitizationAccumulator = {
|
|
3
4
|
maskedValueCount: number;
|
|
4
5
|
droppedFieldCount: number;
|
|
@@ -15,5 +16,5 @@ export declare const parseUrlForNetwork: (rawUrl: string) => {
|
|
|
15
16
|
urlMaskedQuery: string;
|
|
16
17
|
};
|
|
17
18
|
export declare const maybeParseJson: (value: unknown) => unknown;
|
|
18
|
-
export {
|
|
19
|
+
export { detectTextContainsPII, detectWholeValuePII };
|
|
19
20
|
export declare const hashIdentityValue: (value: string) => string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sha256 } from "@noble/hashes/sha256";
|
|
2
2
|
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
|
|
3
|
-
import {
|
|
3
|
+
import { detectTextContainsPII, PII_HASH_HEX_LENGTH, PII_HASH_PREFIX, } from "../_generated/identity-pii-policy-text-contains.js";
|
|
4
|
+
import { detectWholeValuePII } from "../_generated/identity-pii-policy-whole-value.js";
|
|
4
5
|
import { isDeniedLookupKey, toDeniedLookupKeySet, } from "../_generated/ingest-privacy-tables.js";
|
|
5
6
|
import { toPathTemplate } from "../exporter/path-template.js";
|
|
6
7
|
import { ALWAYS_DENIED_KEYS, MASKED_VALUE, MAX_SANITIZE_DEPTH, } from "../provider/constants.js";
|
|
@@ -67,7 +68,7 @@ export const sanitizeObject = (value, deniedKeys = [], summary, pathPrefix = "",
|
|
|
67
68
|
if (maskIdentityPii &&
|
|
68
69
|
!preserveExplicitTraits &&
|
|
69
70
|
typeof current === "string" &&
|
|
70
|
-
|
|
71
|
+
detectTextContainsPII(current) !== null) {
|
|
71
72
|
if (current === MASKED_VALUE)
|
|
72
73
|
return current;
|
|
73
74
|
if (summary) {
|
|
@@ -140,5 +141,5 @@ export const maybeParseJson = (value) => {
|
|
|
140
141
|
return null;
|
|
141
142
|
}
|
|
142
143
|
};
|
|
143
|
-
export {
|
|
144
|
+
export { detectTextContainsPII, detectWholeValuePII };
|
|
144
145
|
export const hashIdentityValue = (value) => `${PII_HASH_PREFIX}${bytesToHex(sha256(utf8ToBytes(value))).slice(0, PII_HASH_HEX_LENGTH)}`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import init from "../wasm/pkg-web/cluebase_sdk_core_wasm.js";
|
|
2
|
+
export type SdkCoreWasmInitInput = Parameters<typeof init>[0];
|
|
3
|
+
export declare const initSdkCoreWasm: (input?: SdkCoreWasmInitInput) => Promise<void>;
|
|
4
|
+
export declare const isSdkCoreWasmReady: () => boolean;
|
|
5
|
+
export declare const spanToSdkSignalJson: (spanJson: string, optionsJson: string) => string;
|