@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.
- package/README.md +11 -9
- package/dist/_generated/correlation-attribute-keys.js +4 -0
- package/dist/_generated/otel-span-conversion.d.ts +0 -34
- package/dist/_generated/otel-span-conversion.js +0 -32
- package/dist/context/action-context.d.ts +39 -0
- package/dist/context/action-context.js +298 -0
- package/dist/context/browser-storage.d.ts +0 -12
- package/dist/context/browser-storage.js +0 -13
- package/dist/context/identity.js +0 -4
- package/dist/context/organization.d.ts +0 -4
- package/dist/context/organization.js +0 -7
- package/dist/context/session.js +0 -3
- package/dist/context/tab.js +0 -5
- package/dist/context/trace.d.ts +13 -1
- package/dist/context/trace.js +119 -21
- package/dist/exporter/batch.d.ts +0 -12
- package/dist/exporter/batch.js +0 -27
- package/dist/exporter/browser-outbox.js +0 -17
- package/dist/exporter/client.d.ts +0 -15
- package/dist/exporter/client.js +0 -43
- package/dist/exporter/delivery-response-contract.js +0 -6
- package/dist/exporter/delivery-response.d.ts +0 -16
- package/dist/exporter/delivery-response.js +0 -19
- package/dist/exporter/frontend-resource.d.ts +0 -9
- package/dist/exporter/frontend-resource.js +0 -9
- package/dist/exporter/observation-source-event-exporter.d.ts +0 -28
- package/dist/exporter/observation-source-event-exporter.js +0 -18
- package/dist/exporter/parameter-snapshot.js +1 -24
- package/dist/exporter/raw-event-to-source-signal.js +18 -11
- package/dist/exporter/sanitize.d.ts +1 -12
- package/dist/exporter/sanitize.js +6 -39
- package/dist/exporter/source-event.d.ts +2 -0
- package/dist/exporter/span-to-source-signal.js +7 -3
- package/dist/exporter/text-fingerprint.d.ts +0 -8
- package/dist/exporter/text-fingerprint.js +0 -8
- package/dist/index.d.ts +0 -4
- package/dist/index.js +9 -39
- package/dist/instrumentation/action.js +3 -2
- package/dist/instrumentation/cluebase-authored-attributes.d.ts +0 -2
- package/dist/instrumentation/cluebase-authored-attributes.js +0 -23
- package/dist/instrumentation/common.d.ts +0 -63
- package/dist/instrumentation/common.js +0 -48
- package/dist/instrumentation/dom.d.ts +0 -25
- package/dist/instrumentation/dom.js +5 -41
- package/dist/instrumentation/drag.js +1 -2
- package/dist/instrumentation/form.js +1 -2
- package/dist/instrumentation/input.js +10 -8
- package/dist/instrumentation/naming-evidence.d.ts +0 -12
- package/dist/instrumentation/navigation.js +0 -2
- package/dist/instrumentation/network-request.d.ts +2 -2
- package/dist/instrumentation/network-request.js +1 -2
- package/dist/instrumentation/network-span-attributes.d.ts +0 -7
- package/dist/instrumentation/network-span-attributes.js +1 -3
- package/dist/instrumentation/network.js +6 -1
- package/dist/instrumentation/page.js +0 -2
- package/dist/instrumentation/performance.js +0 -7
- package/dist/integrations/react-router.js +0 -3
- package/dist/internal/internal-log.d.ts +0 -7
- package/dist/internal/internal-log.js +0 -11
- package/dist/provider/_version.d.ts +1 -1
- package/dist/provider/_version.js +1 -1
- package/dist/provider/config.d.ts +0 -7
- package/dist/provider/config.js +3 -16
- package/dist/provider/constants.d.ts +1 -17
- package/dist/provider/constants.js +1 -31
- package/dist/provider/privacy-policy-store.d.ts +0 -12
- package/dist/provider/privacy-policy-store.js +0 -12
- package/dist/provider/sdk-event-pipeline.d.ts +1 -1
- package/dist/provider/sdk-event-pipeline.js +41 -54
- package/dist/provider/sdk-otel-pipeline.js +50 -4
- package/dist/provider/sdk-runtime-surfaces.js +3 -1
- package/dist/provider/sdk.js +3 -23
- package/dist/provider/types.d.ts +3 -39
- package/dist/provider/usage-governor.js +0 -2
- package/package.json +3 -6
package/dist/context/tab.js
CHANGED
|
@@ -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
|
}
|
package/dist/context/trace.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/context/trace.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
137
|
-
const
|
|
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 ??
|
|
145
|
-
|
|
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,
|
package/dist/exporter/batch.d.ts
CHANGED
|
@@ -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;
|
package/dist/exporter/batch.js
CHANGED
|
@@ -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;
|
package/dist/exporter/client.js
CHANGED
|
@@ -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;
|