@genn-inc/cluebase-frontend-sdk 0.0.6 → 0.0.9

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 (69) hide show
  1. package/dist/_generated/correlation-attribute-keys.d.ts +3 -0
  2. package/dist/_generated/correlation-attribute-keys.js +35 -0
  3. package/dist/_generated/diagnostic-codes.d.ts +4 -0
  4. package/dist/_generated/diagnostic-codes.js +48 -0
  5. package/dist/_generated/drop-reasons.d.ts +2 -0
  6. package/dist/_generated/drop-reasons.js +25 -0
  7. package/dist/_generated/ingest-routes.d.ts +5 -0
  8. package/dist/_generated/ingest-routes.js +12 -0
  9. package/dist/context/trace.js +2 -4
  10. package/dist/exporter/batch.d.ts +23 -26
  11. package/dist/exporter/batch.js +254 -289
  12. package/dist/exporter/browser-outbox-records.d.ts +7 -24
  13. package/dist/exporter/browser-outbox-records.js +6 -94
  14. package/dist/exporter/browser-outbox.d.ts +23 -28
  15. package/dist/exporter/browser-outbox.js +99 -211
  16. package/dist/exporter/client.d.ts +24 -34
  17. package/dist/exporter/client.js +107 -318
  18. package/dist/exporter/delivery-pipeline.d.ts +72 -0
  19. package/dist/exporter/delivery-pipeline.js +199 -0
  20. package/dist/exporter/delivery-response-contract.d.ts +5 -81
  21. package/dist/exporter/delivery-response-contract.js +4 -166
  22. package/dist/exporter/delivery-response.d.ts +8 -4
  23. package/dist/exporter/delivery-response.js +62 -133
  24. package/dist/exporter/diagnostics.d.ts +2 -1
  25. package/dist/exporter/diagnostics.js +1 -25
  26. package/dist/exporter/event-id.d.ts +1 -0
  27. package/dist/exporter/event-id.js +3 -0
  28. package/dist/exporter/observation-source-event-exporter.d.ts +1 -1
  29. package/dist/exporter/observation-source-event-exporter.js +2 -2
  30. package/dist/exporter/parameter-encryption.d.ts +10 -0
  31. package/dist/exporter/parameter-encryption.js +219 -0
  32. package/dist/exporter/parameter-snapshot.js +25 -3
  33. package/dist/exporter/sdk-core-delivery.d.ts +226 -0
  34. package/dist/exporter/sdk-core-delivery.js +97 -0
  35. package/dist/exporter/sdk-core-wasm.d.ts +24 -1
  36. package/dist/exporter/sdk-core-wasm.js +31 -3
  37. package/dist/exporter/source-event.d.ts +2 -0
  38. package/dist/exporter/source-event.js +17 -0
  39. package/dist/exporter/span-to-source-signal.d.ts +1 -1
  40. package/dist/exporter/span-to-source-signal.js +20 -140
  41. package/dist/index.d.ts +3 -3
  42. package/dist/index.js +5 -3
  43. package/dist/instrumentation/network.d.ts +1 -1
  44. package/dist/instrumentation/network.js +17 -0
  45. package/dist/provider/_version.d.ts +1 -1
  46. package/dist/provider/_version.js +1 -1
  47. package/dist/provider/config.js +16 -3
  48. package/dist/provider/constants.js +0 -1
  49. package/dist/provider/contracts.d.ts +2 -0
  50. package/dist/provider/contracts.js +2 -0
  51. package/dist/provider/lifecycle-event-dedup.d.ts +7 -0
  52. package/dist/provider/lifecycle-event-dedup.js +31 -0
  53. package/dist/provider/sdk-encryption-store.d.ts +7 -0
  54. package/dist/provider/sdk-encryption-store.js +15 -0
  55. package/dist/provider/sdk-event-pipeline.d.ts +1 -3
  56. package/dist/provider/sdk-event-pipeline.js +0 -15
  57. package/dist/provider/sdk-otel-pipeline.d.ts +2 -1
  58. package/dist/provider/sdk-otel-pipeline.js +2 -11
  59. package/dist/provider/sdk-runtime-surfaces.d.ts +3 -2
  60. package/dist/provider/sdk-runtime-surfaces.js +7 -6
  61. package/dist/provider/sdk.js +88 -54
  62. package/dist/provider/types.d.ts +14 -19
  63. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +18 -9
  64. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +170 -29
  65. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
  66. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +10 -9
  67. package/package.json +1 -1
  68. package/dist/provider/usage-governor.d.ts +0 -7
  69. package/dist/provider/usage-governor.js +0 -114
package/dist/index.js CHANGED
@@ -97,14 +97,16 @@ const cluebase = {
97
97
  internalSdk.setOrganization(groupKey, traits);
98
98
  });
99
99
  },
100
+ link(input) {
101
+ safeCall("link", () => {
102
+ internalSdk.link(input);
103
+ });
104
+ },
100
105
  linkExternalAccount(namespace, externalId) {
101
106
  safeCall("linkExternalAccount", () => {
102
107
  internalSdk.linkExternalAccount(namespace, externalId);
103
108
  });
104
109
  },
105
- logout() {
106
- safeCall("logout", () => internalSdk.logout());
107
- },
108
110
  reset() {
109
111
  safeCall("reset", () => internalSdk.reset());
110
112
  },
@@ -9,7 +9,7 @@ export declare const createCluebaseNetworkPropagator: () => TextMapPropagator;
9
9
  export declare const startNetworkCapture: (context: CaptureModuleContext, options?: {
10
10
  tracerProvider?: TracerProvider;
11
11
  deniedKeys?: readonly string[];
12
- ignoreUrlPrefixes?: string[];
12
+ ignoreUrlPrefixes?: readonly string[];
13
13
  tracePropagationOrigins?: string[];
14
14
  createRequestTraceContext?: (input?: RequestTraceContextInput) => RequestTraceContext;
15
15
  }) => CaptureStopHandle;
@@ -497,6 +497,23 @@ export const startNetworkCapture = (context, options = {}) => {
497
497
  flush: async () => {
498
498
  await waitForFetchSpanEnd();
499
499
  },
500
+ refreshIgnoreUrlPrefixes: () => {
501
+ if (activeCapture !== capture)
502
+ return;
503
+ const ignoreUrls = urlPrefixPatterns(capture.ignoreUrlPrefixes);
504
+ if (fetchInstrumentation) {
505
+ fetchInstrumentation.setConfig({
506
+ ...fetchInstrumentation.getConfig(),
507
+ ignoreUrls,
508
+ });
509
+ }
510
+ if (xhrInstrumentation) {
511
+ xhrInstrumentation.setConfig({
512
+ ...xhrInstrumentation.getConfig(),
513
+ ignoreUrls,
514
+ });
515
+ }
516
+ },
500
517
  stop: () => {
501
518
  if (activeCapture !== capture)
502
519
  return;
@@ -1 +1 @@
1
- export declare const SDK_VERSION: "0.0.6";
1
+ export declare const SDK_VERSION: "0.0.9";
@@ -1,3 +1,3 @@
1
1
  // This file is generated by scripts/generate-version-module.mjs.
2
2
  // Do NOT edit by hand. The source of truth is `package.json` `version`.
3
- export const SDK_VERSION = "0.0.6";
3
+ export const SDK_VERSION = "0.0.9";
@@ -1,3 +1,4 @@
1
+ import { CANONICAL_BROWSER_INGEST_PATH, CLUEBASE_API_V1_PREFIX, } from "../_generated/ingest-routes.js";
1
2
  import { ALWAYS_DENIED_KEYS, DEFAULT_API_BASE_URL, DEFAULT_CONFIG, DEFAULT_FRONTEND_SOURCE_IDENTIFIER, MAX_DIAGNOSTIC_TTL_MS, } from "./constants.js";
2
3
  import { SDK_COLLECTION_MODE } from "./contracts.js";
3
4
  const normalizeKeyList = (keys) => {
@@ -74,6 +75,12 @@ export const parseSendableUrl = (value) => {
74
75
  return null;
75
76
  }
76
77
  };
78
+ const isExtendableIngestPath = (pathname) => {
79
+ const path = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
80
+ return (path === "" ||
81
+ path.endsWith(CLUEBASE_API_V1_PREFIX) ||
82
+ path.endsWith(CANONICAL_BROWSER_INGEST_PATH));
83
+ };
77
84
  const normalizeConfiguredEndpoint = (value) => parseSendableUrl(value)?.toString() ?? "";
78
85
  const normalizeTracePropagationOrigin = (value) => {
79
86
  const trimmed = value.trim();
@@ -101,7 +108,6 @@ export const resolveConfig = (options) => {
101
108
  return {
102
109
  endpoint: originalEndpoint,
103
110
  originalEndpoint,
104
- localSetupCheckEndpoint: null,
105
111
  projectKey: options.projectKey?.trim() ?? "",
106
112
  producerId: DEFAULT_FRONTEND_SOURCE_IDENTIFIER,
107
113
  autocapture: options.autocapture ?? DEFAULT_CONFIG.autocapture,
@@ -138,8 +144,15 @@ export const resolveConfig = (options) => {
138
144
  };
139
145
  export const validateConfig = (config) => {
140
146
  const issues = [];
141
- if (!config.endpoint) {
142
- issues.push("endpoint is required");
147
+ const endpoint = parseSendableUrl(config.endpoint);
148
+ if (!endpoint) {
149
+ issues.push("endpoint must be an absolute http(s) url with a host, such as https://api.cluebase.io");
150
+ }
151
+ else if (endpoint.search || endpoint.hash) {
152
+ issues.push("endpoint must not carry a query string or fragment; pass the api base url only");
153
+ }
154
+ else if (!isExtendableIngestPath(endpoint.pathname)) {
155
+ issues.push(`endpoint path cannot be extended to ${CANONICAL_BROWSER_INGEST_PATH}; pass the api base url, the ${CLUEBASE_API_V1_PREFIX} base, or the canonical ingest path`);
143
156
  }
144
157
  if (!config.projectKey) {
145
158
  issues.push("projectKey is required");
@@ -41,7 +41,6 @@ export const MAX_SANITIZE_DEPTH = 20;
41
41
  export const DEFAULT_CONFIG = {
42
42
  endpoint: "",
43
43
  originalEndpoint: "",
44
- localSetupCheckEndpoint: null,
45
44
  projectKey: "",
46
45
  producerId: DEFAULT_FRONTEND_SOURCE_IDENTIFIER,
47
46
  autocapture: true,
@@ -47,5 +47,7 @@ export declare const PARAMETER_CAPTURE_MODE: {
47
47
  readonly maskedFingerprint: "masked_fingerprint";
48
48
  readonly forbidden: "forbidden";
49
49
  readonly truncated: "truncated";
50
+ readonly encrypted: "encrypted";
51
+ readonly plain: "plain";
50
52
  };
51
53
  export type ParameterCaptureMode = (typeof PARAMETER_CAPTURE_MODE)[keyof typeof PARAMETER_CAPTURE_MODE];
@@ -43,4 +43,6 @@ export const PARAMETER_CAPTURE_MODE = {
43
43
  maskedFingerprint: "masked_fingerprint",
44
44
  forbidden: "forbidden",
45
45
  truncated: "truncated",
46
+ encrypted: "encrypted",
47
+ plain: "plain",
46
48
  };
@@ -0,0 +1,7 @@
1
+ import type { CanonicalJsonValue } from "./types.js";
2
+ export type LifecycleEventIdentity = Readonly<Record<string, CanonicalJsonValue>>;
3
+ export declare class LifecycleEventDedup {
4
+ private readonly seenAtMs;
5
+ shouldEmit(identity: LifecycleEventIdentity): boolean;
6
+ clear(): void;
7
+ }
@@ -0,0 +1,31 @@
1
+ import { canonicalize } from "json-canonicalize";
2
+ const LIFECYCLE_DEDUPE_TTL_MS = 300000;
3
+ const LIFECYCLE_DEDUPE_MAX_ENTRIES = 4096;
4
+ export class LifecycleEventDedup {
5
+ constructor() {
6
+ this.seenAtMs = new Map();
7
+ }
8
+ shouldEmit(identity) {
9
+ const key = canonicalize(identity);
10
+ const now = Date.now();
11
+ for (const [seenKey, seenAt] of this.seenAtMs) {
12
+ if (now - seenAt > LIFECYCLE_DEDUPE_TTL_MS) {
13
+ this.seenAtMs.delete(seenKey);
14
+ }
15
+ }
16
+ if (this.seenAtMs.has(key)) {
17
+ this.seenAtMs.set(key, now);
18
+ return false;
19
+ }
20
+ if (this.seenAtMs.size >= LIFECYCLE_DEDUPE_MAX_ENTRIES) {
21
+ const oldest = [...this.seenAtMs.entries()].sort((left, right) => left[1] - right[1])[0]?.[0];
22
+ if (oldest !== undefined)
23
+ this.seenAtMs.delete(oldest);
24
+ }
25
+ this.seenAtMs.set(key, now);
26
+ return true;
27
+ }
28
+ clear() {
29
+ this.seenAtMs.clear();
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ export type SdkEncryptionConfig = {
2
+ readonly keyVersion: number;
3
+ readonly publicKey: string;
4
+ };
5
+ export declare const applyDeliveredSdkEncryption: (value: unknown) => void;
6
+ export declare const resetSdkEncryption: () => void;
7
+ export declare const currentSdkEncryption: () => SdkEncryptionConfig | null;
@@ -0,0 +1,15 @@
1
+ import { ingestSdkEncryptionSchema } from "../exporter/delivery-response-contract.js";
2
+ let appliedEncryption = null;
3
+ export const applyDeliveredSdkEncryption = (value) => {
4
+ const parsed = ingestSdkEncryptionSchema.safeParse(value);
5
+ appliedEncryption = parsed.success
6
+ ? {
7
+ keyVersion: parsed.data.key_version,
8
+ publicKey: parsed.data.public_key,
9
+ }
10
+ : null;
11
+ };
12
+ export const resetSdkEncryption = () => {
13
+ appliedEncryption = null;
14
+ };
15
+ export const currentSdkEncryption = () => appliedEncryption;
@@ -6,7 +6,7 @@ import type { TabManager } from "../context/tab.js";
6
6
  import { TraceManager } from "../context/trace.js";
7
7
  import type { BatchTransport } from "../exporter/batch.js";
8
8
  import type { InternalMetricsStore } from "../internal/metrics.js";
9
- import type { CluebaseConsentState, CluebaseContext, CluebaseInternalLogger, IngestUsageGovernorFeedback, RawCapturedEvent, ResolvedCluebaseConfig } from "./types.js";
9
+ import type { CluebaseConsentState, CluebaseContext, CluebaseInternalLogger, RawCapturedEvent, ResolvedCluebaseConfig } from "./types.js";
10
10
  export declare const OFFICIAL_CUSTOM_EVENT_NAME: {
11
11
  readonly identityIdentified: "identity_identified";
12
12
  readonly identityReset: "identity_reset";
@@ -33,7 +33,6 @@ export declare const getSessionCaptureEligibility: (transport: BatchTransport) =
33
33
  export declare class ProviderEventPipeline {
34
34
  private runtime;
35
35
  private currentViewId;
36
- private usageGovernorFeedback;
37
36
  private sessionCaptureEligible;
38
37
  private unregisterSessionCaptureEligibility;
39
38
  configure(runtime: ProviderEventPipelineRuntime): void;
@@ -48,7 +47,6 @@ export declare class ProviderEventPipeline {
48
47
  tabId: string | null;
49
48
  };
50
49
  enqueueRawEvent(event: RawCapturedEvent, skipSessionTouch?: boolean): void;
51
- applyUsageGovernorFeedback(feedback: IngestUsageGovernorFeedback | null | undefined): void;
52
50
  refreshSessionCaptureEligibility(): void;
53
51
  buildContext(event?: RawCapturedEvent): CluebaseContext;
54
52
  private attachInteractionContext;
@@ -6,7 +6,6 @@ import { sanitizeObject, } from "../exporter/sanitize.js";
6
6
  import { FRONTEND_EVENT_NAME } from "./contracts.js";
7
7
  import { shouldSampleSession } from "./sampling.js";
8
8
  import { stripQueryAndHashFromUrl, toStringOrNull, toSubjectProfile, } from "./sdk-input-normalization.js";
9
- import { DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK, hasUsageGovernorFeedbackChanged, isCostCapabilityDisabled, normalizeIngestUsageGovernorFeedback, } from "./usage-governor.js";
10
9
  const TRACE_ROOT_EVENTS = new Set([
11
10
  "page_view",
12
11
  "route_change",
@@ -76,7 +75,6 @@ export class ProviderEventPipeline {
76
75
  constructor() {
77
76
  this.runtime = null;
78
77
  this.currentViewId = null;
79
- this.usageGovernorFeedback = DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK;
80
78
  this.sessionCaptureEligible = true;
81
79
  this.unregisterSessionCaptureEligibility = null;
82
80
  }
@@ -86,14 +84,12 @@ export class ProviderEventPipeline {
86
84
  this.unregisterSessionCaptureEligibility =
87
85
  registerSessionCaptureEligibility(runtime.transport, () => this.sessionCaptureEligible);
88
86
  this.currentViewId = generateViewId();
89
- this.usageGovernorFeedback = DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK;
90
87
  this.refreshSessionCaptureEligibility();
91
88
  }
92
89
  clearRuntime() {
93
90
  this.unregisterSessionCaptureEligibility?.();
94
91
  this.unregisterSessionCaptureEligibility = null;
95
92
  this.runtime = null;
96
- this.usageGovernorFeedback = DEFAULT_INGEST_USAGE_GOVERNOR_FEEDBACK;
97
93
  this.sessionCaptureEligible = true;
98
94
  }
99
95
  resetViewId() {
@@ -150,10 +146,6 @@ export class ProviderEventPipeline {
150
146
  runtime.internalMetrics.increment("sampling_dropped_count", droppedCount);
151
147
  }
152
148
  }
153
- if (isCostCapabilityDisabled(this.usageGovernorFeedback, "event_collection")) {
154
- runtime.internalMetrics.increment("dropped_count");
155
- return;
156
- }
157
149
  const sourceSignal = rawEventToSourceSignalEvent(eventWithLocation, {
158
150
  context,
159
151
  resource: createFrontendResourceAttributes(runtime.config),
@@ -188,13 +180,6 @@ export class ProviderEventPipeline {
188
180
  });
189
181
  }
190
182
  }
191
- applyUsageGovernorFeedback(feedback) {
192
- const nextFeedback = normalizeIngestUsageGovernorFeedback(feedback);
193
- if (!hasUsageGovernorFeedbackChanged(this.usageGovernorFeedback, nextFeedback)) {
194
- return;
195
- }
196
- this.usageGovernorFeedback = nextFeedback;
197
- }
198
183
  refreshSessionCaptureEligibility() {
199
184
  const config = this.runtime?.config;
200
185
  if (!config) {
@@ -6,8 +6,9 @@ type CreateCluebaseOtelPipelineOptions = {
6
6
  resolved: ResolvedCluebaseConfig;
7
7
  getTransport: () => BatchTransport | null;
8
8
  getContext: () => CluebaseContext;
9
+ ignoreUrlPrefixes: readonly string[];
9
10
  };
10
- export declare const createCluebaseOtelPipeline: ({ resolved, getTransport, getContext, }: CreateCluebaseOtelPipelineOptions) => CluebaseOtelProvider | null;
11
+ export declare const createCluebaseOtelPipeline: ({ resolved, getTransport, getContext, ignoreUrlPrefixes, }: CreateCluebaseOtelPipelineOptions) => CluebaseOtelProvider | null;
11
12
  export declare const shutdownCluebaseOtelProvider: (provider: CluebaseOtelProvider | null) => void;
12
13
  export declare const shutdownCluebaseOtelProviderAsync: (provider: CluebaseOtelProvider | null) => Promise<void>;
13
14
  export declare const forceFlushCluebaseOtelProvider: (provider: CluebaseOtelProvider | null, signal?: AbortSignal) => Promise<void>;
@@ -2,7 +2,6 @@ import { context, propagation, trace } from "@opentelemetry/api";
2
2
  import { resourceFromAttributes } from "@opentelemetry/resources";
3
3
  import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
4
4
  import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
5
- import { resolveBrowserTokenEndpoint, resolveCanonicalIngestEndpoint, } from "../exporter/client.js";
6
5
  import { createFrontendResourceAttributes } from "../exporter/frontend-resource.js";
7
6
  import { ObservationSourceEventExporter } from "../exporter/observation-source-event-exporter.js";
8
7
  import { createCluebaseNetworkPropagator } from "../instrumentation/network.js";
@@ -16,7 +15,7 @@ const getTracerDelegate = (provider) => {
16
15
  const candidate = provider;
17
16
  return candidate.getDelegate?.();
18
17
  };
19
- export const createCluebaseOtelPipeline = ({ resolved, getTransport, getContext, }) => {
18
+ export const createCluebaseOtelPipeline = ({ resolved, getTransport, getContext, ignoreUrlPrefixes, }) => {
20
19
  if (!getTransport()) {
21
20
  return null;
22
21
  }
@@ -58,15 +57,7 @@ export const createCluebaseOtelPipeline = ({ resolved, getTransport, getContext,
58
57
  },
59
58
  getContext,
60
59
  deniedKeys: resolved.privacy?.deniedKeys ?? [],
61
- ignoreUrlPrefixes: [
62
- resolveCanonicalIngestEndpoint(resolved.endpoint),
63
- resolveCanonicalIngestEndpoint(resolved.originalEndpoint),
64
- resolveBrowserTokenEndpoint(resolved.endpoint),
65
- resolveBrowserTokenEndpoint(resolved.originalEndpoint),
66
- ...(resolved.localSetupCheckEndpoint
67
- ? [resolved.localSetupCheckEndpoint]
68
- : []),
69
- ].filter((url) => typeof url === "string" && url.length > 0),
60
+ ignoreUrlPrefixes,
70
61
  });
71
62
  const provider = new WebTracerProvider({
72
63
  resource,
@@ -1,6 +1,6 @@
1
1
  import { TraceManager } from "../context/trace.js";
2
2
  import type { BatchTransport } from "../exporter/batch.js";
3
- import { type IngestClient } from "../exporter/client.js";
3
+ import type { IngestClient } from "../exporter/client.js";
4
4
  import type { InternalMetricsStore } from "../internal/metrics.js";
5
5
  import { type CluebaseOtelProvider } from "./sdk-otel-pipeline.js";
6
6
  import type { CaptureModuleContext, CluebaseConsentState, CluebaseInternalLogger, RawCapturedEvent, ResolvedCluebaseConfig } from "./types.js";
@@ -41,7 +41,8 @@ export declare class ProviderRuntimeSurfaces {
41
41
  constructor(options: ProviderRuntimeSurfacesOptions);
42
42
  buildCaptureContext(): CaptureModuleContext;
43
43
  warmupBrowserToken(client: IngestClient): void;
44
- startCaptureModules(config: ResolvedCluebaseConfig): void;
44
+ refreshIgnoreUrlPrefixes(): void;
45
+ startCaptureModules(config: ResolvedCluebaseConfig, ignoreUrlPrefixes: readonly string[]): void;
45
46
  stopCaptureModules(): void;
46
47
  abortBrowserTokenWarmup(): void;
47
48
  flushCaptureSummaries(reason: string): Promise<void>;
@@ -1,5 +1,4 @@
1
1
  import { TraceManager } from "../context/trace.js";
2
- import { resolveBrowserTokenEndpoint, resolveCanonicalIngestEndpoint, } from "../exporter/client.js";
3
2
  import { startActionCapture } from "../instrumentation/action.js";
4
3
  import { startDragCapture } from "../instrumentation/drag.js";
5
4
  import { startErrorCapture } from "../instrumentation/error.js";
@@ -88,7 +87,12 @@ export class ProviderRuntimeSurfaces {
88
87
  }
89
88
  });
90
89
  }
91
- startCaptureModules(config) {
90
+ refreshIgnoreUrlPrefixes() {
91
+ for (const handle of this.captureHandles) {
92
+ handle.refreshIgnoreUrlPrefixes?.();
93
+ }
94
+ }
95
+ startCaptureModules(config, ignoreUrlPrefixes) {
92
96
  const captureContext = this.buildCaptureContext();
93
97
  const handles = [];
94
98
  handles.push(startInputCapture(captureContext));
@@ -101,10 +105,7 @@ export class ProviderRuntimeSurfaces {
101
105
  handles.push(startNetworkCapture(captureContext, {
102
106
  tracerProvider: this.getTracerProvider() ?? undefined,
103
107
  deniedKeys: config.privacy.deniedKeys,
104
- ignoreUrlPrefixes: [
105
- resolveCanonicalIngestEndpoint(config.endpoint),
106
- resolveBrowserTokenEndpoint(config.endpoint),
107
- ],
108
+ ignoreUrlPrefixes,
108
109
  tracePropagationOrigins: config.network.tracePropagationOrigins,
109
110
  createRequestTraceContext: (input) => {
110
111
  const traceManager = this.getTraceManager();
@@ -12,6 +12,7 @@ import { initSdkCoreWasm } from "../exporter/sdk-core-wasm.js";
12
12
  import { InternalMetricsStore } from "../internal/metrics.js";
13
13
  import { isConfigValid, resolveConfig, validateConfig } from "./config.js";
14
14
  import { DEFAULT_BATCH_FLUSH_THRESHOLD_BYTES, DEFAULT_CONSENT, DEFAULT_MAX_BATCH_QUEUE_BYTES, DEFAULT_MAX_BATCH_QUEUE_ITEMS, SDK_VERSION, } from "./constants.js";
15
+ import { LifecycleEventDedup } from "./lifecycle-event-dedup.js";
15
16
  import { getSdkErrorDetails, reportSdkError } from "./sdk-error.js";
16
17
  import { OFFICIAL_CUSTOM_EVENT_NAME, ProviderEventPipeline, readCurrentLifecycleUrl, } from "./sdk-event-pipeline.js";
17
18
  import { isValidPublicCustomEventName, normalizeProfileAvatarUrl, normalizePublicIdentityId, toStringOrNull, } from "./sdk-input-normalization.js";
@@ -48,6 +49,8 @@ class CluebaseBrowserSdkImpl {
48
49
  this.environmentManager = null;
49
50
  this.traceManager = null;
50
51
  this.diagnostics = new DiagnosticStore();
52
+ this.lifecycleDedup = new LifecycleEventDedup();
53
+ this.ingestUrlPrefixes = [];
51
54
  this.transport = null;
52
55
  this.tracerProvider = null;
53
56
  this.logger = logger;
@@ -99,7 +102,6 @@ class CluebaseBrowserSdkImpl {
99
102
  const nextSignature = JSON.stringify({
100
103
  endpoint: resolved.endpoint,
101
104
  originalEndpoint: resolved.originalEndpoint,
102
- localSetupCheckEndpoint: resolved.localSetupCheckEndpoint,
103
105
  projectKey: resolved.projectKey,
104
106
  producerId: resolved.producerId,
105
107
  autocapture: resolved.autocapture,
@@ -122,6 +124,7 @@ class CluebaseBrowserSdkImpl {
122
124
  this.shutdown();
123
125
  this.internalMetrics.reset();
124
126
  this.diagnostics = new DiagnosticStore();
127
+ this.lifecycleDedup.clear();
125
128
  this.config = resolved;
126
129
  this.initSignature = nextSignature;
127
130
  this.identityManager = new IdentityManager();
@@ -143,6 +146,9 @@ class CluebaseBrowserSdkImpl {
143
146
  schemaVersion: 1,
144
147
  metrics: this.internalMetrics,
145
148
  diagnostics: this.diagnostics,
149
+ onEndpointsResolved: (endpoints) => {
150
+ this.adoptIngestUrlPrefixes(endpoints);
151
+ },
146
152
  });
147
153
  this.transport = new BatchTransport({
148
154
  client: ingestClient,
@@ -154,12 +160,13 @@ class CluebaseBrowserSdkImpl {
154
160
  queueMaxBytes: DEFAULT_MAX_BATCH_QUEUE_BYTES,
155
161
  queueMaxItems: DEFAULT_MAX_BATCH_QUEUE_ITEMS,
156
162
  projectKey: resolved.projectKey,
163
+ endpoint: resolved.endpoint,
164
+ sdkVersion: SDK_VERSION,
165
+ sourceSchemaVersion: "1",
166
+ sdkRequest: "browser",
157
167
  initialConsent: this.consent,
158
168
  metrics: this.internalMetrics,
159
169
  diagnostics: this.diagnostics,
160
- onGovernorFeedback: (feedback) => {
161
- this.applyUsageGovernorFeedback(feedback);
162
- },
163
170
  });
164
171
  this.consent = this.transport.currentConsent();
165
172
  this.transport.start();
@@ -186,6 +193,7 @@ class CluebaseBrowserSdkImpl {
186
193
  resolved,
187
194
  getTransport: () => this.transport,
188
195
  getContext: () => this.buildContext(),
196
+ ignoreUrlPrefixes: this.ingestUrlPrefixes,
189
197
  });
190
198
  this.initialized = true;
191
199
  this.refreshSessionCaptureEligibility();
@@ -248,6 +256,16 @@ class CluebaseBrowserSdkImpl {
248
256
  const sanitizedTraits = this.sanitizeIdentifyTraits(traits);
249
257
  const subjectProfileKeys = safeProfileKeys(traits, this.config?.privacy.deniedKeys ?? []);
250
258
  this.identityManager.identify(sanitizedUserId, sanitizedTraits);
259
+ if (!this.lifecycleDedup.shouldEmit({
260
+ action: "identify",
261
+ userId: sanitizedUserId,
262
+ anonymousId: previousAnonymousId,
263
+ organizationId: this.organizationManager?.getContext().organizationId ?? null,
264
+ subjectTraits: this.identityManager.getContext().traits,
265
+ profileKeys: subjectProfileKeys,
266
+ })) {
267
+ return;
268
+ }
251
269
  this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.identityIdentified, {
252
270
  identity_action: "identify",
253
271
  profile_keys: subjectProfileKeys,
@@ -279,9 +297,20 @@ class CluebaseBrowserSdkImpl {
279
297
  return;
280
298
  }
281
299
  const sanitizedOrgId = this.sanitizeIdentityValue("organizationId", normalizedOrganizationId);
282
- const previousUserId = this.identityManager?.getContext().userId ?? null;
300
+ const identityBeforeGroup = this.identityManager?.getContext();
301
+ const previousUserId = identityBeforeGroup?.userId ?? null;
283
302
  const organizationProfileKeys = safeProfileKeys(traits, this.config?.privacy.deniedKeys ?? []);
284
303
  this.organizationManager.setOrganization(sanitizedOrgId, traits);
304
+ if (!this.lifecycleDedup.shouldEmit({
305
+ action: "associate_organization",
306
+ userId: previousUserId,
307
+ anonymousId: identityBeforeGroup?.anonymousId ?? null,
308
+ organizationId: sanitizedOrgId,
309
+ organizationTraits: this.organizationManager.getContext().traits,
310
+ profileKeys: organizationProfileKeys,
311
+ })) {
312
+ return;
313
+ }
285
314
  this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.organizationAssociated, {
286
315
  organization_action: "associate",
287
316
  profile_keys: organizationProfileKeys,
@@ -298,6 +327,22 @@ class CluebaseBrowserSdkImpl {
298
327
  });
299
328
  }
300
329
  }
330
+ link(input) {
331
+ if (!this.ensureInitializedForPublicCall("link")) {
332
+ return;
333
+ }
334
+ const subject = input?.subject;
335
+ if (subject?.type !== "organization") {
336
+ this.warnPublicDrop("link", "subject.type must be 'organization'");
337
+ return;
338
+ }
339
+ const organizationId = normalizePublicIdentityId(subject.id);
340
+ if (!organizationId) {
341
+ this.warnPublicDrop("link", "subject.id must be a non-empty string");
342
+ return;
343
+ }
344
+ this.emitExternalLink("link", organizationId, input?.external?.namespace, input?.external?.id);
345
+ }
301
346
  linkExternalAccount(namespace, externalId) {
302
347
  if (!this.ensureInitializedForPublicCall("linkExternalAccount")) {
303
348
  return;
@@ -306,21 +351,32 @@ class CluebaseBrowserSdkImpl {
306
351
  this.warnPublicDrop("linkExternalAccount", "organization manager is unavailable");
307
352
  return;
308
353
  }
354
+ const organizationId = this.organizationManager.getContext().organizationId;
355
+ if (!organizationId) {
356
+ this.warnPublicDrop("linkExternalAccount", "setOrganization must be called before linkExternalAccount");
357
+ return;
358
+ }
359
+ this.emitExternalLink("linkExternalAccount", organizationId, namespace, externalId);
360
+ }
361
+ emitExternalLink(publicCallName, organizationId, namespace, externalId) {
309
362
  try {
310
- const organizationId = this.organizationManager.getContext().organizationId;
311
- if (!organizationId) {
312
- this.warnPublicDrop("linkExternalAccount", "setOrganization must be called before linkExternalAccount");
313
- return;
314
- }
315
363
  const normalizedNamespace = normalizePublicIdentityId(namespace);
316
364
  const normalizedExternalId = normalizePublicIdentityId(externalId);
317
365
  if (!normalizedNamespace || !normalizedExternalId) {
318
- this.warnPublicDrop("linkExternalAccount", "namespace and externalId must be non-empty strings");
366
+ this.warnPublicDrop(publicCallName, "namespace and externalId must be non-empty strings");
319
367
  return;
320
368
  }
321
369
  const sanitizedExternalId = this.sanitizeIdentityValue("externalId", normalizedExternalId);
370
+ if (!this.lifecycleDedup.shouldEmit({
371
+ action: "link",
372
+ organizationId,
373
+ externalNamespace: normalizedNamespace,
374
+ externalId: sanitizedExternalId,
375
+ })) {
376
+ return;
377
+ }
322
378
  this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.externalAccountLinked, {
323
- identity_action: "link_external_account",
379
+ identity_action: "link",
324
380
  external_namespace: normalizedNamespace,
325
381
  }, false, {
326
382
  link_type: "canonical_subject_to_external",
@@ -331,36 +387,12 @@ class CluebaseBrowserSdkImpl {
331
387
  this.flushLifecycleEvents();
332
388
  }
333
389
  catch {
334
- this.logger.error("linkExternalAccount failed", {
390
+ this.logger.error(`${publicCallName} failed`, {
335
391
  error_code: "sdk_operation_failed",
336
392
  });
337
393
  }
338
394
  }
339
395
  reset() {
340
- if (!this.initialized) {
341
- return;
342
- }
343
- try {
344
- const previousUserId = this.identityManager?.getContext().userId ?? null;
345
- const previousOrganizationId = this.organizationManager?.getContext().organizationId ?? null;
346
- this.enqueueOfficialCustomEvent(OFFICIAL_CUSTOM_EVENT_NAME.identityReset, {
347
- identity_action: "reset",
348
- }, false, {
349
- link_type: "logout",
350
- user_id: previousUserId,
351
- organization_id: previousOrganizationId,
352
- });
353
- }
354
- catch {
355
- this.logger.error("reset failed", {
356
- error_code: "sdk_operation_failed",
357
- });
358
- }
359
- finally {
360
- this.resetSessionState();
361
- }
362
- }
363
- logout() {
364
396
  if (!this.initialized) {
365
397
  return;
366
398
  }
@@ -376,7 +408,7 @@ class CluebaseBrowserSdkImpl {
376
408
  });
377
409
  }
378
410
  catch {
379
- this.logger.error("logout failed", {
411
+ this.logger.error("reset failed", {
380
412
  error_code: "sdk_operation_failed",
381
413
  });
382
414
  }
@@ -441,6 +473,9 @@ class CluebaseBrowserSdkImpl {
441
473
  });
442
474
  }
443
475
  setConsent(consent) {
476
+ if (this.consent !== consent) {
477
+ this.lifecycleDedup.clear();
478
+ }
444
479
  this.consent = consent;
445
480
  if (!this.initialized) {
446
481
  return;
@@ -538,21 +573,15 @@ class CluebaseBrowserSdkImpl {
538
573
  void this.transport?.flush();
539
574
  }
540
575
  async flush() {
541
- const deadline = this.transport?.beginFlushDeadline();
542
- try {
543
- await awaitUntilDeadline(this.flushCaptureSummaries("manual_flush"), deadline?.signal);
544
- await forceFlushCluebaseOtelProvider(this.tracerProvider, deadline?.signal);
545
- await this.transport?.flush(deadline);
546
- }
547
- finally {
548
- this.transport?.endFlushDeadline(deadline);
549
- }
576
+ await this.flushCaptureSummaries("manual_flush");
577
+ await forceFlushCluebaseOtelProvider(this.tracerProvider);
578
+ await this.transport?.flush();
550
579
  }
551
580
  async close() {
552
- const closeSignal = this.transport?.beginClose();
553
- await awaitUntilDeadline(this.flushCaptureSummaries("close"), closeSignal);
581
+ this.transport?.beginClose();
582
+ await this.flushCaptureSummaries("close");
554
583
  this.stopCaptureModules();
555
- await forceFlushCluebaseOtelProvider(this.tracerProvider, closeSignal ?? undefined);
584
+ await forceFlushCluebaseOtelProvider(this.tracerProvider);
556
585
  await this.transport?.close();
557
586
  if (this.transport?.getDiagnostics().lifecycle === "closing_failed") {
558
587
  await this.shutdownAsync();
@@ -563,11 +592,19 @@ class CluebaseBrowserSdkImpl {
563
592
  warmupBrowserToken(client) {
564
593
  this.runtimeSurfaces.warmupBrowserToken(client);
565
594
  }
595
+ adoptIngestUrlPrefixes(endpoints) {
596
+ for (const url of [endpoints.ingestBatchUrl, endpoints.browserTokenUrl]) {
597
+ if (url.length > 0 && !this.ingestUrlPrefixes.includes(url)) {
598
+ this.ingestUrlPrefixes.push(url);
599
+ }
600
+ }
601
+ this.runtimeSurfaces.refreshIgnoreUrlPrefixes();
602
+ }
566
603
  startCaptureModules() {
567
604
  if (!this.config) {
568
605
  return;
569
606
  }
570
- this.runtimeSurfaces.startCaptureModules(this.config);
607
+ this.runtimeSurfaces.startCaptureModules(this.config, this.ingestUrlPrefixes);
571
608
  }
572
609
  stopCaptureModules() {
573
610
  this.runtimeSurfaces.stopCaptureModules();
@@ -575,9 +612,6 @@ class CluebaseBrowserSdkImpl {
575
612
  async flushCaptureSummaries(reason) {
576
613
  await this.runtimeSurfaces.flushCaptureSummaries(reason);
577
614
  }
578
- applyUsageGovernorFeedback(feedback) {
579
- this.eventPipeline.applyUsageGovernorFeedback(feedback);
580
- }
581
615
  buildContext() {
582
616
  return this.eventPipeline.buildContext();
583
617
  }