@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.
Files changed (73) hide show
  1. package/README.md +21 -0
  2. package/dist/_generated/{identity-pii-policy.d.ts → identity-pii-policy-text-contains.d.ts} +1 -1
  3. package/dist/_generated/{identity-pii-policy.js → identity-pii-policy-text-contains.js} +1 -1
  4. package/dist/_generated/identity-pii-policy-whole-value.d.ts +37 -0
  5. package/dist/_generated/identity-pii-policy-whole-value.js +106 -0
  6. package/dist/_generated/otel-span-conversion.js +1 -9
  7. package/dist/context/browser-storage.js +1 -2
  8. package/dist/context/environment.js +5 -3
  9. package/dist/context/identity.js +5 -9
  10. package/dist/context/organization.d.ts +1 -1
  11. package/dist/context/organization.js +6 -8
  12. package/dist/context/session.js +6 -8
  13. package/dist/context/tab.js +8 -12
  14. package/dist/context/trace.js +2 -1
  15. package/dist/exporter/batch.d.ts +3 -1
  16. package/dist/exporter/batch.js +6 -10
  17. package/dist/exporter/browser-outbox-records.d.ts +1 -1
  18. package/dist/exporter/browser-outbox-records.js +15 -6
  19. package/dist/exporter/browser-outbox.d.ts +5 -2
  20. package/dist/exporter/browser-outbox.js +48 -11
  21. package/dist/exporter/client.d.ts +2 -2
  22. package/dist/exporter/client.js +15 -30
  23. package/dist/exporter/delivery-response-contract.js +67 -17
  24. package/dist/exporter/delivery-response.d.ts +3 -1
  25. package/dist/exporter/delivery-response.js +31 -20
  26. package/dist/exporter/diagnostics.d.ts +1 -1
  27. package/dist/exporter/diagnostics.js +4 -1
  28. package/dist/exporter/event-id.js +1 -3
  29. package/dist/exporter/frontend-resource.js +5 -4
  30. package/dist/exporter/observation-source-event-exporter.d.ts +1 -0
  31. package/dist/exporter/observation-source-event-exporter.js +5 -2
  32. package/dist/exporter/path-template.js +3 -1
  33. package/dist/exporter/raw-event-to-source-signal.js +4 -2
  34. package/dist/exporter/sanitize.d.ts +3 -2
  35. package/dist/exporter/sanitize.js +4 -3
  36. package/dist/exporter/sdk-core-wasm.d.ts +5 -0
  37. package/dist/exporter/sdk-core-wasm.js +18 -0
  38. package/dist/exporter/source-event.d.ts +1 -0
  39. package/dist/exporter/span-to-source-signal.d.ts +1 -1
  40. package/dist/exporter/span-to-source-signal.js +122 -82
  41. package/dist/index.d.ts +5 -4
  42. package/dist/index.js +16 -6
  43. package/dist/instrumentation/action.js +9 -8
  44. package/dist/instrumentation/dom.js +13 -21
  45. package/dist/instrumentation/drag.js +0 -3
  46. package/dist/instrumentation/error.js +3 -1
  47. package/dist/instrumentation/form.js +5 -6
  48. package/dist/instrumentation/input.js +4 -8
  49. package/dist/instrumentation/location.js +3 -1
  50. package/dist/instrumentation/network-request.js +1 -1
  51. package/dist/instrumentation/network-span-attributes.js +2 -2
  52. package/dist/instrumentation/network.js +7 -3
  53. package/dist/instrumentation/performance.js +1 -2
  54. package/dist/internal/internal-log.js +2 -1
  55. package/dist/provider/_version.d.ts +1 -1
  56. package/dist/provider/_version.js +1 -1
  57. package/dist/provider/config.js +0 -1
  58. package/dist/provider/constants.d.ts +1 -1
  59. package/dist/provider/constants.js +0 -1
  60. package/dist/provider/sdk-event-pipeline.d.ts +2 -1
  61. package/dist/provider/sdk-event-pipeline.js +6 -3
  62. package/dist/provider/sdk-input-normalization.js +2 -2
  63. package/dist/provider/sdk-otel-pipeline.js +5 -4
  64. package/dist/provider/sdk-runtime-surfaces.d.ts +15 -12
  65. package/dist/provider/sdk-runtime-surfaces.js +8 -7
  66. package/dist/provider/sdk.js +67 -12
  67. package/dist/provider/types.d.ts +6 -5
  68. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +41 -0
  69. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +267 -0
  70. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
  71. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +12 -0
  72. package/dist/wasm/pkg-web/package.json +15 -0
  73. 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 = "storage" in options ? options.storage ?? null : readStorage();
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({ type: "denied", generation: this.generation });
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 (!dropped)
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 { type BrowserSourceEvent } from "./source-event.js";
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 { IngestUsageGovernorFeedback } from "../provider/types.js";
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";
@@ -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 { SDK_SOURCE_SIGNAL_EVENT_NAME, SDK_SOURCE_SIGNAL_EVENT_TYPE, isBrowserSourceSignalEvent, } from "./source-event.js";
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 { appliedPrivacyPolicyToken, applyDeliveredPrivacyPolicy, } from "../provider/privacy-policy-store.js";
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.metrics?.increment("dropped_count", events.length);
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
- if (!this.diagnostics)
262
+ const diagnostics = this.diagnostics;
263
+ if (!diagnostics)
273
264
  return;
274
- if (decision.reason === "invalid_response") {
275
- this.diagnostics.record({
276
- code: "invalid_response",
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: 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)),
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) || error.status === null) {
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.number().int().nonnegative().max(MAX_INGEST_EVENTS_PER_BATCH);
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.array(ingestDroppedEventSchema).max(MAX_INGEST_EVENTS_PER_BATCH),
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 !== value.accepted_event_count + value.dropped_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.array(ingestDroppedEventSchema).max(MAX_INGEST_EVENTS_PER_BATCH).optional(),
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 = [value.submitted_event_count, value.accepted_event_count, value.dropped_event_count];
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({ code: z.ZodIssueCode.custom, path: ["submitted_event_count"], message: "rejected response counts must be all present or all absent" });
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 || dropped > submitted || accepted + dropped > submitted) {
104
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dropped_event_count"], message: "rejected counts must not exceed submitted count" });
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({ code: z.ZodIssueCode.custom, path: ["submitted_event_count"], message: "drop rejection requires all counts" });
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({ code: z.ZodIssueCode.custom, path: ["dropped_event_count"], message: "drop rejection must reject every submitted event" });
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({ code: z.ZodIssueCode.custom, path: ["dropped_events"], message: "non-drop rejection must not contain dropped event details" });
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") !== (value.limits !== undefined)) {
123
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["limits"], message: "byte limits are valid only for split rejection" });
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({ code: z.ZodIssueCode.custom, path: ["dropped_events"], message: "dropped event details must match dropped count" });
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 || indexes.has(detail.event_index)) {
137
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dropped_events", detailIndex, "event_index"], message: "dropped event indexes must be valid and unique" });
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 fallback = (status) => {
108
- const retryStrategy = classifyIngestHttpStatus(status);
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" && Number.isSafeInteger(attempt) && attempt > 0) {
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 { isDeniedKey } from "./sanitize.js";
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 { CLUEBASE_OTEL_SDK_SOURCE_REQUIRED_RESOURCE_KEYS, CLUEBASE_OTEL_SDK_SOURCE_RESOURCE_KEYS, } from "../_generated/otel-resource-keys.js";
4
- const FRONTEND_RESOURCE_KEYS = new Set(CLUEBASE_OTEL_SDK_SOURCE_RESOURCE_KEYS);
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) && !FRONTEND_REQUIRED_RESOURCE_KEYS.has(key))) {
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
- void this.flushImpl()
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.split("/").filter((segment) => segment.length > 0);
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")] = failureType;
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 { detectIdentityPII } from "../_generated/identity-pii-policy.js";
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 { detectIdentityPII };
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 { detectIdentityPII, PII_HASH_HEX_LENGTH, PII_HASH_PREFIX, } from "../_generated/identity-pii-policy.js";
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
- detectIdentityPII(current) !== null) {
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 { detectIdentityPII };
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;