@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.
- package/README.md +21 -0
- package/dist/_generated/{identity-pii-policy.d.ts → identity-pii-policy-text-contains.d.ts} +1 -1
- package/dist/_generated/{identity-pii-policy.js → identity-pii-policy-text-contains.js} +1 -1
- package/dist/_generated/identity-pii-policy-whole-value.d.ts +37 -0
- package/dist/_generated/identity-pii-policy-whole-value.js +106 -0
- package/dist/_generated/otel-span-conversion.js +1 -9
- package/dist/context/browser-storage.js +1 -2
- package/dist/context/environment.js +5 -3
- package/dist/context/identity.js +5 -9
- package/dist/context/organization.d.ts +1 -1
- package/dist/context/organization.js +6 -8
- package/dist/context/session.js +6 -8
- package/dist/context/tab.js +8 -12
- package/dist/context/trace.js +2 -1
- package/dist/exporter/batch.d.ts +3 -1
- package/dist/exporter/batch.js +6 -10
- package/dist/exporter/browser-outbox-records.d.ts +1 -1
- package/dist/exporter/browser-outbox-records.js +15 -6
- package/dist/exporter/browser-outbox.d.ts +5 -2
- package/dist/exporter/browser-outbox.js +48 -11
- package/dist/exporter/client.d.ts +2 -2
- package/dist/exporter/client.js +15 -30
- package/dist/exporter/delivery-response-contract.js +67 -17
- package/dist/exporter/delivery-response.d.ts +3 -1
- package/dist/exporter/delivery-response.js +31 -20
- package/dist/exporter/diagnostics.d.ts +1 -1
- package/dist/exporter/diagnostics.js +4 -1
- package/dist/exporter/event-id.js +1 -3
- package/dist/exporter/frontend-resource.js +5 -4
- package/dist/exporter/observation-source-event-exporter.d.ts +1 -0
- package/dist/exporter/observation-source-event-exporter.js +5 -2
- package/dist/exporter/path-template.js +3 -1
- package/dist/exporter/raw-event-to-source-signal.js +4 -2
- package/dist/exporter/sanitize.d.ts +3 -2
- package/dist/exporter/sanitize.js +4 -3
- package/dist/exporter/sdk-core-wasm.d.ts +5 -0
- package/dist/exporter/sdk-core-wasm.js +18 -0
- package/dist/exporter/source-event.d.ts +1 -0
- package/dist/exporter/span-to-source-signal.d.ts +1 -1
- package/dist/exporter/span-to-source-signal.js +122 -82
- package/dist/index.d.ts +5 -4
- package/dist/index.js +16 -6
- package/dist/instrumentation/action.js +9 -8
- package/dist/instrumentation/dom.js +13 -21
- package/dist/instrumentation/drag.js +0 -3
- package/dist/instrumentation/error.js +3 -1
- package/dist/instrumentation/form.js +5 -6
- package/dist/instrumentation/input.js +4 -8
- package/dist/instrumentation/location.js +3 -1
- package/dist/instrumentation/network-request.js +1 -1
- package/dist/instrumentation/network-span-attributes.js +2 -2
- package/dist/instrumentation/network.js +7 -3
- package/dist/instrumentation/performance.js +1 -2
- package/dist/internal/internal-log.js +2 -1
- package/dist/provider/_version.d.ts +1 -1
- package/dist/provider/_version.js +1 -1
- package/dist/provider/config.js +0 -1
- package/dist/provider/constants.d.ts +1 -1
- package/dist/provider/constants.js +0 -1
- package/dist/provider/sdk-event-pipeline.d.ts +2 -1
- package/dist/provider/sdk-event-pipeline.js +6 -3
- package/dist/provider/sdk-input-normalization.js +2 -2
- package/dist/provider/sdk-otel-pipeline.js +5 -4
- package/dist/provider/sdk-runtime-surfaces.d.ts +15 -12
- package/dist/provider/sdk-runtime-surfaces.js +8 -7
- package/dist/provider/sdk.js +67 -12
- package/dist/provider/types.d.ts +6 -5
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.d.ts +41 -0
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm.js +267 -0
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm +0 -0
- package/dist/wasm/pkg-web/cluebase_sdk_core_wasm_bg.wasm.d.ts +12 -0
- package/dist/wasm/pkg-web/package.json +15 -0
- 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
|
|
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
|
|
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) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toPathTemplate } from "../exporter/path-template.js";
|
|
2
|
-
import {
|
|
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
|
|
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" &&
|
|
221
|
+
return typeof traits === "object" &&
|
|
222
|
+
traits !== null &&
|
|
223
|
+
!Array.isArray(traits)
|
|
222
224
|
? traits
|
|
223
225
|
: {};
|
|
224
226
|
}
|
package/dist/context/identity.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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: {},
|
package/dist/context/session.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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);
|
package/dist/context/tab.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
}
|
package/dist/context/trace.js
CHANGED
|
@@ -66,7 +66,8 @@ export class TraceManager {
|
|
|
66
66
|
endFrontendAction(interactionId) {
|
|
67
67
|
if (!this.activeInteraction)
|
|
68
68
|
return;
|
|
69
|
-
if (interactionId &&
|
|
69
|
+
if (interactionId &&
|
|
70
|
+
this.activeInteraction.interactionId !== interactionId) {
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
73
|
this.recentInteraction = {
|
package/dist/exporter/batch.d.ts
CHANGED
|
@@ -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;
|
package/dist/exporter/batch.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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) ||
|
|
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" ||
|
|
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" ||
|
|
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" &&
|
|
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 +
|
|
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"
|
|
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
|
-
|
|
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;
|