@genn-inc/cluebase-frontend-sdk 0.0.1 → 0.0.2

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 (75) hide show
  1. package/README.md +11 -9
  2. package/dist/_generated/correlation-attribute-keys.js +4 -0
  3. package/dist/_generated/otel-span-conversion.d.ts +0 -34
  4. package/dist/_generated/otel-span-conversion.js +0 -32
  5. package/dist/context/action-context.d.ts +39 -0
  6. package/dist/context/action-context.js +298 -0
  7. package/dist/context/browser-storage.d.ts +0 -12
  8. package/dist/context/browser-storage.js +0 -13
  9. package/dist/context/identity.js +0 -4
  10. package/dist/context/organization.d.ts +0 -4
  11. package/dist/context/organization.js +0 -7
  12. package/dist/context/session.js +0 -3
  13. package/dist/context/tab.js +0 -5
  14. package/dist/context/trace.d.ts +13 -1
  15. package/dist/context/trace.js +119 -21
  16. package/dist/exporter/batch.d.ts +0 -12
  17. package/dist/exporter/batch.js +0 -27
  18. package/dist/exporter/browser-outbox.js +0 -17
  19. package/dist/exporter/client.d.ts +0 -15
  20. package/dist/exporter/client.js +0 -43
  21. package/dist/exporter/delivery-response-contract.js +0 -6
  22. package/dist/exporter/delivery-response.d.ts +0 -16
  23. package/dist/exporter/delivery-response.js +0 -19
  24. package/dist/exporter/frontend-resource.d.ts +0 -9
  25. package/dist/exporter/frontend-resource.js +0 -9
  26. package/dist/exporter/observation-source-event-exporter.d.ts +0 -28
  27. package/dist/exporter/observation-source-event-exporter.js +0 -18
  28. package/dist/exporter/parameter-snapshot.js +1 -24
  29. package/dist/exporter/raw-event-to-source-signal.js +18 -11
  30. package/dist/exporter/sanitize.d.ts +1 -12
  31. package/dist/exporter/sanitize.js +6 -39
  32. package/dist/exporter/source-event.d.ts +2 -0
  33. package/dist/exporter/span-to-source-signal.js +7 -3
  34. package/dist/exporter/text-fingerprint.d.ts +0 -8
  35. package/dist/exporter/text-fingerprint.js +0 -8
  36. package/dist/index.d.ts +0 -4
  37. package/dist/index.js +9 -39
  38. package/dist/instrumentation/action.js +3 -2
  39. package/dist/instrumentation/cluebase-authored-attributes.d.ts +0 -2
  40. package/dist/instrumentation/cluebase-authored-attributes.js +0 -23
  41. package/dist/instrumentation/common.d.ts +0 -63
  42. package/dist/instrumentation/common.js +0 -48
  43. package/dist/instrumentation/dom.d.ts +0 -25
  44. package/dist/instrumentation/dom.js +5 -41
  45. package/dist/instrumentation/drag.js +1 -2
  46. package/dist/instrumentation/form.js +1 -2
  47. package/dist/instrumentation/input.js +10 -8
  48. package/dist/instrumentation/naming-evidence.d.ts +0 -12
  49. package/dist/instrumentation/navigation.js +0 -2
  50. package/dist/instrumentation/network-request.d.ts +2 -2
  51. package/dist/instrumentation/network-request.js +1 -2
  52. package/dist/instrumentation/network-span-attributes.d.ts +0 -7
  53. package/dist/instrumentation/network-span-attributes.js +1 -3
  54. package/dist/instrumentation/network.js +6 -1
  55. package/dist/instrumentation/page.js +0 -2
  56. package/dist/instrumentation/performance.js +0 -7
  57. package/dist/integrations/react-router.js +0 -3
  58. package/dist/internal/internal-log.d.ts +0 -7
  59. package/dist/internal/internal-log.js +0 -11
  60. package/dist/provider/_version.d.ts +1 -1
  61. package/dist/provider/_version.js +1 -1
  62. package/dist/provider/config.d.ts +0 -7
  63. package/dist/provider/config.js +3 -16
  64. package/dist/provider/constants.d.ts +1 -17
  65. package/dist/provider/constants.js +1 -31
  66. package/dist/provider/privacy-policy-store.d.ts +0 -12
  67. package/dist/provider/privacy-policy-store.js +0 -12
  68. package/dist/provider/sdk-event-pipeline.d.ts +1 -1
  69. package/dist/provider/sdk-event-pipeline.js +41 -54
  70. package/dist/provider/sdk-otel-pipeline.js +50 -4
  71. package/dist/provider/sdk-runtime-surfaces.js +3 -1
  72. package/dist/provider/sdk.js +3 -23
  73. package/dist/provider/types.d.ts +3 -39
  74. package/dist/provider/usage-governor.js +0 -2
  75. package/package.json +3 -6
@@ -14,7 +14,6 @@ const resolveClaimStorage = () => {
14
14
  }
15
15
  }
16
16
  catch {
17
- // Ignore and continue without duplicate-tab coordination.
18
17
  }
19
18
  return null;
20
19
  };
@@ -33,7 +32,6 @@ const parseTabClaim = (value) => {
33
32
  }
34
33
  }
35
34
  catch {
36
- // Ignore invalid claim records and let the current document reclaim it.
37
35
  }
38
36
  return null;
39
37
  };
@@ -64,7 +62,6 @@ export class TabManager {
64
62
  this.storage.setItem(STORAGE_KEYS.tabId, tabId);
65
63
  }
66
64
  catch {
67
- // Swallow to keep SDK non-blocking.
68
65
  }
69
66
  return tabId;
70
67
  }
@@ -89,7 +86,6 @@ export class TabManager {
89
86
  }));
90
87
  }
91
88
  catch {
92
- // Swallow to keep SDK non-blocking.
93
89
  }
94
90
  }
95
91
  installLifecycleCleanup() {
@@ -111,7 +107,6 @@ export class TabManager {
111
107
  globalThis.addEventListener("beforeunload", cleanup, { once: true });
112
108
  }
113
109
  catch {
114
- // Swallow to keep SDK non-blocking.
115
110
  }
116
111
  }
117
112
  }
@@ -1,3 +1,4 @@
1
+ import { type ActionContextSnapshot } from "./action-context.js";
1
2
  export type TraceContextSnapshot = {
2
3
  traceId: string;
3
4
  activeSpanId: string | null;
@@ -7,12 +8,14 @@ export type RequestTraceContext = {
7
8
  parentSpanId: string | null;
8
9
  requestId: string;
9
10
  requestSpanId: string;
11
+ actionId: string | null;
10
12
  interactionId: string;
11
13
  interactionInitiator: string | null;
12
14
  interactionStartEventKind: string | null;
13
15
  originFrontendInteractionId: string | null;
14
16
  };
15
17
  export type ActiveInteractionSnapshot = {
18
+ actionId: string | null;
16
19
  interactionId: string;
17
20
  interactionInitiator: string | null;
18
21
  interactionStartEventKind: string | null;
@@ -22,6 +25,7 @@ export type ActiveInteractionSnapshot = {
22
25
  interactionScopeKey: string | null;
23
26
  };
24
27
  export type InteractionStartInput = {
28
+ actionId?: string | null;
25
29
  interactionId?: string | null;
26
30
  viewId?: string | null;
27
31
  interactionInitiator?: string | null;
@@ -41,6 +45,7 @@ export declare class TraceManager {
41
45
  private activeInteraction;
42
46
  private recentInteraction;
43
47
  private latestTrigger;
48
+ private readonly actionContextManager;
44
49
  constructor();
45
50
  reset(options?: {
46
51
  preserveActiveInteraction?: boolean;
@@ -53,7 +58,6 @@ export declare class TraceManager {
53
58
  setActiveSpan(spanId: string | null): void;
54
59
  startFrontendAction(input?: FrontendActionInput): string;
55
60
  endFrontendAction(interactionId?: string | null): void;
56
- private getOrStartFrontendAction;
57
61
  private recordTrigger;
58
62
  startInteraction(input: InteractionStartInput): string;
59
63
  joinOrStartInteraction(input: InteractionStartInput): string;
@@ -62,6 +66,7 @@ export declare class TraceManager {
62
66
  nowMs?: number;
63
67
  }): string | null;
64
68
  getActiveInteractionId(): string | null;
69
+ getActiveActionId(): string | null;
65
70
  getActiveInteractionSnapshot(): ActiveInteractionSnapshot | null;
66
71
  getRecentInteractionSnapshot(nowMs: number | undefined, maxAgeMs: number): ActiveInteractionSnapshot | null;
67
72
  createRequestContext(input?: {
@@ -70,7 +75,14 @@ export declare class TraceManager {
70
75
  urlKey?: string | null;
71
76
  nowMs?: number;
72
77
  }): RequestTraceContext;
78
+ getCurrentActionContext(): ActionContextSnapshot | null;
79
+ getBoundActionContext(): ActionContextSnapshot | null;
80
+ bindEventContext(event: Event): void;
81
+ runWithActionContext<T>(snapshot: ActionContextSnapshot, callback: () => T): T;
82
+ enableAsyncContext(): void;
83
+ disableAsyncContext(): void;
73
84
  private getActiveInteractionState;
85
+ private readAmbientActionContext;
74
86
  private snapshotOf;
75
87
  }
76
88
  export {};
@@ -1,7 +1,10 @@
1
1
  import { generateEventId, generateInteractionId, generateRequestId, generateRequestSpanId, generateTraceId, } from "../exporter/event-id.js";
2
+ import { PrivateActionContextManager, } from "./action-context.js";
2
3
  const generateSpanId = () => {
3
4
  return `span_${generateEventId()}`;
4
5
  };
6
+ const generateActionId = () => `action_${generateEventId()}`;
7
+ const CLICK_SUBMIT_ACTIVATION_WINDOW_MS = 1000;
5
8
  export class TraceManager {
6
9
  constructor() {
7
10
  this.traceId = generateTraceId();
@@ -9,6 +12,7 @@ export class TraceManager {
9
12
  this.activeInteraction = null;
10
13
  this.recentInteraction = null;
11
14
  this.latestTrigger = null;
15
+ this.actionContextManager = new PrivateActionContextManager(() => this.readAmbientActionContext());
12
16
  }
13
17
  reset(options) {
14
18
  this.traceId = generateTraceId();
@@ -38,9 +42,11 @@ export class TraceManager {
38
42
  }
39
43
  startFrontendAction(input = {}) {
40
44
  const nowMs = input.nowMs ?? Date.now();
41
- const interactionId = input.interactionId ?? generateInteractionId();
45
+ const actionId = input.actionId === undefined ? null : input.actionId?.trim() || null;
46
+ const interactionId = input.interactionId?.trim() || generateInteractionId();
42
47
  this.traceId = generateTraceId();
43
48
  this.activeInteraction = {
49
+ actionId,
44
50
  interactionId,
45
51
  interactionInitiator: input.interactionInitiator ?? null,
46
52
  interactionStartEventKind: input.interactionStartEventKind ?? null,
@@ -70,15 +76,6 @@ export class TraceManager {
70
76
  this.activeInteraction = null;
71
77
  this.latestTrigger = null;
72
78
  }
73
- getOrStartFrontendAction(input) {
74
- if (!this.activeInteraction) {
75
- return this.startFrontendAction(input);
76
- }
77
- if (input.viewId && this.activeInteraction.viewId !== input.viewId) {
78
- return this.startFrontendAction(input);
79
- }
80
- return this.activeInteraction.interactionId;
81
- }
82
79
  recordTrigger(input) {
83
80
  this.latestTrigger = {
84
81
  interactionInitiator: input.interactionInitiator ?? null,
@@ -89,10 +86,35 @@ export class TraceManager {
89
86
  interactionScopeKey: input.interactionScopeKey ?? null,
90
87
  };
91
88
  }
92
- // UI 操作はページ visit interaction に参加する。request はこの id を共有せず、
93
- // createRequestContext() で request ごとに fresh な interaction_id を持つ。
94
89
  startInteraction(input) {
95
- const interactionId = this.getOrStartFrontendAction(input);
90
+ const nowMs = input.nowMs ?? Date.now();
91
+ const active = this.activeInteraction;
92
+ const formKey = input.formKey ?? null;
93
+ if (input.interactionId && active?.interactionId === input.interactionId) {
94
+ this.recordTrigger(input);
95
+ return active.interactionId;
96
+ }
97
+ const isSubmitActivation = input.interactionStartEventKind === "submit" &&
98
+ active?.interactionInitiator === "click" &&
99
+ active.formKey !== null &&
100
+ formKey !== null &&
101
+ active.formKey === formKey &&
102
+ this.recentInteraction !== null &&
103
+ nowMs - this.recentInteraction.lastSeenAtMs <=
104
+ CLICK_SUBMIT_ACTIVATION_WINDOW_MS;
105
+ const interactionId = isSubmitActivation
106
+ ? active.interactionId
107
+ : this.startFrontendAction({
108
+ ...input,
109
+ actionId: input.actionId?.trim() || generateActionId(),
110
+ nowMs,
111
+ });
112
+ if (isSubmitActivation && active) {
113
+ this.recentInteraction = {
114
+ ...this.snapshotOf(active),
115
+ lastSeenAtMs: nowMs,
116
+ };
117
+ }
96
118
  this.recordTrigger(input);
97
119
  return interactionId;
98
120
  }
@@ -100,20 +122,54 @@ export class TraceManager {
100
122
  return this.startInteraction(input);
101
123
  }
102
124
  joinClickInteractionForSubmit(input) {
103
- if (!this.activeInteraction)
125
+ const nowMs = input.nowMs ?? Date.now();
126
+ const formKey = input.formKey ?? null;
127
+ if (!this.activeInteraction ||
128
+ this.activeInteraction.interactionInitiator !== "click" ||
129
+ this.activeInteraction.formKey === null ||
130
+ formKey === null ||
131
+ this.activeInteraction.formKey !== formKey ||
132
+ !this.recentInteraction ||
133
+ nowMs - this.recentInteraction.lastSeenAtMs >
134
+ CLICK_SUBMIT_ACTIVATION_WINDOW_MS) {
104
135
  return null;
136
+ }
105
137
  this.recordTrigger({
106
138
  formKey: input.formKey,
107
139
  interactionInitiator: "submit",
108
140
  interactionStartEventKind: "submit",
109
141
  nowMs: input.nowMs,
110
142
  });
143
+ this.recentInteraction = {
144
+ ...this.snapshotOf(this.activeInteraction),
145
+ lastSeenAtMs: nowMs,
146
+ };
111
147
  return this.activeInteraction.interactionId;
112
148
  }
113
149
  getActiveInteractionId() {
114
- return this.getActiveInteractionSnapshot()?.interactionId ?? null;
150
+ return (this.actionContextManager.getCurrent()?.interactionId ??
151
+ this.activeInteraction?.interactionId ??
152
+ null);
153
+ }
154
+ getActiveActionId() {
155
+ return (this.actionContextManager.getCurrent()?.actionId ??
156
+ this.activeInteraction?.actionId ??
157
+ null);
115
158
  }
116
159
  getActiveInteractionSnapshot() {
160
+ const current = this.actionContextManager.getCurrent();
161
+ if (current) {
162
+ return {
163
+ actionId: current.actionId,
164
+ interactionId: current.interactionId,
165
+ interactionInitiator: current.interactionInitiator,
166
+ interactionStartEventKind: current.interactionStartEventKind,
167
+ targetStableKey: current.targetStableKey,
168
+ formKey: current.formKey,
169
+ interactionScopeKind: current.interactionScopeKind,
170
+ interactionScopeKey: current.interactionScopeKey,
171
+ };
172
+ }
117
173
  const active = this.getActiveInteractionState();
118
174
  return active ? this.snapshotOf(active) : null;
119
175
  }
@@ -133,24 +189,66 @@ export class TraceManager {
133
189
  return snapshot;
134
190
  }
135
191
  createRequestContext(input) {
136
- const active = this.getActiveInteractionState();
137
- const trigger = this.latestTrigger;
192
+ const current = this.actionContextManager.getCurrent();
193
+ const synchronousEvent = this.actionContextManager.getSynchronousEventContext();
194
+ const active = current ??
195
+ synchronousEvent ??
196
+ (this.actionContextManager.isEnabled()
197
+ ? null
198
+ : this.getActiveInteractionState());
199
+ const trigger = !this.actionContextManager.isEnabled() && active
200
+ ? this.latestTrigger
201
+ : null;
138
202
  return {
139
- traceId: generateTraceId(),
140
- parentSpanId: this.activeSpanId,
203
+ traceId: current?.traceId ?? generateTraceId(),
204
+ parentSpanId: current?.activeSpanId ?? this.activeSpanId,
141
205
  requestId: input?.requestId ?? generateRequestId(),
142
206
  requestSpanId: generateRequestSpanId(),
207
+ actionId: active?.actionId ?? null,
143
208
  interactionId: generateInteractionId(),
144
- interactionInitiator: trigger?.interactionInitiator ?? "background",
145
- interactionStartEventKind: trigger?.interactionStartEventKind ?? "background_request",
209
+ interactionInitiator: trigger?.interactionInitiator ??
210
+ active?.interactionInitiator ??
211
+ "background",
212
+ interactionStartEventKind: trigger?.interactionStartEventKind ??
213
+ active?.interactionStartEventKind ??
214
+ "background_request",
146
215
  originFrontendInteractionId: active?.interactionId ?? null,
147
216
  };
148
217
  }
218
+ getCurrentActionContext() {
219
+ return (this.actionContextManager.getCurrent() ?? this.readAmbientActionContext());
220
+ }
221
+ getBoundActionContext() {
222
+ return this.actionContextManager.getCurrent();
223
+ }
224
+ bindEventContext(event) {
225
+ this.actionContextManager.bindEvent(event);
226
+ }
227
+ runWithActionContext(snapshot, callback) {
228
+ return this.actionContextManager.run(snapshot, callback);
229
+ }
230
+ enableAsyncContext() {
231
+ this.actionContextManager.enable();
232
+ }
233
+ disableAsyncContext() {
234
+ this.actionContextManager.disable();
235
+ }
149
236
  getActiveInteractionState() {
150
237
  return this.activeInteraction;
151
238
  }
239
+ readAmbientActionContext() {
240
+ const active = this.activeInteraction;
241
+ if (!active)
242
+ return null;
243
+ return {
244
+ ...this.snapshotOf(active),
245
+ traceId: this.traceId,
246
+ activeSpanId: this.activeSpanId,
247
+ };
248
+ }
152
249
  snapshotOf(state) {
153
250
  return {
251
+ actionId: state.actionId,
154
252
  interactionId: state.interactionId,
155
253
  interactionInitiator: state.interactionInitiator,
156
254
  interactionStartEventKind: state.interactionStartEventKind,
@@ -4,18 +4,6 @@ import { type CollectionPermit } from "./browser-outbox.js";
4
4
  import { DiagnosticStore, type DiagnosticsSnapshot } from "./diagnostics.js";
5
5
  import type { IngestClient } from "./client.js";
6
6
  import type { BrowserSourceEvent } from "./source-event.js";
7
- /**
8
- * 束ねて送る側が、送信の口に実際に求めるもの。
9
- *
10
- * **`IngestClient` 全体(26 member)を求めていました。** ここで使うのは 4 つだけ
11
- * なので、試験の作り物は残り 22 個を持てず、`as unknown as IngestClient` で
12
- * 嘘をつくしかない状態でした(`tests/README.md` §10)。求める口を実際に使うものへ
13
- * 狭めると、作り物は嘘なしで満たせます。**5 つ目を使い始めたら、ここが変わって
14
- * 作り物が落ちます。** それが狙いです。
15
- *
16
- * 一覧は `Pick` で本体から引きます。**member の形を書き写しません**
17
- * (`CLAUDE.md` §0.2)。
18
- */
19
7
  export type BatchIngestClient = Pick<IngestClient, "send" | "hasUsableBrowserToken" | "createBatchEnvelopeMetadata" | "measurePayloadBytes">;
20
8
  export type BatchTransportOptions = {
21
9
  enabled: boolean;
@@ -14,18 +14,8 @@ export class BatchTransport {
14
14
  this.flushDeadlineAt = null;
15
15
  this.lifecycle = "open";
16
16
  this.closeHadTerminalDisposition = false;
17
- // Promise tracking the currently executing flush cycle. Awaiters that
18
- // arrive while a flush is in flight await this promise so the SDK's public
19
- // `flush()` resolves after token issuance and canonical ingest have
20
- // completed.
21
17
  this.activeFlushPromise = null;
22
- // A public enqueue cannot await Web Locks, so it appends its durable draft
23
- // mutation here. A flush snapshots and awaits this chain before acquiring
24
- // the same project lock, which prevents a flush from racing an enqueue.
25
18
  this.enqueueTail = Promise.resolve();
26
- // Capture modules and OTel span export both feed source signals into this
27
- // transport. The bounded ring prevents duplicate event_id rows from being
28
- // sent to ingest.
29
19
  this.seenEventIds = new Set();
30
20
  this.enabled = options.enabled;
31
21
  this.flushIntervalMs = Math.max(100, options.flushIntervalMs);
@@ -93,10 +83,6 @@ export class BatchTransport {
93
83
  this.metrics?.increment("dropped_count");
94
84
  return;
95
85
  }
96
- // Take ownership of the event before enqueue returns: `ownEvent` deep
97
- // clones and validates it, so a caller mutating the object afterwards
98
- // cannot taint what is retained and sent. Events without a usable
99
- // event_id cannot be deduped or retained, so drop them here.
100
86
  const collectionPermit = permit ?? this.outbox.capturePermit();
101
87
  const owned = ownEvent(event, collectionPermit);
102
88
  const eventId = event.event_id;
@@ -105,11 +91,6 @@ export class BatchTransport {
105
91
  return;
106
92
  }
107
93
  void this.queueOutboxMutation(async () => {
108
- // Dedup by event_id with a bounded LRU so converged capture and OTel
109
- // emission cannot produce duplicate ingest rows. Registration is
110
- // committed on the serialized mutation queue only after retention
111
- // succeeds, so a rejected retention (capacity or stale generation)
112
- // leaves the id retriable instead of silently swallowing the retry.
113
94
  if (this.seenEventIds.has(eventId)) {
114
95
  return;
115
96
  }
@@ -120,8 +101,6 @@ export class BatchTransport {
120
101
  }
121
102
  this.seenEventIds.add(eventId);
122
103
  if (this.seenEventIds.size > BatchTransport.DEDUP_CAPACITY) {
123
- // Drop the oldest entry to keep the set bounded. Set iteration
124
- // order is insertion order in JS, so the first key is the oldest.
125
104
  const oldest = this.seenEventIds.values().next().value;
126
105
  if (oldest !== undefined)
127
106
  this.seenEventIds.delete(oldest);
@@ -175,8 +154,6 @@ export class BatchTransport {
175
154
  async flush(deadline = this.currentDeadline()) {
176
155
  if (this.lifecycle === "closed")
177
156
  return;
178
- // Link onto the in-flight flush so awaiters resolve after the token and
179
- // ingest pipeline has actually drained.
180
157
  if (this.flushing) {
181
158
  this.flushAgainAfterCurrent = true;
182
159
  if (this.activeFlushPromise) {
@@ -210,10 +187,6 @@ export class BatchTransport {
210
187
  }
211
188
  async runFlushUntilIdle(deadline) {
212
189
  while (true) {
213
- // Enqueue mutations acquire the same project Web Lock as the flush
214
- // cycle. Wait for mutations outside that lock; awaiting `enqueueTail`
215
- // from inside `withDeliveryLock` deadlocks when a capture event arrives
216
- // while the first batch is being sent.
217
190
  const enqueuesAtCycleStart = this.enqueueTail;
218
191
  await enqueuesAtCycleStart;
219
192
  await this.outbox.withDeliveryLock(() => this.runFlushCycle(deadline));
@@ -33,9 +33,6 @@ export class BrowserOutbox {
33
33
  this.initialDenialPending = options.initialConsent === "denied";
34
34
  if (this.initialDenialPending)
35
35
  this.legacyImportBlocked = true;
36
- // Consent is the only state that must be available before the first lock
37
- // acquisition. All outbox scans and mutations happen under the project
38
- // Web Lock in `withDeliveryLock`.
39
36
  if (!this.initialDenialPending)
40
37
  this.restore(false);
41
38
  if (!this.durableOutboxSupported) {
@@ -262,11 +259,6 @@ export class BrowserOutbox {
262
259
  break;
263
260
  }
264
261
  this.pending.push(record);
265
- // The pending record now owns these events. Drop the drafts from the
266
- // in-memory list unconditionally so a failed storage removal cannot
267
- // leave them as live candidates for a duplicate send; the pending
268
- // record carries the responsibility to delete any surviving draft
269
- // record when it is finalized (`removeClaimedDraftRecords`).
270
262
  for (const draft of selected) {
271
263
  this.removeRecord(draftRecordKey(this.prefix, draft.eventKeyHash));
272
264
  }
@@ -293,11 +285,6 @@ export class BrowserOutbox {
293
285
  const key = pendingRecordKey(this.prefix, record.batchId);
294
286
  if (record.kind === "pending" ||
295
287
  record.nextOrdinal === record.children.length - 1) {
296
- // Delete any draft record still claimed by this batch before dropping
297
- // the pending record. If a claimed draft cannot be removed, keep the
298
- // pending record so the draft stays covered and cannot resurrect as a
299
- // fresh event on the next restore; the batch is retried idempotently
300
- // instead.
301
288
  if (!this.removeClaimedDraftRecords(record))
302
289
  return false;
303
290
  if (!this.removeRecord(key))
@@ -346,8 +333,6 @@ export class BrowserOutbox {
346
333
  removed;
347
334
  }
348
335
  for (const pending of this.pending) {
349
- // Also remove any draft record the batch still claims so a draft that
350
- // survived its commit cannot outlive the clear and resurrect on restore.
351
336
  removed = this.removeClaimedDraftRecords(pending) && removed;
352
337
  removed =
353
338
  this.removeRecord(pendingRecordKey(this.prefix, pending.batchId)) &&
@@ -436,8 +421,6 @@ export class BrowserOutbox {
436
421
  });
437
422
  return;
438
423
  }
439
- // A synchronous revocation must fence mutations that were queued before
440
- // its durable commit. Never restore an older permitted record over it.
441
424
  if (this.consent === "denied")
442
425
  return;
443
426
  const restored = this.restore(false);
@@ -27,7 +27,6 @@ export type SendEventsOptions = {
27
27
  requireCachedBrowserToken?: boolean;
28
28
  maxPayloadBytes?: number;
29
29
  metadata?: BatchEnvelopeMetadata;
30
- /** Absolute monotonic deadline shared by one SDK flush or close operation. */
31
30
  deadlineAt?: number;
32
31
  };
33
32
  export type SendEventsResult = {
@@ -72,20 +71,6 @@ export declare class IngestClient {
72
71
  private recordDecision;
73
72
  private postBatch;
74
73
  private createId;
75
- /**
76
- * Obtain a browser ingest token directly from the Cluebase backend.
77
- *
78
- * The SDK POSTs `{ projectKey }` to
79
- * `<endpoint>/api/v1/ingest/browser-tokens`; the browser stamps the `Origin`
80
- * header automatically and the Cluebase backend validates project key + origin
81
- * before minting a short-lived token.
82
- *
83
- * Tokens are cached in-memory until they are within
84
- * `BROWSER_TOKEN_REFRESH_LEEWAY_MS` of expiry, at which point the next
85
- * `ensureBrowserToken()` call transparently refreshes them. The function
86
- * throws on transport / HTTP errors so the caller (`send`) can mark the
87
- * batch as failed without bypassing the credentialled boundary.
88
- */
89
74
  private getUsableCachedBrowserToken;
90
75
  private ensureBrowserToken;
91
76
  private safeEnsureBrowserToken;
@@ -19,17 +19,10 @@ export const resolveCanonicalIngestEndpoint = (endpoint) => {
19
19
  if (endpoint.endsWith(CANONICAL_INGEST_PATH_SUFFIX)) {
20
20
  return endpoint;
21
21
  }
22
- // Strip a trailing slash if the host app provided only the base URL.
23
22
  const trimmed = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
24
23
  if (trimmed.endsWith("/api/v1")) {
25
24
  return `${trimmed}${CANONICAL_INGEST_PATH_SUFFIX}`;
26
25
  }
27
- // Host apps typically pass the bare API origin (e.g. `http://api.example.com`
28
- // or `http://127.0.0.1:4000`). Append the full canonical ingest path so the
29
- // SDK can POST to a real Cluebase endpoint instead of the origin root. This
30
- // mirrors `resolveBrowserTokenEndpoint`, which already rewrites the pathname
31
- // unconditionally — keeping the two resolvers in sync prevents the SDK from
32
- // silently posting events to `/` while still successfully fetching tokens.
33
26
  try {
34
27
  const parsed = new URL(trimmed);
35
28
  if (parsed.pathname === "" || parsed.pathname === "/") {
@@ -40,12 +33,9 @@ export const resolveCanonicalIngestEndpoint = (endpoint) => {
40
33
  }
41
34
  }
42
35
  catch {
43
- // Relative / malformed endpoints fall through unchanged.
44
36
  }
45
37
  return endpoint;
46
38
  };
47
- // Refresh the cached token this many milliseconds before its real expiry so
48
- // transports do not race a token that expires mid-flight.
49
39
  const BROWSER_TOKEN_REFRESH_LEEWAY_MS = 60000;
50
40
  class BrowserTokenRequestError extends Error {
51
41
  constructor(status) {
@@ -58,8 +48,6 @@ export const resolveBrowserTokenEndpoint = (endpoint) => {
58
48
  if (!endpoint) {
59
49
  return CANONICAL_BROWSER_TOKEN_PATH;
60
50
  }
61
- // `endpoint` may already point at an ingest path. Derive the API root, then
62
- // append the canonical browser-tokens path.
63
51
  try {
64
52
  const parsed = new URL(endpoint);
65
53
  parsed.pathname = CANONICAL_BROWSER_TOKEN_PATH;
@@ -68,7 +56,6 @@ export const resolveBrowserTokenEndpoint = (endpoint) => {
68
56
  return parsed.toString();
69
57
  }
70
58
  catch {
71
- // Fall back to a string-based rewrite for unusual / relative endpoints.
72
59
  const trimmed = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
73
60
  const apiV1Idx = trimmed.indexOf("/api/v1");
74
61
  if (apiV1Idx >= 0) {
@@ -82,8 +69,6 @@ export class IngestClient {
82
69
  constructor(options) {
83
70
  this.sourceIdentifier = DEFAULT_FRONTEND_SOURCE_IDENTIFIER;
84
71
  this.producerId = DEFAULT_FRONTEND_SOURCE_IDENTIFIER;
85
- // Memory-only token cache. Refreshed before expiry so the next ingest POST
86
- // always has a live token.
87
72
  this.cachedToken = null;
88
73
  this.pendingTokenRequest = null;
89
74
  this.endpoint = resolveCanonicalIngestEndpoint(options.endpoint);
@@ -231,16 +216,12 @@ export class IngestClient {
231
216
  .byteLength;
232
217
  }
233
218
  stringifyPayload(events, metadata) {
234
- // Emit the canonical batch envelope against `/ingest/browser`. Field names
235
- // follow the camelCase `observationSourceEventBatchRequestSchema`.
236
219
  return JSON.stringify({
237
220
  sourceType: "frontend_sdk",
238
221
  sourceSchemaVersion: String(this.schemaVersion),
239
222
  batchId: metadata.batch_id,
240
223
  idempotencyKey: metadata.idempotency_key,
241
224
  sentAt: metadata.sent_at,
242
- // いま適用している方針の合印。受付側は違う時だけ本体を返す。持っていなければ
243
- // 省く(= まだ全部伏せている)ので、受付側は必ず本体を返してくれる。
244
225
  privacyPolicyToken: appliedPrivacyPolicyToken(),
245
226
  producerMetadata: {
246
227
  producer_id: this.producerId,
@@ -251,9 +232,6 @@ export class IngestClient {
251
232
  });
252
233
  }
253
234
  withSourceEvidence(event) {
254
- // Privacy metadata is explicit at the transport boundary. Re-apply the
255
- // strictest fallback so malformed upstream events never ship without
256
- // privacy declaration.
257
235
  return {
258
236
  ...event,
259
237
  source_event_id: event.source_event_id ?? event.event_id,
@@ -333,7 +311,6 @@ export class IngestClient {
333
311
  bodyText: await response.text(),
334
312
  localSubmittedCount: input.localSubmittedCount,
335
313
  });
336
- // 受付側が方針を返した時だけ適用する。読めない形なら全部伏せた側へ戻る。
337
314
  if (decision.privacyPolicy !== undefined) {
338
315
  applyDeliveredPrivacyPolicy(decision.privacyPolicy);
339
316
  }
@@ -364,20 +341,6 @@ export class IngestClient {
364
341
  createId() {
365
342
  return generateEventId();
366
343
  }
367
- /**
368
- * Obtain a browser ingest token directly from the Cluebase backend.
369
- *
370
- * The SDK POSTs `{ projectKey }` to
371
- * `<endpoint>/api/v1/ingest/browser-tokens`; the browser stamps the `Origin`
372
- * header automatically and the Cluebase backend validates project key + origin
373
- * before minting a short-lived token.
374
- *
375
- * Tokens are cached in-memory until they are within
376
- * `BROWSER_TOKEN_REFRESH_LEEWAY_MS` of expiry, at which point the next
377
- * `ensureBrowserToken()` call transparently refreshes them. The function
378
- * throws on transport / HTTP errors so the caller (`send`) can mark the
379
- * batch as failed without bypassing the credentialled boundary.
380
- */
381
344
  getUsableCachedBrowserToken(nowMs = Date.now()) {
382
345
  if (this.cachedToken &&
383
346
  this.cachedToken.expiresAtMs > nowMs + BROWSER_TOKEN_REFRESH_LEEWAY_MS) {
@@ -394,8 +357,6 @@ export class IngestClient {
394
357
  if (this.pendingTokenRequest) {
395
358
  return await awaitWithAbort(this.pendingTokenRequest, signal);
396
359
  }
397
- // `x-cluebase-project-key` lets the Cluebase API resolve project-scoped CORS and
398
- // token issuance consistently with the JSON body.
399
360
  const request = (async () => {
400
361
  const response = await fetchWithDeadline(this.browserTokenEndpoint, {
401
362
  method: "POST",
@@ -455,10 +416,6 @@ export class IngestClient {
455
416
  if (isConsentAbort(options.signal)) {
456
417
  return { ok: false, retryStrategy: "drop_event" };
457
418
  }
458
- // A transport failure carries no status and may succeed later. Anything
459
- // that did reach Cluebase is classified by the same owner batch delivery
460
- // uses, so a refusal the token endpoint will repeat forever — a malformed
461
- // body, a bad key, a wrong path — stops instead of being replayed.
462
419
  if (!(error instanceof BrowserTokenRequestError) || error.status === null) {
463
420
  return { ok: false, retryStrategy: "retry_same_batch" };
464
421
  }
@@ -1,4 +1,3 @@
1
- // 止める対象と上限の種類は正本から生成した部品を読みます。
2
1
  import { COST_CAPABILITIES, LIMIT_KINDS } from "../_generated/usage-governor-tables.js";
3
2
  import { z } from "zod";
4
3
  const MAX_INGEST_EVENTS_PER_BATCH = 10000;
@@ -33,11 +32,6 @@ const ingestUsageGovernorFeedbackSchema = z.object({
33
32
  });
34
33
  const boundedCount = z.number().int().nonnegative().max(MAX_INGEST_EVENTS_PER_BATCH);
35
34
  const positiveCount = boundedCount.refine((count) => count > 0);
36
- // 顧客が画面で平文にした項目。**受付側の合印と違う時だけ載ります。**
37
- // 封筒の契約は受付側が正本 (`ingest-privacy-policy-delivery.schema.ts`)。この配る
38
- // 部品は `@cluebase/schema` を実行時に持てない (= 顧客の bundle に入れない) ので、
39
- // この file が受け取る形を持ちます。ここと正本がずれたら、届いた方針は形が
40
- // 合わずに捨てられ、**全部伏せた側へ倒れます。**
41
35
  export const ingestPrivacyPolicySchema = z
42
36
  .object({
43
37
  token: z.string().trim().min(1).max(200),
@@ -4,7 +4,6 @@ export declare const BROWSER_REJECTION_STRATEGIES: Readonly<Record<string, Inges
4
4
  export type { IngestRetryStrategy } from "./delivery-response-contract.js";
5
5
  export type BrowserDeliveryDecision = {
6
6
  readonly ok: boolean;
7
- /** 受付側が返した方針。合印が同じなら来ない。 */
8
7
  readonly privacyPolicy?: unknown;
9
8
  readonly retryStrategy: IngestRetryStrategy | null;
10
9
  readonly governor: IngestUsageGovernorFeedback | null;
@@ -19,19 +18,4 @@ export declare const evaluateBrowserDeliveryResponse: (input: {
19
18
  readonly bodyText: string;
20
19
  readonly localSubmittedCount: number;
21
20
  }) => BrowserDeliveryDecision;
22
- /**
23
- * Decide from the HTTP status alone whether a failed exchange with Cluebase is
24
- * worth replaying.
25
- *
26
- * This is the single owner of that judgement. Batch delivery consults it when
27
- * the body carries no usable verdict, and browser-token acquisition consults it
28
- * because that endpoint carries no verdict at all. Without one owner the same
29
- * status is permanent on one path and transient on the other, and a request
30
- * that can never succeed is replayed until the customer's events expire.
31
- *
32
- * A status is only replayed when it says "the same request may succeed later".
33
- * That is the rule the SDK's own core already applies — OpenTelemetry's OTLP
34
- * exporter retries 429/502/503/504 and nothing else (`isExportRetryable`) —
35
- * widened here to the statuses this ingest contract additionally defines.
36
- */
37
21
  export declare const classifyIngestHttpStatus: (status: number) => IngestRetryStrategy;