@genn-inc/cluebase-frontend-sdk 0.0.1
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 +222 -0
- package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
- package/dist/_generated/correlation-attribute-keys.js +33 -0
- package/dist/_generated/identity-pii-policy.d.ts +38 -0
- package/dist/_generated/identity-pii-policy.js +103 -0
- package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
- package/dist/_generated/ingest-privacy-tables.js +890 -0
- package/dist/_generated/otel-resource-keys.d.ts +21 -0
- package/dist/_generated/otel-resource-keys.js +72 -0
- package/dist/_generated/otel-span-conversion.d.ts +48 -0
- package/dist/_generated/otel-span-conversion.js +69 -0
- package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
- package/dist/_generated/parameter-snapshot-leaf.js +37 -0
- package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
- package/dist/_generated/sdk-runtime-policy.js +14 -0
- package/dist/_generated/usage-governor-tables.d.ts +15 -0
- package/dist/_generated/usage-governor-tables.js +44 -0
- package/dist/context/browser-storage.d.ts +15 -0
- package/dist/context/browser-storage.js +38 -0
- package/dist/context/environment.d.ts +37 -0
- package/dist/context/environment.js +229 -0
- package/dist/context/identity.d.ts +16 -0
- package/dist/context/identity.js +139 -0
- package/dist/context/organization.d.ts +16 -0
- package/dist/context/organization.js +60 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +130 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +117 -0
- package/dist/context/trace.d.ts +76 -0
- package/dist/context/trace.js +163 -0
- package/dist/exporter/batch.d.ts +94 -0
- package/dist/exporter/batch.js +485 -0
- package/dist/exporter/browser-outbox-records.d.ts +159 -0
- package/dist/exporter/browser-outbox-records.js +341 -0
- package/dist/exporter/browser-outbox.d.ts +112 -0
- package/dist/exporter/browser-outbox.js +876 -0
- package/dist/exporter/client.d.ts +92 -0
- package/dist/exporter/client.js +541 -0
- package/dist/exporter/delivery-response-contract.d.ts +103 -0
- package/dist/exporter/delivery-response-contract.js +147 -0
- package/dist/exporter/delivery-response.d.ts +37 -0
- package/dist/exporter/delivery-response.js +151 -0
- package/dist/exporter/diagnostics.d.ts +42 -0
- package/dist/exporter/diagnostics.js +148 -0
- package/dist/exporter/event-id.d.ts +10 -0
- package/dist/exporter/event-id.js +75 -0
- package/dist/exporter/frontend-resource.d.ts +20 -0
- package/dist/exporter/frontend-resource.js +52 -0
- package/dist/exporter/local-minimization.d.ts +29 -0
- package/dist/exporter/local-minimization.js +87 -0
- package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
- package/dist/exporter/observation-source-event-exporter.js +154 -0
- package/dist/exporter/parameter-snapshot.d.ts +14 -0
- package/dist/exporter/parameter-snapshot.js +215 -0
- package/dist/exporter/path-template.d.ts +1 -0
- package/dist/exporter/path-template.js +67 -0
- package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
- package/dist/exporter/raw-event-to-source-signal.js +297 -0
- package/dist/exporter/retry.d.ts +8 -0
- package/dist/exporter/retry.js +8 -0
- package/dist/exporter/sanitize.d.ts +30 -0
- package/dist/exporter/sanitize.js +177 -0
- package/dist/exporter/schema-evidence.d.ts +20 -0
- package/dist/exporter/schema-evidence.js +232 -0
- package/dist/exporter/source-event.d.ts +99 -0
- package/dist/exporter/source-event.js +13 -0
- package/dist/exporter/span-to-source-signal.d.ts +8 -0
- package/dist/exporter/span-to-source-signal.js +174 -0
- package/dist/exporter/text-fingerprint.d.ts +9 -0
- package/dist/exporter/text-fingerprint.js +16 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +158 -0
- package/dist/instrumentation/action.d.ts +2 -0
- package/dist/instrumentation/action.js +171 -0
- package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
- package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
- package/dist/instrumentation/common.d.ts +73 -0
- package/dist/instrumentation/common.js +90 -0
- package/dist/instrumentation/dom.d.ts +63 -0
- package/dist/instrumentation/dom.js +640 -0
- package/dist/instrumentation/drag.d.ts +2 -0
- package/dist/instrumentation/drag.js +82 -0
- package/dist/instrumentation/error.d.ts +2 -0
- package/dist/instrumentation/error.js +318 -0
- package/dist/instrumentation/form.d.ts +2 -0
- package/dist/instrumentation/form.js +132 -0
- package/dist/instrumentation/input.d.ts +2 -0
- package/dist/instrumentation/input.js +180 -0
- package/dist/instrumentation/location.d.ts +11 -0
- package/dist/instrumentation/location.js +69 -0
- package/dist/instrumentation/naming-evidence.d.ts +20 -0
- package/dist/instrumentation/naming-evidence.js +11 -0
- package/dist/instrumentation/navigation.d.ts +2 -0
- package/dist/instrumentation/navigation.js +144 -0
- package/dist/instrumentation/network-request.d.ts +103 -0
- package/dist/instrumentation/network-request.js +285 -0
- package/dist/instrumentation/network-span-attributes.d.ts +25 -0
- package/dist/instrumentation/network-span-attributes.js +153 -0
- package/dist/instrumentation/network.d.ts +16 -0
- package/dist/instrumentation/network.js +502 -0
- package/dist/instrumentation/page.d.ts +2 -0
- package/dist/instrumentation/page.js +56 -0
- package/dist/instrumentation/performance.d.ts +2 -0
- package/dist/instrumentation/performance.js +365 -0
- package/dist/integrations/next-router.d.ts +16 -0
- package/dist/integrations/next-router.js +18 -0
- package/dist/integrations/react-router.d.ts +7 -0
- package/dist/integrations/react-router.js +61 -0
- package/dist/internal/internal-log.d.ts +23 -0
- package/dist/internal/internal-log.js +47 -0
- package/dist/internal/metrics.d.ts +10 -0
- package/dist/internal/metrics.js +50 -0
- package/dist/provider/_version.d.ts +1 -0
- package/dist/provider/_version.js +3 -0
- package/dist/provider/config.d.ts +12 -0
- package/dist/provider/config.js +177 -0
- package/dist/provider/constants.d.ts +62 -0
- package/dist/provider/constants.js +118 -0
- package/dist/provider/contracts.d.ts +51 -0
- package/dist/provider/contracts.js +46 -0
- package/dist/provider/privacy-policy-store.d.ts +20 -0
- package/dist/provider/privacy-policy-store.js +56 -0
- package/dist/provider/sampling.d.ts +2 -0
- package/dist/provider/sampling.js +17 -0
- package/dist/provider/sdk-event-pipeline.d.ts +60 -0
- package/dist/provider/sdk-event-pipeline.js +478 -0
- package/dist/provider/sdk-input-normalization.d.ts +7 -0
- package/dist/provider/sdk-input-normalization.js +113 -0
- package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
- package/dist/provider/sdk-otel-pipeline.js +148 -0
- package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
- package/dist/provider/sdk-runtime-surfaces.js +216 -0
- package/dist/provider/sdk.d.ts +2 -0
- package/dist/provider/sdk.js +614 -0
- package/dist/provider/types.d.ts +323 -0
- package/dist/provider/types.js +1 -0
- package/dist/provider/usage-governor.d.ts +7 -0
- package/dist/provider/usage-governor.js +116 -0
- package/package.json +59 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { EnvironmentManager } from "../context/environment.js";
|
|
2
|
+
import { IdentityManager } from "../context/identity.js";
|
|
3
|
+
import { OrganizationManager } from "../context/organization.js";
|
|
4
|
+
import { SessionManager } from "../context/session.js";
|
|
5
|
+
import { TabManager } from "../context/tab.js";
|
|
6
|
+
import { TraceManager } from "../context/trace.js";
|
|
7
|
+
import { BatchTransport } from "../exporter/batch.js";
|
|
8
|
+
import { IngestClient } from "../exporter/client.js";
|
|
9
|
+
import { DiagnosticStore } from "../exporter/diagnostics.js";
|
|
10
|
+
import { detectIdentityPII, hashIdentityValue, isDeniedKey, } from "../exporter/sanitize.js";
|
|
11
|
+
import { InternalMetricsStore } from "../internal/metrics.js";
|
|
12
|
+
import { isConfigValid, resolveConfig, validateConfig } from "./config.js";
|
|
13
|
+
import { DEFAULT_BATCH_FLUSH_THRESHOLD_BYTES, DEFAULT_CONSENT, DEFAULT_MAX_BATCH_QUEUE_BYTES, DEFAULT_MAX_BATCH_QUEUE_ITEMS, SDK_VERSION, } from "./constants.js";
|
|
14
|
+
import { isValidPublicCustomEventName, normalizeProfileAvatarUrl, normalizePublicIdentityId, toStringOrNull, } from "./sdk-input-normalization.js";
|
|
15
|
+
import { OFFICIAL_CUSTOM_EVENT_NAME, ProviderEventPipeline, readCurrentLifecycleUrl, } from "./sdk-event-pipeline.js";
|
|
16
|
+
import { createCluebaseOtelPipeline, forceFlushCluebaseOtelProvider, shutdownCluebaseOtelProvider, shutdownCluebaseOtelProviderAsync, } from "./sdk-otel-pipeline.js";
|
|
17
|
+
import { ProviderRuntimeSurfaces } from "./sdk-runtime-surfaces.js";
|
|
18
|
+
const createNoopLogger = () => {
|
|
19
|
+
return {
|
|
20
|
+
debug: () => undefined,
|
|
21
|
+
warn: () => undefined,
|
|
22
|
+
error: () => undefined,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
const PROFILE_IDENTITY_KEYS = new Set([
|
|
26
|
+
"accountId",
|
|
27
|
+
"account_id",
|
|
28
|
+
"workspaceId",
|
|
29
|
+
"workspace_id",
|
|
30
|
+
"organizationId",
|
|
31
|
+
"organization_id",
|
|
32
|
+
]);
|
|
33
|
+
const safeProfileKeys = (traits, deniedKeys) => Object.keys(traits ?? {})
|
|
34
|
+
.filter((key) => !PROFILE_IDENTITY_KEYS.has(key) && !isDeniedKey(key, deniedKeys))
|
|
35
|
+
.sort();
|
|
36
|
+
class CluebaseBrowserSdkImpl {
|
|
37
|
+
constructor(logger = createNoopLogger()) {
|
|
38
|
+
this.config = null;
|
|
39
|
+
this.initialized = false;
|
|
40
|
+
this.initSignature = null;
|
|
41
|
+
this.consent = DEFAULT_CONSENT;
|
|
42
|
+
this.identityManager = null;
|
|
43
|
+
this.sessionManager = null;
|
|
44
|
+
this.tabManager = null;
|
|
45
|
+
this.organizationManager = null;
|
|
46
|
+
this.environmentManager = null;
|
|
47
|
+
this.traceManager = null;
|
|
48
|
+
this.diagnostics = new DiagnosticStore();
|
|
49
|
+
this.transport = null;
|
|
50
|
+
// OTel TracerProvider wired with the source-signal exporter so every span
|
|
51
|
+
// produced by `@opentelemetry/api`'s global tracer flows through
|
|
52
|
+
// `ObservationSourceEventExporter` → `BatchTransport.enqueue` →
|
|
53
|
+
// Cluebase ingest endpoint.
|
|
54
|
+
this.tracerProvider = null;
|
|
55
|
+
this.logger = logger;
|
|
56
|
+
this.internalMetrics = new InternalMetricsStore();
|
|
57
|
+
this.eventPipeline = new ProviderEventPipeline();
|
|
58
|
+
this.runtimeSurfaces = new ProviderRuntimeSurfaces({
|
|
59
|
+
internalMetrics: this.internalMetrics,
|
|
60
|
+
logger: this.logger,
|
|
61
|
+
enqueueRawEvent: (event) => {
|
|
62
|
+
this.enqueueRawEvent(event);
|
|
63
|
+
},
|
|
64
|
+
getViewId: () => this.eventPipeline.getViewId(),
|
|
65
|
+
getConsent: () => this.consent,
|
|
66
|
+
getObservationContext: () => this.eventPipeline.getObservationContext(),
|
|
67
|
+
getTraceManager: () => this.traceManager,
|
|
68
|
+
getTransport: () => this.transport,
|
|
69
|
+
getTracerProvider: () => this.tracerProvider,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
getInternalMetrics() {
|
|
73
|
+
return this.internalMetrics.getSnapshot();
|
|
74
|
+
}
|
|
75
|
+
getDiagnostics() {
|
|
76
|
+
return this.diagnostics.snapshot();
|
|
77
|
+
}
|
|
78
|
+
init(options) {
|
|
79
|
+
try {
|
|
80
|
+
const resolved = resolveConfig(options);
|
|
81
|
+
const issues = validateConfig(resolved);
|
|
82
|
+
if (!isConfigValid(resolved)) {
|
|
83
|
+
this.logger.warn("Cluebase Frontend SDK init validation failed", {
|
|
84
|
+
error_code: "invalid_sdk_config",
|
|
85
|
+
});
|
|
86
|
+
this.warnPublicDrop("init", `invalid SDK config: ${issues.join(", ")}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const nextSignature = JSON.stringify({
|
|
90
|
+
endpoint: resolved.endpoint,
|
|
91
|
+
originalEndpoint: resolved.originalEndpoint,
|
|
92
|
+
localSetupCheckEndpoint: resolved.localSetupCheckEndpoint,
|
|
93
|
+
projectKey: resolved.projectKey,
|
|
94
|
+
producerId: resolved.producerId,
|
|
95
|
+
transportProfile: resolved.transportProfile,
|
|
96
|
+
autocapture: resolved.autocapture,
|
|
97
|
+
captureNetwork: resolved.captureNetwork,
|
|
98
|
+
captureErrors: resolved.captureErrors,
|
|
99
|
+
capturePerformance: resolved.capturePerformance,
|
|
100
|
+
network: resolved.network,
|
|
101
|
+
batch: resolved.batch,
|
|
102
|
+
privacy: resolved.privacy,
|
|
103
|
+
sampling: resolved.sampling,
|
|
104
|
+
collection: resolved.collection,
|
|
105
|
+
release: resolved.release,
|
|
106
|
+
});
|
|
107
|
+
if (this.initialized && this.initSignature === nextSignature) {
|
|
108
|
+
this.logger.debug("Cluebase Frontend SDK init skipped", {
|
|
109
|
+
reason: "same_config",
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.shutdown();
|
|
114
|
+
this.internalMetrics.reset();
|
|
115
|
+
this.diagnostics = new DiagnosticStore();
|
|
116
|
+
this.config = resolved;
|
|
117
|
+
this.initSignature = nextSignature;
|
|
118
|
+
this.identityManager = new IdentityManager();
|
|
119
|
+
this.sessionManager = new SessionManager();
|
|
120
|
+
this.tabManager = new TabManager();
|
|
121
|
+
this.organizationManager = new OrganizationManager();
|
|
122
|
+
this.traceManager = new TraceManager();
|
|
123
|
+
this.environmentManager = new EnvironmentManager({
|
|
124
|
+
frontendRelease: resolved.release.frontendRelease,
|
|
125
|
+
contextTraitsProvider: resolved.contextTraitsProvider,
|
|
126
|
+
});
|
|
127
|
+
const effectiveBatch = resolved.batch;
|
|
128
|
+
const ingestClient = new IngestClient({
|
|
129
|
+
endpoint: resolved.endpoint,
|
|
130
|
+
projectKey: resolved.projectKey,
|
|
131
|
+
sdkType: "browser",
|
|
132
|
+
sdkVersion: SDK_VERSION,
|
|
133
|
+
schemaVersion: 1,
|
|
134
|
+
metrics: this.internalMetrics,
|
|
135
|
+
diagnostics: this.diagnostics,
|
|
136
|
+
});
|
|
137
|
+
this.transport = new BatchTransport({
|
|
138
|
+
client: ingestClient,
|
|
139
|
+
enabled: effectiveBatch.enabled,
|
|
140
|
+
flushIntervalMs: effectiveBatch.flushIntervalMs,
|
|
141
|
+
maxPayloadBytes: effectiveBatch.maxPayloadBytes,
|
|
142
|
+
flushThresholdBytes: DEFAULT_BATCH_FLUSH_THRESHOLD_BYTES,
|
|
143
|
+
queueMaxBytes: DEFAULT_MAX_BATCH_QUEUE_BYTES,
|
|
144
|
+
queueMaxItems: DEFAULT_MAX_BATCH_QUEUE_ITEMS,
|
|
145
|
+
projectKey: resolved.projectKey,
|
|
146
|
+
initialConsent: this.consent,
|
|
147
|
+
metrics: this.internalMetrics,
|
|
148
|
+
diagnostics: this.diagnostics,
|
|
149
|
+
onGovernorFeedback: (feedback) => {
|
|
150
|
+
this.applyUsageGovernorFeedback(feedback);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
this.consent = this.transport.currentConsent();
|
|
154
|
+
this.transport.start();
|
|
155
|
+
this.eventPipeline.configure({
|
|
156
|
+
config: resolved,
|
|
157
|
+
identityManager: this.identityManager,
|
|
158
|
+
sessionManager: this.sessionManager,
|
|
159
|
+
tabManager: this.tabManager,
|
|
160
|
+
organizationManager: this.organizationManager,
|
|
161
|
+
environmentManager: this.environmentManager,
|
|
162
|
+
traceManager: this.traceManager,
|
|
163
|
+
transport: this.transport,
|
|
164
|
+
internalMetrics: this.internalMetrics,
|
|
165
|
+
logger: this.logger,
|
|
166
|
+
getConsent: () => this.consent,
|
|
167
|
+
});
|
|
168
|
+
if (this.consent !== "denied") {
|
|
169
|
+
this.warmupBrowserToken(ingestClient);
|
|
170
|
+
}
|
|
171
|
+
// Wire WebTracerProvider with the canonical exporter. The provider is
|
|
172
|
+
// registered globally so OTel-instrumented code and capture modules
|
|
173
|
+
// converge on the same transport buffer.
|
|
174
|
+
this.tracerProvider = createCluebaseOtelPipeline({
|
|
175
|
+
resolved,
|
|
176
|
+
getTransport: () => this.transport,
|
|
177
|
+
getContext: () => this.buildContext(),
|
|
178
|
+
});
|
|
179
|
+
this.initialized = true;
|
|
180
|
+
this.refreshSessionCaptureEligibility();
|
|
181
|
+
const sessionLifecycle = this.sessionManager.ensureActiveSession(Date.now());
|
|
182
|
+
if (sessionLifecycle.started) {
|
|
183
|
+
this.enqueueRawEvent({
|
|
184
|
+
type: "session_started",
|
|
185
|
+
occurredAtMs: Date.now(),
|
|
186
|
+
payload: {
|
|
187
|
+
entryUrl: readCurrentLifecycleUrl(),
|
|
188
|
+
},
|
|
189
|
+
}, true);
|
|
190
|
+
}
|
|
191
|
+
this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.sdkInitialized, {
|
|
192
|
+
sdk_action: "init",
|
|
193
|
+
sdk_scope: "lifecycle",
|
|
194
|
+
}, true);
|
|
195
|
+
this.flushLifecycleEvents();
|
|
196
|
+
if (resolved.autocapture && this.consent !== "denied") {
|
|
197
|
+
this.startCaptureModules();
|
|
198
|
+
}
|
|
199
|
+
this.installUnloadHandlers();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
this.logger.error("Cluebase Frontend SDK init failed", {
|
|
203
|
+
error_code: "sdk_operation_failed",
|
|
204
|
+
});
|
|
205
|
+
this.warnPublicDrop("init", "sdk_operation_failed");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
identify(userId, traits) {
|
|
209
|
+
if (!this.ensureInitializedForPublicCall("identify")) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!this.identityManager) {
|
|
213
|
+
this.warnPublicDrop("identify", "identity manager is unavailable");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
// Capture the pre-identify anonymous id so the worker can fuse the
|
|
218
|
+
// anonymous → identified subject as a single resolution chain.
|
|
219
|
+
const normalizedUserId = normalizePublicIdentityId(userId);
|
|
220
|
+
if (!normalizedUserId) {
|
|
221
|
+
this.warnPublicDrop("identify", "userId must be a non-empty string");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const previousAnonymousId = this.identityManager.getContext().anonymousId;
|
|
225
|
+
const sanitizedUserId = this.sanitizeIdentityValue("userId", normalizedUserId);
|
|
226
|
+
const sanitizedTraits = this.sanitizeIdentifyTraits(traits);
|
|
227
|
+
const subjectProfileKeys = safeProfileKeys(traits, this.config?.privacy.deniedKeys ?? []);
|
|
228
|
+
this.identityManager.identify(sanitizedUserId, sanitizedTraits);
|
|
229
|
+
this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.identityIdentified, {
|
|
230
|
+
identity_action: "identify",
|
|
231
|
+
profile_keys: subjectProfileKeys,
|
|
232
|
+
}, false, {
|
|
233
|
+
link_type: "anonymous_to_user",
|
|
234
|
+
anonymous_id: previousAnonymousId,
|
|
235
|
+
user_id: sanitizedUserId,
|
|
236
|
+
});
|
|
237
|
+
this.flushLifecycleEvents();
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
this.logger.error("identify failed", {
|
|
241
|
+
error_code: "sdk_operation_failed",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
setOrganization(organizationId, traits) {
|
|
246
|
+
if (!this.ensureInitializedForPublicCall("setOrganization")) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!this.organizationManager) {
|
|
250
|
+
this.warnPublicDrop("setOrganization", "organization manager is unavailable");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const normalizedOrganizationId = normalizePublicIdentityId(organizationId);
|
|
255
|
+
if (!normalizedOrganizationId) {
|
|
256
|
+
this.warnPublicDrop("setOrganization", "organizationId must be a non-empty string");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const sanitizedOrgId = this.sanitizeIdentityValue("organizationId", normalizedOrganizationId);
|
|
260
|
+
const previousUserId = this.identityManager?.getContext().userId ?? null;
|
|
261
|
+
const organizationProfileKeys = safeProfileKeys(traits, this.config?.privacy.deniedKeys ?? []);
|
|
262
|
+
this.organizationManager.setOrganization(sanitizedOrgId, traits);
|
|
263
|
+
this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.organizationAssociated, {
|
|
264
|
+
organization_action: "associate",
|
|
265
|
+
profile_keys: organizationProfileKeys,
|
|
266
|
+
}, false, {
|
|
267
|
+
link_type: "user_to_organization",
|
|
268
|
+
user_id: previousUserId,
|
|
269
|
+
organization_id: sanitizedOrgId,
|
|
270
|
+
});
|
|
271
|
+
this.flushLifecycleEvents();
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
this.logger.error("setOrganization failed", {
|
|
275
|
+
error_code: "sdk_operation_failed",
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
reset() {
|
|
280
|
+
if (!this.initialized) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const previousUserId = this.identityManager?.getContext().userId ?? null;
|
|
285
|
+
const previousOrganizationId = this.organizationManager?.getContext().organizationId ?? null;
|
|
286
|
+
this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.identityReset, {
|
|
287
|
+
identity_action: "reset",
|
|
288
|
+
}, false, {
|
|
289
|
+
link_type: "logout",
|
|
290
|
+
user_id: previousUserId,
|
|
291
|
+
organization_id: previousOrganizationId,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
this.logger.error("reset failed", {
|
|
296
|
+
error_code: "sdk_operation_failed",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
this.resetSessionState();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
logout() {
|
|
304
|
+
if (!this.initialized) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const previousUserId = this.identityManager?.getContext().userId ?? null;
|
|
309
|
+
const previousOrganizationId = this.organizationManager?.getContext().organizationId ?? null;
|
|
310
|
+
this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.identityReset, {
|
|
311
|
+
identity_action: "logout",
|
|
312
|
+
}, false, {
|
|
313
|
+
link_type: "logout",
|
|
314
|
+
user_id: previousUserId,
|
|
315
|
+
organization_id: previousOrganizationId,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
this.logger.error("logout failed", {
|
|
320
|
+
error_code: "sdk_operation_failed",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
this.resetSessionState();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
resetSessionState() {
|
|
328
|
+
if (!this.initialized) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const ended = this.sessionManager?.endSession(Date.now());
|
|
333
|
+
if (ended) {
|
|
334
|
+
this.enqueueRawEvent({
|
|
335
|
+
type: "session_ended",
|
|
336
|
+
occurredAtMs: Date.now(),
|
|
337
|
+
payload: {
|
|
338
|
+
sessionId: ended.sessionId,
|
|
339
|
+
exitUrl: readCurrentLifecycleUrl(),
|
|
340
|
+
durationMs: ended.durationMs,
|
|
341
|
+
},
|
|
342
|
+
}, true);
|
|
343
|
+
}
|
|
344
|
+
this.identityManager?.reset();
|
|
345
|
+
this.organizationManager?.reset();
|
|
346
|
+
this.traceManager?.reset({ preserveActiveInteraction: true });
|
|
347
|
+
this.sessionManager?.reset(Date.now());
|
|
348
|
+
this.eventPipeline.resetViewId();
|
|
349
|
+
this.enqueueRawEvent({
|
|
350
|
+
type: "session_started",
|
|
351
|
+
occurredAtMs: Date.now(),
|
|
352
|
+
payload: {
|
|
353
|
+
entryUrl: readCurrentLifecycleUrl(),
|
|
354
|
+
},
|
|
355
|
+
}, true);
|
|
356
|
+
this.flushLifecycleEvents();
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
this.logger.error("reset session state failed", {
|
|
360
|
+
error_code: "sdk_operation_failed",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
track(eventName, properties, metrics) {
|
|
365
|
+
if (!this.ensureInitializedForPublicCall("track")) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (!isValidPublicCustomEventName(eventName)) {
|
|
369
|
+
this.warnPublicDrop("track", "eventName must match ^[A-Za-z0-9_.:-]{1,128}$");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
this.enqueueRawEvent({
|
|
373
|
+
type: "custom_emitted",
|
|
374
|
+
occurredAtMs: Date.now(),
|
|
375
|
+
payload: {
|
|
376
|
+
eventName,
|
|
377
|
+
customEventOrigin: "explicit_track",
|
|
378
|
+
properties: properties ?? {},
|
|
379
|
+
metrics: metrics ?? {},
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
// SDK は observation source event だけを送り、ingest core が意味付けを行う。
|
|
384
|
+
setConsent(consent) {
|
|
385
|
+
this.consent = consent;
|
|
386
|
+
if (!this.initialized) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (consent === "denied") {
|
|
390
|
+
this.runtimeSurfaces.abortBrowserTokenWarmup();
|
|
391
|
+
this.stopCaptureModules();
|
|
392
|
+
this.transport?.setConsent(consent);
|
|
393
|
+
this.identityManager?.clearUser();
|
|
394
|
+
this.organizationManager?.reset();
|
|
395
|
+
this.traceManager?.reset();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.transport?.setConsent(consent);
|
|
399
|
+
if (this.config?.autocapture && !this.runtimeSurfaces.isCaptureActive()) {
|
|
400
|
+
this.startCaptureModules();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
ensureInitializedForPublicCall(operation) {
|
|
404
|
+
if (this.initialized && this.config && this.transport) {
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
this.warnPublicDrop(operation, "cluebase.init() has not completed");
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
warnPublicDrop(operation, _reason) {
|
|
411
|
+
this.internalMetrics.increment("public_drop_count");
|
|
412
|
+
this.logger.warn(`Cluebase Frontend SDK ${operation} dropped`, {
|
|
413
|
+
error_code: "sdk_public_drop",
|
|
414
|
+
});
|
|
415
|
+
try {
|
|
416
|
+
if (typeof console !== "undefined" &&
|
|
417
|
+
typeof console.warn === "function") {
|
|
418
|
+
console.warn(`[cluebase] ${operation} dropped`, {
|
|
419
|
+
error_code: "sdk_public_drop",
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// Public warnings must never break the host app.
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
warnPublicFieldDrop(operation, field, _reason) {
|
|
428
|
+
this.logger.warn(`Cluebase Frontend SDK ${operation} field dropped`, {
|
|
429
|
+
field,
|
|
430
|
+
error_code: "sdk_field_drop",
|
|
431
|
+
});
|
|
432
|
+
try {
|
|
433
|
+
if (typeof console !== "undefined" &&
|
|
434
|
+
typeof console.warn === "function") {
|
|
435
|
+
console.warn(`[cluebase] ${operation} field dropped`, { field, error_code: "sdk_field_drop" });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// Public warnings must never break the host app.
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
sanitizeIdentifyTraits(traits) {
|
|
443
|
+
if (!traits) {
|
|
444
|
+
return traits;
|
|
445
|
+
}
|
|
446
|
+
if (!("avatarUrl" in traits)) {
|
|
447
|
+
return traits;
|
|
448
|
+
}
|
|
449
|
+
const avatarUrl = traits.avatarUrl;
|
|
450
|
+
if (avatarUrl == null || toStringOrNull(avatarUrl) === null) {
|
|
451
|
+
return traits;
|
|
452
|
+
}
|
|
453
|
+
if (normalizeProfileAvatarUrl(avatarUrl) !== null) {
|
|
454
|
+
return traits;
|
|
455
|
+
}
|
|
456
|
+
this.warnPublicFieldDrop("identify", "avatarUrl", "must use http:, https:, or a safe raster data:image URL");
|
|
457
|
+
return { ...traits, avatarUrl: null };
|
|
458
|
+
}
|
|
459
|
+
enqueueOfficialCustomEvent(eventName, properties, skipSessionTouch = false, identityLinkHint) {
|
|
460
|
+
this.enqueueRawEvent({
|
|
461
|
+
type: "custom_emitted",
|
|
462
|
+
occurredAtMs: Date.now(),
|
|
463
|
+
payload: {
|
|
464
|
+
eventName,
|
|
465
|
+
customEventOrigin: "official_sdk_lifecycle",
|
|
466
|
+
metrics: {
|
|
467
|
+
count: 1,
|
|
468
|
+
},
|
|
469
|
+
properties,
|
|
470
|
+
...(identityLinkHint ? { identity_link_hint: identityLinkHint } : {}),
|
|
471
|
+
},
|
|
472
|
+
}, skipSessionTouch);
|
|
473
|
+
}
|
|
474
|
+
enqueueRawEvent(event, skipSessionTouch = false) {
|
|
475
|
+
if (!this.initialized || !this.config || !this.transport)
|
|
476
|
+
return;
|
|
477
|
+
this.eventPipeline.enqueueRawEvent(event, skipSessionTouch);
|
|
478
|
+
}
|
|
479
|
+
flushLifecycleEvents() {
|
|
480
|
+
void this.transport?.flush();
|
|
481
|
+
}
|
|
482
|
+
// 内部 transport の即時 flush を要求する。SPA navigation や明示的 shutdown シナリオ用。
|
|
483
|
+
async flush() {
|
|
484
|
+
const deadline = this.transport?.beginFlushDeadline();
|
|
485
|
+
try {
|
|
486
|
+
await awaitUntilDeadline(this.flushCaptureSummaries("manual_flush"), deadline?.signal);
|
|
487
|
+
await forceFlushCluebaseOtelProvider(this.tracerProvider, deadline?.signal);
|
|
488
|
+
await this.transport?.flush(deadline);
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
this.transport?.endFlushDeadline(deadline);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// 残バッファを flush した上で SDK を停止する。テスト / 明示的 shutdown 用。
|
|
495
|
+
async close() {
|
|
496
|
+
const closeSignal = this.transport?.beginClose();
|
|
497
|
+
await awaitUntilDeadline(this.flushCaptureSummaries("close"), closeSignal);
|
|
498
|
+
this.stopCaptureModules();
|
|
499
|
+
await forceFlushCluebaseOtelProvider(this.tracerProvider, closeSignal ?? undefined);
|
|
500
|
+
await this.transport?.close();
|
|
501
|
+
if (this.transport?.getDiagnostics().lifecycle === "closing_failed") {
|
|
502
|
+
this.runtimeSurfaces.stopRuntimeSurfaces();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
await this.shutdownAsync();
|
|
506
|
+
}
|
|
507
|
+
warmupBrowserToken(client) {
|
|
508
|
+
this.runtimeSurfaces.warmupBrowserToken(client);
|
|
509
|
+
}
|
|
510
|
+
startCaptureModules() {
|
|
511
|
+
if (!this.config) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
this.runtimeSurfaces.startCaptureModules(this.config);
|
|
515
|
+
}
|
|
516
|
+
stopCaptureModules() {
|
|
517
|
+
this.runtimeSurfaces.stopCaptureModules();
|
|
518
|
+
}
|
|
519
|
+
async flushCaptureSummaries(reason) {
|
|
520
|
+
await this.runtimeSurfaces.flushCaptureSummaries(reason);
|
|
521
|
+
}
|
|
522
|
+
applyUsageGovernorFeedback(feedback) {
|
|
523
|
+
this.eventPipeline.applyUsageGovernorFeedback(feedback);
|
|
524
|
+
}
|
|
525
|
+
buildContext() {
|
|
526
|
+
return this.eventPipeline.buildContext();
|
|
527
|
+
}
|
|
528
|
+
installUnloadHandlers() {
|
|
529
|
+
this.runtimeSurfaces.installUnloadHandlers(() => {
|
|
530
|
+
const ended = this.sessionManager?.endSession(Date.now());
|
|
531
|
+
if (ended) {
|
|
532
|
+
this.enqueueRawEvent({
|
|
533
|
+
type: "session_ended",
|
|
534
|
+
occurredAtMs: Date.now(),
|
|
535
|
+
payload: {
|
|
536
|
+
sessionId: ended.sessionId,
|
|
537
|
+
exitUrl: readCurrentLifecycleUrl(),
|
|
538
|
+
durationMs: ended.durationMs,
|
|
539
|
+
},
|
|
540
|
+
}, true);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
refreshSessionCaptureEligibility() {
|
|
545
|
+
this.eventPipeline.refreshSessionCaptureEligibility();
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Sanitize an identity value (userId / organizationId) before publishing.
|
|
549
|
+
* If the caller passed an email / phone / credit-card / SSN-shaped
|
|
550
|
+
* value, hash it deterministically and warn via the internal logger.
|
|
551
|
+
* This prevents raw PII from landing in canonical span attributes
|
|
552
|
+
* (`cluebase.actor.user_id`) and raw observation storage.
|
|
553
|
+
*
|
|
554
|
+
*/
|
|
555
|
+
sanitizeIdentityValue(field, raw) {
|
|
556
|
+
if (typeof raw !== "string")
|
|
557
|
+
return raw;
|
|
558
|
+
const trimmed = raw.trim();
|
|
559
|
+
if (!trimmed)
|
|
560
|
+
return trimmed;
|
|
561
|
+
const piiCategory = detectIdentityPII(trimmed);
|
|
562
|
+
if (piiCategory === null)
|
|
563
|
+
return trimmed;
|
|
564
|
+
const hashed = hashIdentityValue(trimmed);
|
|
565
|
+
this.logger.warn(`${field} looks like ${piiCategory}; replaced with deterministic hash to prevent PII leak via canonical span attributes. Use an opaque ID (UUID / ULID / numeric) instead.`);
|
|
566
|
+
return hashed;
|
|
567
|
+
}
|
|
568
|
+
shutdown() {
|
|
569
|
+
this.stopRuntimeSurfaces();
|
|
570
|
+
const provider = this.tracerProvider;
|
|
571
|
+
this.tracerProvider = null;
|
|
572
|
+
shutdownCluebaseOtelProvider(provider);
|
|
573
|
+
this.stopTransport();
|
|
574
|
+
}
|
|
575
|
+
async shutdownAsync() {
|
|
576
|
+
this.stopRuntimeSurfaces();
|
|
577
|
+
const provider = this.tracerProvider;
|
|
578
|
+
this.tracerProvider = null;
|
|
579
|
+
await shutdownCluebaseOtelProviderAsync(provider);
|
|
580
|
+
this.stopTransport();
|
|
581
|
+
}
|
|
582
|
+
stopRuntimeSurfaces() {
|
|
583
|
+
this.runtimeSurfaces.stopRuntimeSurfaces();
|
|
584
|
+
}
|
|
585
|
+
stopTransport() {
|
|
586
|
+
if (this.transport) {
|
|
587
|
+
this.transport.dispose();
|
|
588
|
+
this.transport = null;
|
|
589
|
+
}
|
|
590
|
+
this.traceManager = null;
|
|
591
|
+
this.initialized = false;
|
|
592
|
+
this.initSignature = null;
|
|
593
|
+
this.eventPipeline.clearRuntime();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const awaitUntilDeadline = async (promise, signal) => {
|
|
597
|
+
if (!signal) {
|
|
598
|
+
await promise;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (signal.aborted)
|
|
602
|
+
return;
|
|
603
|
+
await new Promise((resolve) => {
|
|
604
|
+
const abort = () => resolve();
|
|
605
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
606
|
+
void promise.finally(() => {
|
|
607
|
+
signal.removeEventListener("abort", abort);
|
|
608
|
+
resolve();
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
};
|
|
612
|
+
export const createCluebaseBrowserSdk = () => {
|
|
613
|
+
return new CluebaseBrowserSdkImpl();
|
|
614
|
+
};
|