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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +21 -0
  2. package/dist/_generated/{identity-pii-policy.d.ts → identity-pii-policy-text-contains.d.ts} +1 -1
  3. package/dist/_generated/{identity-pii-policy.js → identity-pii-policy-text-contains.js} +1 -1
  4. package/dist/_generated/identity-pii-policy-whole-value.d.ts +37 -0
  5. package/dist/_generated/identity-pii-policy-whole-value.js +106 -0
  6. package/dist/_generated/otel-span-conversion.js +1 -9
  7. package/dist/context/action-context.js +27 -7
  8. package/dist/context/browser-storage.js +1 -2
  9. package/dist/context/environment.js +5 -3
  10. package/dist/context/identity.js +5 -9
  11. package/dist/context/organization.d.ts +1 -1
  12. package/dist/context/organization.js +6 -8
  13. package/dist/context/session.js +6 -8
  14. package/dist/context/tab.js +8 -12
  15. package/dist/context/trace.js +2 -1
  16. package/dist/exporter/batch.d.ts +3 -1
  17. package/dist/exporter/batch.js +6 -10
  18. package/dist/exporter/browser-outbox-records.d.ts +1 -1
  19. package/dist/exporter/browser-outbox-records.js +15 -6
  20. package/dist/exporter/browser-outbox.d.ts +5 -2
  21. package/dist/exporter/browser-outbox.js +48 -11
  22. package/dist/exporter/client.d.ts +2 -2
  23. package/dist/exporter/client.js +15 -30
  24. package/dist/exporter/delivery-response-contract.js +67 -17
  25. package/dist/exporter/delivery-response.d.ts +3 -1
  26. package/dist/exporter/delivery-response.js +31 -20
  27. package/dist/exporter/diagnostics.d.ts +1 -1
  28. package/dist/exporter/diagnostics.js +4 -1
  29. package/dist/exporter/event-id.js +1 -3
  30. package/dist/exporter/frontend-resource.js +5 -4
  31. package/dist/exporter/observation-source-event-exporter.d.ts +1 -0
  32. package/dist/exporter/observation-source-event-exporter.js +5 -2
  33. package/dist/exporter/path-template.js +3 -1
  34. package/dist/exporter/raw-event-to-source-signal.js +4 -2
  35. package/dist/exporter/sanitize.d.ts +3 -2
  36. package/dist/exporter/sanitize.js +4 -3
  37. package/dist/exporter/sdk-core-wasm.d.ts +5 -0
  38. package/dist/exporter/sdk-core-wasm.js +18 -0
  39. package/dist/exporter/source-event.d.ts +1 -0
  40. package/dist/exporter/span-to-source-signal.d.ts +1 -1
  41. package/dist/exporter/span-to-source-signal.js +122 -82
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.js +23 -11
  44. package/dist/instrumentation/action.js +9 -8
  45. package/dist/instrumentation/dom.js +13 -21
  46. package/dist/instrumentation/drag.js +0 -3
  47. package/dist/instrumentation/error.js +3 -1
  48. package/dist/instrumentation/form.js +5 -6
  49. package/dist/instrumentation/input.js +4 -8
  50. package/dist/instrumentation/location.js +3 -1
  51. package/dist/instrumentation/network-request.js +1 -1
  52. package/dist/instrumentation/network-span-attributes.js +2 -2
  53. package/dist/instrumentation/network.js +7 -3
  54. package/dist/instrumentation/performance.js +1 -2
  55. package/dist/internal/internal-log.js +2 -1
  56. package/dist/provider/_version.d.ts +1 -1
  57. package/dist/provider/_version.js +1 -1
  58. package/dist/provider/config.js +0 -1
  59. package/dist/provider/constants.d.ts +1 -1
  60. package/dist/provider/constants.js +0 -1
  61. package/dist/provider/sdk-error.d.ts +7 -0
  62. package/dist/provider/sdk-error.js +48 -0
  63. package/dist/provider/sdk-event-pipeline.d.ts +2 -1
  64. package/dist/provider/sdk-event-pipeline.js +6 -3
  65. package/dist/provider/sdk-input-normalization.js +2 -2
  66. package/dist/provider/sdk-otel-pipeline.js +5 -4
  67. package/dist/provider/sdk-runtime-surfaces.d.ts +15 -12
  68. package/dist/provider/sdk-runtime-surfaces.js +8 -7
  69. package/dist/provider/sdk.js +81 -12
  70. package/dist/provider/types.d.ts +6 -5
  71. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +41 -0
  72. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +267 -0
  73. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
  74. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +12 -0
  75. package/dist/wasm/pkg-web/package.json +15 -0
  76. package/package.json +10 -3
package/README.md CHANGED
@@ -31,6 +31,27 @@ environment.
31
31
  appends `/api/v1/ingest/browser-tokens` and the canonical browser observation
32
32
  batch path internally.
33
33
 
34
+ ## Content Security Policy requirement
35
+
36
+ The SDK converts each OpenTelemetry span through a WebAssembly module, so a
37
+ host page that sends a `Content-Security-Policy` header must allow it:
38
+
39
+ - `script-src` must include `'wasm-unsafe-eval'`. Without it the browser blocks
40
+ `WebAssembly.instantiate` and the module never initializes. `'unsafe-eval'`
41
+ also works but is a far wider grant, so prefer `'wasm-unsafe-eval'`.
42
+ - `connect-src` must allow the origin that serves the bundled
43
+ `cluebase_sdk_core_wasm_bg.wasm` asset (usually `'self'`), because the module
44
+ is fetched at runtime relative to the SDK bundle.
45
+ - `connect-src` must also allow the Cluebase API origin (`endpoint`, by default
46
+ `https://api.cluebase.io`).
47
+
48
+ Bundlers must emit the `.wasm` asset. Webpack 5, Vite, and Next.js do this
49
+ automatically for the `new URL(..., import.meta.url)` form the SDK uses.
50
+
51
+ If the module fails to initialize, every span export is dropped and counted as
52
+ a dropped span; the SDK does not fall back to a JavaScript implementation.
53
+ Verify the policy in a staging environment before launch.
54
+
34
55
  ## Responsibility boundary
35
56
 
36
57
  - SDK captures browser signals and applies local minimization where the signal is obviously high-risk.
@@ -35,4 +35,4 @@ export declare const passesLuhn: (digits: string) => boolean;
35
35
  * 保管されます。値**そのもの**が識別子かを問いたい面は、もう一方の面
36
36
  * (whole_value_is_pii)を使ってください。
37
37
  */
38
- export declare const detectIdentityPII: (value: string) => string | null;
38
+ export declare const detectTextContainsPII: (value: string) => string | null;
@@ -80,7 +80,7 @@ const DIGIT_RUN_MATCHERS = {
80
80
  * 保管されます。値**そのもの**が識別子かを問いたい面は、もう一方の面
81
81
  * (whole_value_is_pii)を使ってください。
82
82
  */
83
- export const detectIdentityPII = (value) => {
83
+ export const detectTextContainsPII = (value) => {
84
84
  if (typeof value !== "string" || value.length === 0)
85
85
  return null;
86
86
  const trimmed = value.trim();
@@ -0,0 +1,37 @@
1
+ /** 見る順序。先に当たった種類を返します。 */
2
+ export declare const PII_CATEGORY_ORDER: readonly ["email", "ssn", "phone", "credit_card"];
3
+ /** 電子メールの形。 */
4
+ export declare const PII_EMAIL_PATTERN = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
5
+ /** 社会保障番号の形。 */
6
+ export declare const PII_SSN_PATTERN = "^\\d{3}-\\d{2}-\\d{4}$";
7
+ /** 電話とみなす桁数の範囲と、求める区切り。 */
8
+ export declare const PII_PHONE_MIN_DIGITS = 7;
9
+ export declare const PII_PHONE_MAX_DIGITS = 15;
10
+ export declare const PII_PHONE_SEPARATOR_PATTERN = "[+().\\-\\s]";
11
+ /** 支払い用の番号とみなす桁数の範囲。Luhn の検算を必ず通します。 */
12
+ export declare const PII_CREDIT_CARD_MIN_DIGITS = 13;
13
+ export declare const PII_CREDIT_CARD_MAX_DIGITS = 19;
14
+ /** 電話の見た目として許す文字だけでできているか。 */
15
+ export declare const PII_PHONE_SHAPE_PATTERN = "^[+()0-9.\\-\\s]+$";
16
+ /** 支払い用の番号の見た目として許す文字だけでできているか。 */
17
+ export declare const PII_CREDIT_CARD_SHAPE_PATTERN = "^[0-9\\s\\-]+$";
18
+ /** 伏せた値の形。同じ入力なら配る部品 3 つとも同じ値になります。 */
19
+ export declare const PII_HASH_PREFIX = "cluebase_pii_hashed:";
20
+ export declare const PII_HASH_HEX_LENGTH = 32;
21
+ /** どの面の規則か(読み手が取り違えないため)。 */
22
+ export declare const PII_SURFACE: "whole_value_is_pii";
23
+ /**
24
+ * Luhn の検算。支払い用の番号らしさを、無作為な数字の並びと分けます。
25
+ *
26
+ * **手順もここから出します。** 桁数と綴りだけを正本化して手順を手書きで残すと、
27
+ * 同じ算法が 4 か所(受付側と配る部品 3 つ)に残ります。実際に残っていました。
28
+ */
29
+ export declare const passesLuhn: (digits: string) => boolean;
30
+ /**
31
+ * この値**そのもの**が個人情報か。両端まで含めて照合します。
32
+ *
33
+ * 途中に埋もれた形は拾いません(拾うと利用者の不透明な識別子を巻き込み、
34
+ * 紐付けが黙って壊れます)。文字列が個人情報を**含む**かを問いたい面は、
35
+ * もう一方の面(text_contains_pii)を使ってください。
36
+ */
37
+ export declare const detectWholeValuePII: (value: string) => string | null;
@@ -0,0 +1,106 @@
1
+ // 自動生成です。**手で編集しないでください。**
2
+ //
3
+ // 正本: packages/cluebase-shared/src/ingest/identity-pii-policy.v1.json
4
+ // 出し直す: node scripts/generate-shared-contract-tables.mjs
5
+ //
6
+ // 正本を直したらこの file も一緒に commit してください。CI が
7
+ // `node scripts/generate-shared-contract-tables.mjs --check` で照合し、ずれていれば落とします。
8
+ /** 見る順序。先に当たった種類を返します。 */
9
+ export const PII_CATEGORY_ORDER = [
10
+ "email",
11
+ "ssn",
12
+ "phone",
13
+ "credit_card",
14
+ ];
15
+ /** 電子メールの形。 */
16
+ export const PII_EMAIL_PATTERN = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
17
+ /** 社会保障番号の形。 */
18
+ export const PII_SSN_PATTERN = "^\\d{3}-\\d{2}-\\d{4}$";
19
+ /** 電話とみなす桁数の範囲と、求める区切り。 */
20
+ export const PII_PHONE_MIN_DIGITS = 7;
21
+ export const PII_PHONE_MAX_DIGITS = 15;
22
+ export const PII_PHONE_SEPARATOR_PATTERN = "[+().\\-\\s]";
23
+ /** 支払い用の番号とみなす桁数の範囲。Luhn の検算を必ず通します。 */
24
+ export const PII_CREDIT_CARD_MIN_DIGITS = 13;
25
+ export const PII_CREDIT_CARD_MAX_DIGITS = 19;
26
+ /** 電話の見た目として許す文字だけでできているか。 */
27
+ export const PII_PHONE_SHAPE_PATTERN = "^[+()0-9.\\-\\s]+$";
28
+ /** 支払い用の番号の見た目として許す文字だけでできているか。 */
29
+ export const PII_CREDIT_CARD_SHAPE_PATTERN = "^[0-9\\s\\-]+$";
30
+ /** 伏せた値の形。同じ入力なら配る部品 3 つとも同じ値になります。 */
31
+ export const PII_HASH_PREFIX = "cluebase_pii_hashed:";
32
+ export const PII_HASH_HEX_LENGTH = 32;
33
+ /** どの面の規則か(読み手が取り違えないため)。 */
34
+ export const PII_SURFACE = "whole_value_is_pii";
35
+ /**
36
+ * Luhn の検算。支払い用の番号らしさを、無作為な数字の並びと分けます。
37
+ *
38
+ * **手順もここから出します。** 桁数と綴りだけを正本化して手順を手書きで残すと、
39
+ * 同じ算法が 4 か所(受付側と配る部品 3 つ)に残ります。実際に残っていました。
40
+ */
41
+ export const passesLuhn = (digits) => {
42
+ let sum = 0;
43
+ let doubleDigit = false;
44
+ for (let index = digits.length - 1; index >= 0; index -= 1) {
45
+ let digit = Number(digits[index]);
46
+ if (doubleDigit) {
47
+ digit *= 2;
48
+ if (digit > 9)
49
+ digit -= 9;
50
+ }
51
+ sum += digit;
52
+ doubleDigit = !doubleDigit;
53
+ }
54
+ return sum % 10 === 0;
55
+ };
56
+ const EMAIL_RE = new RegExp(PII_EMAIL_PATTERN);
57
+ const SSN_RE = new RegExp(PII_SSN_PATTERN);
58
+ const PHONE_SHAPE_RE = new RegExp(PII_PHONE_SHAPE_PATTERN);
59
+ const PHONE_SEPARATOR_RE = new RegExp(PII_PHONE_SEPARATOR_PATTERN);
60
+ const CREDIT_CARD_SHAPE_RE = new RegExp(PII_CREDIT_CARD_SHAPE_PATTERN);
61
+ const digitsOf = (value) => value.replace(/[^0-9]/g, "");
62
+ const MATCHERS = {
63
+ email: (value) => EMAIL_RE.test(value),
64
+ ssn: (value) => SSN_RE.test(value),
65
+ phone: (value) => {
66
+ const digits = digitsOf(value);
67
+ if (digits.length < PII_PHONE_MIN_DIGITS)
68
+ return false;
69
+ if (digits.length > PII_PHONE_MAX_DIGITS)
70
+ return false;
71
+ // 電話の見た目でない文字(英字など)が混ざっていたら電話ではありません。
72
+ if (!PHONE_SHAPE_RE.test(value))
73
+ return false;
74
+ // 区切りを 1 つ以上求めます。求めないと "10000000" のような整数の主キーを
75
+ // 電話とみなし、利用者の識別子が黙って伏せられて後段の突合が壊れます。
76
+ return PHONE_SEPARATOR_RE.test(value);
77
+ },
78
+ credit_card: (value) => {
79
+ const digits = digitsOf(value);
80
+ if (digits.length < PII_CREDIT_CARD_MIN_DIGITS)
81
+ return false;
82
+ if (digits.length > PII_CREDIT_CARD_MAX_DIGITS)
83
+ return false;
84
+ if (!CREDIT_CARD_SHAPE_RE.test(value))
85
+ return false;
86
+ return passesLuhn(digits);
87
+ },
88
+ };
89
+ /**
90
+ * この値**そのもの**が個人情報か。両端まで含めて照合します。
91
+ *
92
+ * 途中に埋もれた形は拾いません(拾うと利用者の不透明な識別子を巻き込み、
93
+ * 紐付けが黙って壊れます)。文字列が個人情報を**含む**かを問いたい面は、
94
+ * もう一方の面(text_contains_pii)を使ってください。
95
+ */
96
+ export const detectWholeValuePII = (value) => {
97
+ if (typeof value !== "string" || value.length === 0)
98
+ return null;
99
+ const trimmed = value.trim();
100
+ // 見る順序は正本が決めます。ここには書きません。
101
+ for (const category of PII_CATEGORY_ORDER) {
102
+ if (MATCHERS[category]?.(trimmed))
103
+ return category;
104
+ }
105
+ return null;
106
+ };
@@ -1,10 +1,3 @@
1
- // 自動生成です。**手で編集しないでください。**
2
- //
3
- // 正本: packages/cluebase-backend-contract/src/delivery/otel-span-conversion.source.ts
4
- // 出し直す: node scripts/generate-shared-contract-tables.mjs
5
- //
6
- // 正本を直したらこの file も一緒に commit してください。CI が
7
- // `node scripts/generate-shared-contract-tables.mjs --check` で照合し、ずれていれば落とします。
8
1
  export function hrTimeToUnixNano(time) {
9
2
  if (!time)
10
3
  return `${Date.now()}000000`;
@@ -20,8 +13,7 @@ export function nanosToIso(nanos) {
20
13
  if (Number.isFinite(ms) && ms > 0)
21
14
  return new Date(ms).toISOString();
22
15
  }
23
- catch {
24
- }
16
+ catch { }
25
17
  return new Date().toISOString();
26
18
  }
27
19
  export function attrString(attributes, key) {
@@ -10,6 +10,17 @@ const resolveRestoredFunction = (candidate) => {
10
10
  };
11
11
  const captureFlag = (options) => typeof options === "boolean" ? options : options?.capture === true;
12
12
  const isCallable = (value) => typeof value === "function";
13
+ const isWeakMapKey = (value) => (value !== null && typeof value === "object") || typeof value === "function";
14
+ const resolveEventTarget = (receiver) => {
15
+ if (isWeakMapKey(receiver))
16
+ return receiver;
17
+ if (receiver === undefined &&
18
+ typeof EventTarget !== "undefined" &&
19
+ globalThis instanceof EventTarget) {
20
+ return globalThis;
21
+ }
22
+ return null;
23
+ };
13
24
  const invokeListener = (listener, thisValue, event) => {
14
25
  if (typeof listener === "function") {
15
26
  return listener.call(thisValue, event);
@@ -185,11 +196,16 @@ export class PrivateActionContextManager {
185
196
  originalAdd.call(this, type, listener, options);
186
197
  return;
187
198
  }
188
- if (!listener) {
199
+ const target = resolveEventTarget(this);
200
+ if (!target) {
189
201
  originalAdd.call(this, type, listener, options);
190
202
  return;
191
203
  }
192
- const registrations = manager.eventRegistrations.get(this) ?? [];
204
+ if (!listener) {
205
+ originalAdd.call(target, type, listener, options);
206
+ return;
207
+ }
208
+ const registrations = manager.eventRegistrations.get(target) ?? [];
193
209
  if (registrations.some((registration) => registration.type === type &&
194
210
  registration.listener === listener &&
195
211
  captureFlag(registration.options) === captureFlag(options))) {
@@ -198,11 +214,10 @@ export class PrivateActionContextManager {
198
214
  if (typeof options === "object" &&
199
215
  options !== null &&
200
216
  options.signal?.aborted) {
201
- originalAdd.call(this, type, listener, options);
217
+ originalAdd.call(target, type, listener, options);
202
218
  return;
203
219
  }
204
220
  const registrationSnapshot = manager.capture();
205
- const target = this;
206
221
  const wrappedListener = function (event) {
207
222
  const eventSnapshot = manager.eventContexts.get(event);
208
223
  const snapshot = eventSnapshot ?? registrationSnapshot;
@@ -251,16 +266,21 @@ export class PrivateActionContextManager {
251
266
  originalRemove.call(this, type, listener, options);
252
267
  return;
253
268
  }
254
- if (!listener) {
269
+ const target = resolveEventTarget(this);
270
+ if (!target) {
255
271
  originalRemove.call(this, type, listener, options);
256
272
  return;
257
273
  }
258
- const registrations = manager.eventRegistrations.get(this) ?? [];
274
+ if (!listener) {
275
+ originalRemove.call(target, type, listener, options);
276
+ return;
277
+ }
278
+ const registrations = manager.eventRegistrations.get(target) ?? [];
259
279
  const index = registrations.findIndex((registration) => registration.type === type &&
260
280
  registration.listener === listener &&
261
281
  captureFlag(registration.options) === captureFlag(options));
262
282
  if (index < 0) {
263
- originalRemove.call(this, type, listener, options);
283
+ originalRemove.call(target, type, listener, options);
264
284
  return;
265
285
  }
266
286
  const registration = registrations[index];
@@ -19,7 +19,6 @@ export const resolveWebStorage = (kind, probeKey) => {
19
19
  return storage;
20
20
  }
21
21
  }
22
- catch {
23
- }
22
+ catch { }
24
23
  return createMemoryStorage();
25
24
  };
@@ -1,5 +1,5 @@
1
1
  import { toPathTemplate } from "../exporter/path-template.js";
2
- import { detectIdentityPII } from "../exporter/sanitize.js";
2
+ import { detectTextContainsPII } from "../exporter/sanitize.js";
3
3
  import { stripQueryAndHashFromUrl } from "../provider/sdk-input-normalization.js";
4
4
  const firstMatch = (userAgent, pattern) => {
5
5
  const match = pattern.exec(userAgent);
@@ -136,7 +136,7 @@ const sanitizeReferrer = (value) => {
136
136
  .split("/")
137
137
  .map((segment) => {
138
138
  try {
139
- return detectIdentityPII(decodeURIComponent(segment)) === null
139
+ return detectTextContainsPII(decodeURIComponent(segment)) === null
140
140
  ? segment
141
141
  : ":id";
142
142
  }
@@ -218,7 +218,9 @@ export class EnvironmentManager {
218
218
  safeContextTraits() {
219
219
  try {
220
220
  const traits = this.options.contextTraitsProvider?.() ?? {};
221
- return typeof traits === "object" && traits !== null && !Array.isArray(traits)
221
+ return typeof traits === "object" &&
222
+ traits !== null &&
223
+ !Array.isArray(traits)
222
224
  ? traits
223
225
  : {};
224
226
  }
@@ -1,6 +1,6 @@
1
1
  import { generateAnonymousId } from "../exporter/event-id.js";
2
2
  import { STORAGE_KEYS } from "../provider/constants.js";
3
- import { resolveWebStorage, } from "./browser-storage.js";
3
+ import { resolveWebStorage } from "./browser-storage.js";
4
4
  const CONTEXT_ID_TRAIT_KEYS = new Set([
5
5
  "accountId",
6
6
  "account_id",
@@ -89,8 +89,7 @@ export class IdentityManager {
89
89
  try {
90
90
  this.storage.setItem(STORAGE_KEYS.userId, userId);
91
91
  }
92
- catch {
93
- }
92
+ catch { }
94
93
  }
95
94
  clearUser() {
96
95
  this.state.userId = null;
@@ -98,8 +97,7 @@ export class IdentityManager {
98
97
  try {
99
98
  this.storage.removeItem(STORAGE_KEYS.userId);
100
99
  }
101
- catch {
102
- }
100
+ catch { }
103
101
  }
104
102
  rotateAnonymousId() {
105
103
  const anonymousId = generateAnonymousId();
@@ -107,8 +105,7 @@ export class IdentityManager {
107
105
  try {
108
106
  this.storage.setItem(STORAGE_KEYS.anonymousId, anonymousId);
109
107
  }
110
- catch {
111
- }
108
+ catch { }
112
109
  return anonymousId;
113
110
  }
114
111
  reset() {
@@ -122,8 +119,7 @@ export class IdentityManager {
122
119
  try {
123
120
  this.storage.setItem(STORAGE_KEYS.anonymousId, anonymousId);
124
121
  }
125
- catch {
126
- }
122
+ catch { }
127
123
  }
128
124
  const userId = this.storage.getItem(STORAGE_KEYS.userId);
129
125
  return {
@@ -1,6 +1,6 @@
1
1
  import type { OrganizationContext } from "../provider/types.js";
2
- import { type ContextTraitInput } from "./identity.js";
3
2
  import { type StorageLike } from "./browser-storage.js";
3
+ import { type ContextTraitInput } from "./identity.js";
4
4
  export declare class OrganizationManager {
5
5
  private readonly storage;
6
6
  private state;
@@ -1,9 +1,10 @@
1
1
  import { STORAGE_KEYS } from "../provider/constants.js";
2
+ import { resolveWebStorage } from "./browser-storage.js";
2
3
  import { sanitizeContextTraits, } from "./identity.js";
3
- import { resolveWebStorage, } from "./browser-storage.js";
4
4
  export class OrganizationManager {
5
5
  constructor(storage) {
6
- this.storage = storage ?? resolveWebStorage("local", "cluebase:organization_probe");
6
+ this.storage =
7
+ storage ?? resolveWebStorage("local", "cluebase:organization_probe");
7
8
  this.state = this.loadState();
8
9
  }
9
10
  getContext() {
@@ -22,8 +23,7 @@ export class OrganizationManager {
22
23
  try {
23
24
  this.storage.setItem(STORAGE_KEYS.organizationId, organizationId);
24
25
  }
25
- catch {
26
- }
26
+ catch { }
27
27
  }
28
28
  reset() {
29
29
  this.state = {
@@ -33,8 +33,7 @@ export class OrganizationManager {
33
33
  try {
34
34
  this.storage.removeItem(STORAGE_KEYS.organizationId);
35
35
  }
36
- catch {
37
- }
36
+ catch { }
38
37
  }
39
38
  loadState() {
40
39
  const organizationId = this.storage.getItem(STORAGE_KEYS.organizationId);
@@ -43,8 +42,7 @@ export class OrganizationManager {
43
42
  this.storage.setItem(STORAGE_KEYS.organizationId, organizationId);
44
43
  }
45
44
  }
46
- catch {
47
- }
45
+ catch { }
48
46
  return {
49
47
  organizationId,
50
48
  traits: {},
@@ -1,6 +1,6 @@
1
1
  import { generateSessionId } from "../exporter/event-id.js";
2
2
  import { DEFAULT_SESSION_TIMEOUT_MS, STORAGE_KEYS, } from "../provider/constants.js";
3
- import { resolveWebStorage, } from "./browser-storage.js";
3
+ import { resolveWebStorage } from "./browser-storage.js";
4
4
  const parseMs = (value) => {
5
5
  if (!value) {
6
6
  return null;
@@ -14,7 +14,8 @@ const parseMs = (value) => {
14
14
  export class SessionManager {
15
15
  constructor(timeoutMs = DEFAULT_SESSION_TIMEOUT_MS, storage) {
16
16
  this.timeoutMs = timeoutMs;
17
- this.storage = storage ?? resolveWebStorage("session", "cluebase:session_probe");
17
+ this.storage =
18
+ storage ?? resolveWebStorage("session", "cluebase:session_probe");
18
19
  this.context = this.loadState();
19
20
  }
20
21
  getContext() {
@@ -58,8 +59,7 @@ export class SessionManager {
58
59
  try {
59
60
  this.storage.setItem(STORAGE_KEYS.sessionLastActivityAt, String(nowMs));
60
61
  }
61
- catch {
62
- }
62
+ catch { }
63
63
  }
64
64
  endSession(nowMs = Date.now()) {
65
65
  if (!this.context.sessionId || !this.context.startedAtMs) {
@@ -89,8 +89,7 @@ export class SessionManager {
89
89
  this.storage.setItem(STORAGE_KEYS.sessionStartedAt, String(nowMs));
90
90
  this.storage.setItem(STORAGE_KEYS.sessionLastActivityAt, String(nowMs));
91
91
  }
92
- catch {
93
- }
92
+ catch { }
94
93
  return sessionId;
95
94
  }
96
95
  clear() {
@@ -104,8 +103,7 @@ export class SessionManager {
104
103
  this.storage.removeItem(STORAGE_KEYS.sessionStartedAt);
105
104
  this.storage.removeItem(STORAGE_KEYS.sessionLastActivityAt);
106
105
  }
107
- catch {
108
- }
106
+ catch { }
109
107
  }
110
108
  loadState() {
111
109
  const sessionId = this.storage.getItem(STORAGE_KEYS.sessionId);
@@ -1,6 +1,6 @@
1
1
  import { generateEventId, generateTabId } from "../exporter/event-id.js";
2
2
  import { STORAGE_KEYS } from "../provider/constants.js";
3
- import { resolveWebStorage, } from "./browser-storage.js";
3
+ import { resolveWebStorage } from "./browser-storage.js";
4
4
  const TAB_CLAIM_KEY_PREFIX = "cluebase:tab_claim:";
5
5
  const TAB_CLAIM_TTL_MS = 5 * 60 * 1000;
6
6
  const DOCUMENT_CLAIM_ID = `doc_${generateEventId()}`;
@@ -13,8 +13,7 @@ const resolveClaimStorage = () => {
13
13
  return globalThis.localStorage;
14
14
  }
15
15
  }
16
- catch {
17
- }
16
+ catch { }
18
17
  return null;
19
18
  };
20
19
  const parseTabClaim = (value) => {
@@ -31,13 +30,13 @@ const parseTabClaim = (value) => {
31
30
  };
32
31
  }
33
32
  }
34
- catch {
35
- }
33
+ catch { }
36
34
  return null;
37
35
  };
38
36
  export class TabManager {
39
37
  constructor(storage, options) {
40
- this.storage = storage ?? resolveWebStorage("session", "cluebase:tab_probe");
38
+ this.storage =
39
+ storage ?? resolveWebStorage("session", "cluebase:tab_probe");
41
40
  this.claimStorage =
42
41
  options && "claimStorage" in options
43
42
  ? (options.claimStorage ?? null)
@@ -61,8 +60,7 @@ export class TabManager {
61
60
  try {
62
61
  this.storage.setItem(STORAGE_KEYS.tabId, tabId);
63
62
  }
64
- catch {
65
- }
63
+ catch { }
66
64
  return tabId;
67
65
  }
68
66
  isClaimedByAnotherDocument(tabId) {
@@ -85,8 +83,7 @@ export class TabManager {
85
83
  expiresAt: this.now() + TAB_CLAIM_TTL_MS,
86
84
  }));
87
85
  }
88
- catch {
89
- }
86
+ catch { }
90
87
  }
91
88
  installLifecycleCleanup() {
92
89
  if (!this.bindLifecycleEvents || !this.claimStorage) {
@@ -106,7 +103,6 @@ export class TabManager {
106
103
  globalThis.addEventListener("pagehide", cleanup, { once: true });
107
104
  globalThis.addEventListener("beforeunload", cleanup, { once: true });
108
105
  }
109
- catch {
110
- }
106
+ catch { }
111
107
  }
112
108
  }
@@ -66,7 +66,8 @@ export class TraceManager {
66
66
  endFrontendAction(interactionId) {
67
67
  if (!this.activeInteraction)
68
68
  return;
69
- if (interactionId && this.activeInteraction.interactionId !== interactionId) {
69
+ if (interactionId &&
70
+ this.activeInteraction.interactionId !== interactionId) {
70
71
  return;
71
72
  }
72
73
  this.recentInteraction = {
@@ -1,8 +1,8 @@
1
1
  import type { InternalMetricsStore } from "../internal/metrics.js";
2
2
  import type { CluebaseConsentState, IngestUsageGovernorFeedback } from "../provider/types.js";
3
3
  import { type CollectionPermit } from "./browser-outbox.js";
4
- import { DiagnosticStore, type DiagnosticsSnapshot } from "./diagnostics.js";
5
4
  import type { IngestClient } from "./client.js";
5
+ import { DiagnosticStore, type DiagnosticsSnapshot } from "./diagnostics.js";
6
6
  import type { BrowserSourceEvent } from "./source-event.js";
7
7
  export type BatchIngestClient = Pick<IngestClient, "send" | "hasUsableBrowserToken" | "createBatchEnvelopeMetadata" | "measurePayloadBytes">;
8
8
  export type BatchTransportOptions = {
@@ -10,6 +10,7 @@ export type BatchTransportOptions = {
10
10
  flushIntervalMs: number;
11
11
  maxPayloadBytes: number;
12
12
  flushThresholdBytes?: number;
13
+ maxSize?: number;
13
14
  client: BatchIngestClient;
14
15
  metrics?: InternalMetricsStore;
15
16
  queueMaxBytes?: number;
@@ -30,6 +31,7 @@ export declare class BatchTransport {
30
31
  private readonly flushIntervalMs;
31
32
  private readonly maxPayloadBytes;
32
33
  private readonly flushThresholdBytes;
34
+ private readonly maxSize;
33
35
  private readonly metrics;
34
36
  private readonly diagnostics;
35
37
  private readonly onGovernorFeedback;
@@ -1,4 +1,4 @@
1
- import { CONSENT_DENIED_ABORT_REASON } from "../provider/constants.js";
1
+ import { CONSENT_DENIED_ABORT_REASON, DEFAULT_CONFIG, } from "../provider/constants.js";
2
2
  import { BrowserOutbox, } from "./browser-outbox.js";
3
3
  import { ownEvent } from "./browser-outbox-records.js";
4
4
  import { DiagnosticStore } from "./diagnostics.js";
@@ -21,6 +21,7 @@ export class BatchTransport {
21
21
  this.flushIntervalMs = Math.max(100, options.flushIntervalMs);
22
22
  this.maxPayloadBytes = Math.max(1024, options.maxPayloadBytes);
23
23
  this.flushThresholdBytes = Math.min(this.maxPayloadBytes, Math.max(1024, options.flushThresholdBytes ?? Math.floor(this.maxPayloadBytes * 0.8)));
24
+ this.maxSize = Math.max(1, options.maxSize ?? DEFAULT_CONFIG.batch.maxSize);
24
25
  this.client = options.client;
25
26
  this.metrics = options.metrics ?? null;
26
27
  this.diagnostics = options.diagnostics ?? new DiagnosticStore();
@@ -111,7 +112,8 @@ export class BatchTransport {
111
112
  void this.flush();
112
113
  return;
113
114
  }
114
- if (this.outbox.estimatedDraftBytes((events, metadata) => this.client.measurePayloadBytes([...events], metadata), this.client.createBatchEnvelopeMetadata()) >= this.flushThresholdBytes) {
115
+ if (this.outbox.draftCount() >= this.maxSize ||
116
+ this.outbox.estimatedDraftBytes((events, metadata) => this.client.measurePayloadBytes([...events], metadata), this.client.createBatchEnvelopeMetadata()) >= this.flushThresholdBytes) {
115
117
  void this.flush();
116
118
  }
117
119
  });
@@ -355,14 +357,8 @@ export class BatchTransport {
355
357
  this.recordPendingBatchCount();
356
358
  return "break";
357
359
  }
358
- if (result.partial) {
359
- if (this.lifecycle === "closing")
360
- this.closeHadTerminalDisposition = true;
361
- this.diagnostics.record({
362
- code: "intentional_drop",
363
- count: result.droppedEventCount ?? 0,
364
- batch_fingerprint: this.diagnostics.batchFingerprint(batch.diagnosticCanonicalBytes),
365
- });
360
+ if (result.partial && this.lifecycle === "closing") {
361
+ this.closeHadTerminalDisposition = true;
366
362
  }
367
363
  this.recordPendingBatchCount();
368
364
  return "continue";
@@ -1,5 +1,5 @@
1
- import type { BatchEnvelopeMetadata } from "./client.js";
2
1
  import type { CluebaseConsentState } from "../provider/types.js";
2
+ import type { BatchEnvelopeMetadata } from "./client.js";
3
3
  import { type BrowserSourceEvent } from "./source-event.js";
4
4
  export type StorageLike = Pick<Storage, "getItem" | "setItem" | "removeItem" | "key"> & {
5
5
  readonly length: number;