@genn-inc/cluebase-frontend-sdk 0.0.3 → 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 (73) 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/browser-storage.js +1 -2
  8. package/dist/context/environment.js +5 -3
  9. package/dist/context/identity.js +5 -9
  10. package/dist/context/organization.d.ts +1 -1
  11. package/dist/context/organization.js +6 -8
  12. package/dist/context/session.js +6 -8
  13. package/dist/context/tab.js +8 -12
  14. package/dist/context/trace.js +2 -1
  15. package/dist/exporter/batch.d.ts +3 -1
  16. package/dist/exporter/batch.js +6 -10
  17. package/dist/exporter/browser-outbox-records.d.ts +1 -1
  18. package/dist/exporter/browser-outbox-records.js +15 -6
  19. package/dist/exporter/browser-outbox.d.ts +5 -2
  20. package/dist/exporter/browser-outbox.js +48 -11
  21. package/dist/exporter/client.d.ts +2 -2
  22. package/dist/exporter/client.js +15 -30
  23. package/dist/exporter/delivery-response-contract.js +67 -17
  24. package/dist/exporter/delivery-response.d.ts +3 -1
  25. package/dist/exporter/delivery-response.js +31 -20
  26. package/dist/exporter/diagnostics.d.ts +1 -1
  27. package/dist/exporter/diagnostics.js +4 -1
  28. package/dist/exporter/event-id.js +1 -3
  29. package/dist/exporter/frontend-resource.js +5 -4
  30. package/dist/exporter/observation-source-event-exporter.d.ts +1 -0
  31. package/dist/exporter/observation-source-event-exporter.js +5 -2
  32. package/dist/exporter/path-template.js +3 -1
  33. package/dist/exporter/raw-event-to-source-signal.js +4 -2
  34. package/dist/exporter/sanitize.d.ts +3 -2
  35. package/dist/exporter/sanitize.js +4 -3
  36. package/dist/exporter/sdk-core-wasm.d.ts +5 -0
  37. package/dist/exporter/sdk-core-wasm.js +18 -0
  38. package/dist/exporter/source-event.d.ts +1 -0
  39. package/dist/exporter/span-to-source-signal.d.ts +1 -1
  40. package/dist/exporter/span-to-source-signal.js +122 -82
  41. package/dist/index.d.ts +5 -4
  42. package/dist/index.js +16 -6
  43. package/dist/instrumentation/action.js +9 -8
  44. package/dist/instrumentation/dom.js +13 -21
  45. package/dist/instrumentation/drag.js +0 -3
  46. package/dist/instrumentation/error.js +3 -1
  47. package/dist/instrumentation/form.js +5 -6
  48. package/dist/instrumentation/input.js +4 -8
  49. package/dist/instrumentation/location.js +3 -1
  50. package/dist/instrumentation/network-request.js +1 -1
  51. package/dist/instrumentation/network-span-attributes.js +2 -2
  52. package/dist/instrumentation/network.js +7 -3
  53. package/dist/instrumentation/performance.js +1 -2
  54. package/dist/internal/internal-log.js +2 -1
  55. package/dist/provider/_version.d.ts +1 -1
  56. package/dist/provider/_version.js +1 -1
  57. package/dist/provider/config.js +0 -1
  58. package/dist/provider/constants.d.ts +1 -1
  59. package/dist/provider/constants.js +0 -1
  60. package/dist/provider/sdk-event-pipeline.d.ts +2 -1
  61. package/dist/provider/sdk-event-pipeline.js +6 -3
  62. package/dist/provider/sdk-input-normalization.js +2 -2
  63. package/dist/provider/sdk-otel-pipeline.js +5 -4
  64. package/dist/provider/sdk-runtime-surfaces.d.ts +15 -12
  65. package/dist/provider/sdk-runtime-surfaces.js +8 -7
  66. package/dist/provider/sdk.js +67 -12
  67. package/dist/provider/types.d.ts +6 -5
  68. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +41 -0
  69. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +267 -0
  70. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
  71. package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +12 -0
  72. package/dist/wasm/pkg-web/package.json +15 -0
  73. package/package.json +62 -53
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) {
@@ -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;
@@ -188,7 +188,9 @@ export const withoutChecksum = (record) => {
188
188
  export const parseRecord = (raw, projectHash) => {
189
189
  try {
190
190
  const value = JSON.parse(raw);
191
- if (!isRecord(value) || value.storageVersion !== 2 || value.projectHash !== projectHash)
191
+ if (!isRecord(value) ||
192
+ value.storageVersion !== 2 ||
193
+ value.projectHash !== projectHash)
192
194
  return null;
193
195
  const checksumSha256 = value.checksumSha256;
194
196
  if (typeof checksumSha256 !== "string")
@@ -204,11 +206,14 @@ export const parseRecord = (raw, projectHash) => {
204
206
  };
205
207
  const isValidRecord = (value) => {
206
208
  const base = typeof value.createdAt === "string" &&
207
- (typeof value.recordVerifierSha256 === "string" || value.recordVerifierSha256 === null);
209
+ (typeof value.recordVerifierSha256 === "string" ||
210
+ value.recordVerifierSha256 === null);
208
211
  if (!base)
209
212
  return false;
210
213
  if (value.kind === "consent") {
211
- return ((value.state === "granted" || value.state === "denied" || value.state === "unknown") &&
214
+ return ((value.state === "granted" ||
215
+ value.state === "denied" ||
216
+ value.state === "unknown") &&
212
217
  typeof value.collectionGeneration === "string" &&
213
218
  typeof value.legacyImportBlocked === "boolean" &&
214
219
  typeof value.updatedAt === "string" &&
@@ -225,7 +230,8 @@ const isValidRecord = (value) => {
225
230
  isBrowserSourceSignalEvent(value.event));
226
231
  }
227
232
  if (value.kind === "legacy_tombstone") {
228
- return typeof value.eventKeyHash === "string" && typeof value.importedAt === "string";
233
+ return (typeof value.eventKeyHash === "string" &&
234
+ typeof value.importedAt === "string");
229
235
  }
230
236
  if (value.kind === "pending" || value.kind === "split_plan") {
231
237
  if (typeof value.batchId !== "string" ||
@@ -291,7 +297,8 @@ export const draftRecordKey = (prefix, eventKeyHash) => `${prefix}draft:${eventK
291
297
  export const pendingRecordKey = (prefix, batchId) => `${prefix}pending:${keyHash(batchId)}`;
292
298
  export const tombstoneRecordKey = (prefix, eventKeyHash) => `${prefix}legacy-tombstone:${eventKeyHash}`;
293
299
  export const browserEventsByteLength = (events) => events.reduce((total, event) => total + new TextEncoder().encode(JSON.stringify(event)).byteLength, 0);
294
- export const outboxEventCount = (drafts, pending) => drafts.length + pending.reduce((count, record) => count + record.events.length, 0);
300
+ export const outboxEventCount = (drafts, pending) => drafts.length +
301
+ pending.reduce((count, record) => count + record.events.length, 0);
295
302
  export const hasLegacyTombstone = (draft, tombstones) => tombstones.some((record) => record.eventKeyHash === draft.eventKeyHash &&
296
303
  record.recordVerifierSha256 === draft.recordVerifierSha256);
297
304
  export const createGeneration = () => typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
@@ -299,7 +306,9 @@ export const createGeneration = () => typeof crypto !== "undefined" && typeof cr
299
306
  : `${Date.now()}-${Math.random()}`;
300
307
  export const readStorage = () => {
301
308
  try {
302
- return typeof localStorage === "undefined" ? null : localStorage;
309
+ return typeof localStorage === "undefined"
310
+ ? null
311
+ : localStorage;
303
312
  }
304
313
  catch {
305
314
  return null;
@@ -1,10 +1,10 @@
1
1
  import type { InternalMetricsStore } from "../internal/metrics.js";
2
2
  import type { CluebaseConsentState } from "../provider/types.js";
3
+ import { type CollectionPermit, type PendingBrowserBatch, type StorageLike } from "./browser-outbox-records.js";
3
4
  import type { BatchEnvelopeMetadata } from "./client.js";
4
5
  import { DiagnosticStore } from "./diagnostics.js";
5
6
  import type { BrowserSourceEvent } from "./source-event.js";
6
- import { type CollectionPermit, type PendingBrowserBatch, type StorageLike } from "./browser-outbox-records.js";
7
- export type { CollectionPermit, OwnedBrowserEvent, PendingBrowserBatch } from "./browser-outbox-records.js";
7
+ export type { CollectionPermit, OwnedBrowserEvent, PendingBrowserBatch, } from "./browser-outbox-records.js";
8
8
  export type ConsentTransition = {
9
9
  readonly state: CluebaseConsentState;
10
10
  readonly generation: string;
@@ -62,6 +62,7 @@ export declare class BrowserOutbox {
62
62
  private persistCommittedConsent;
63
63
  addDraft(event: BrowserSourceEvent, permit?: CollectionPermit): boolean;
64
64
  estimatedDraftBytes(measure: (events: readonly BrowserSourceEvent[], metadata: BatchEnvelopeMetadata) => number, metadata: BatchEnvelopeMetadata): number;
65
+ draftCount(): number;
65
66
  prepareFence(input: {
66
67
  readonly maxPayloadBytes: number;
67
68
  readonly createMetadata: () => BatchEnvelopeMetadata;
@@ -85,6 +86,8 @@ export declare class BrowserOutbox {
85
86
  private resolvePermit;
86
87
  private initializeConsentForDelivery;
87
88
  private enforceCapacity;
89
+ private dropOldestPendingForCapacity;
90
+ private forciblyRemovePendingRecord;
88
91
  private purgeAll;
89
92
  clearMemory(): void;
90
93
  private restore;