@genn-inc/cluebase-frontend-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -0
- package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
- package/dist/_generated/correlation-attribute-keys.js +33 -0
- package/dist/_generated/identity-pii-policy.d.ts +38 -0
- package/dist/_generated/identity-pii-policy.js +103 -0
- package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
- package/dist/_generated/ingest-privacy-tables.js +890 -0
- package/dist/_generated/otel-resource-keys.d.ts +21 -0
- package/dist/_generated/otel-resource-keys.js +72 -0
- package/dist/_generated/otel-span-conversion.d.ts +48 -0
- package/dist/_generated/otel-span-conversion.js +69 -0
- package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
- package/dist/_generated/parameter-snapshot-leaf.js +37 -0
- package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
- package/dist/_generated/sdk-runtime-policy.js +14 -0
- package/dist/_generated/usage-governor-tables.d.ts +15 -0
- package/dist/_generated/usage-governor-tables.js +44 -0
- package/dist/context/browser-storage.d.ts +15 -0
- package/dist/context/browser-storage.js +38 -0
- package/dist/context/environment.d.ts +37 -0
- package/dist/context/environment.js +229 -0
- package/dist/context/identity.d.ts +16 -0
- package/dist/context/identity.js +139 -0
- package/dist/context/organization.d.ts +16 -0
- package/dist/context/organization.js +60 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +130 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +117 -0
- package/dist/context/trace.d.ts +76 -0
- package/dist/context/trace.js +163 -0
- package/dist/exporter/batch.d.ts +94 -0
- package/dist/exporter/batch.js +485 -0
- package/dist/exporter/browser-outbox-records.d.ts +159 -0
- package/dist/exporter/browser-outbox-records.js +341 -0
- package/dist/exporter/browser-outbox.d.ts +112 -0
- package/dist/exporter/browser-outbox.js +876 -0
- package/dist/exporter/client.d.ts +92 -0
- package/dist/exporter/client.js +541 -0
- package/dist/exporter/delivery-response-contract.d.ts +103 -0
- package/dist/exporter/delivery-response-contract.js +147 -0
- package/dist/exporter/delivery-response.d.ts +37 -0
- package/dist/exporter/delivery-response.js +151 -0
- package/dist/exporter/diagnostics.d.ts +42 -0
- package/dist/exporter/diagnostics.js +148 -0
- package/dist/exporter/event-id.d.ts +10 -0
- package/dist/exporter/event-id.js +75 -0
- package/dist/exporter/frontend-resource.d.ts +20 -0
- package/dist/exporter/frontend-resource.js +52 -0
- package/dist/exporter/local-minimization.d.ts +29 -0
- package/dist/exporter/local-minimization.js +87 -0
- package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
- package/dist/exporter/observation-source-event-exporter.js +154 -0
- package/dist/exporter/parameter-snapshot.d.ts +14 -0
- package/dist/exporter/parameter-snapshot.js +215 -0
- package/dist/exporter/path-template.d.ts +1 -0
- package/dist/exporter/path-template.js +67 -0
- package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
- package/dist/exporter/raw-event-to-source-signal.js +297 -0
- package/dist/exporter/retry.d.ts +8 -0
- package/dist/exporter/retry.js +8 -0
- package/dist/exporter/sanitize.d.ts +30 -0
- package/dist/exporter/sanitize.js +177 -0
- package/dist/exporter/schema-evidence.d.ts +20 -0
- package/dist/exporter/schema-evidence.js +232 -0
- package/dist/exporter/source-event.d.ts +99 -0
- package/dist/exporter/source-event.js +13 -0
- package/dist/exporter/span-to-source-signal.d.ts +8 -0
- package/dist/exporter/span-to-source-signal.js +174 -0
- package/dist/exporter/text-fingerprint.d.ts +9 -0
- package/dist/exporter/text-fingerprint.js +16 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +158 -0
- package/dist/instrumentation/action.d.ts +2 -0
- package/dist/instrumentation/action.js +171 -0
- package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
- package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
- package/dist/instrumentation/common.d.ts +73 -0
- package/dist/instrumentation/common.js +90 -0
- package/dist/instrumentation/dom.d.ts +63 -0
- package/dist/instrumentation/dom.js +640 -0
- package/dist/instrumentation/drag.d.ts +2 -0
- package/dist/instrumentation/drag.js +82 -0
- package/dist/instrumentation/error.d.ts +2 -0
- package/dist/instrumentation/error.js +318 -0
- package/dist/instrumentation/form.d.ts +2 -0
- package/dist/instrumentation/form.js +132 -0
- package/dist/instrumentation/input.d.ts +2 -0
- package/dist/instrumentation/input.js +180 -0
- package/dist/instrumentation/location.d.ts +11 -0
- package/dist/instrumentation/location.js +69 -0
- package/dist/instrumentation/naming-evidence.d.ts +20 -0
- package/dist/instrumentation/naming-evidence.js +11 -0
- package/dist/instrumentation/navigation.d.ts +2 -0
- package/dist/instrumentation/navigation.js +144 -0
- package/dist/instrumentation/network-request.d.ts +103 -0
- package/dist/instrumentation/network-request.js +285 -0
- package/dist/instrumentation/network-span-attributes.d.ts +25 -0
- package/dist/instrumentation/network-span-attributes.js +153 -0
- package/dist/instrumentation/network.d.ts +16 -0
- package/dist/instrumentation/network.js +502 -0
- package/dist/instrumentation/page.d.ts +2 -0
- package/dist/instrumentation/page.js +56 -0
- package/dist/instrumentation/performance.d.ts +2 -0
- package/dist/instrumentation/performance.js +365 -0
- package/dist/integrations/next-router.d.ts +16 -0
- package/dist/integrations/next-router.js +18 -0
- package/dist/integrations/react-router.d.ts +7 -0
- package/dist/integrations/react-router.js +61 -0
- package/dist/internal/internal-log.d.ts +23 -0
- package/dist/internal/internal-log.js +47 -0
- package/dist/internal/metrics.d.ts +10 -0
- package/dist/internal/metrics.js +50 -0
- package/dist/provider/_version.d.ts +1 -0
- package/dist/provider/_version.js +3 -0
- package/dist/provider/config.d.ts +12 -0
- package/dist/provider/config.js +177 -0
- package/dist/provider/constants.d.ts +62 -0
- package/dist/provider/constants.js +118 -0
- package/dist/provider/contracts.d.ts +51 -0
- package/dist/provider/contracts.js +46 -0
- package/dist/provider/privacy-policy-store.d.ts +20 -0
- package/dist/provider/privacy-policy-store.js +56 -0
- package/dist/provider/sampling.d.ts +2 -0
- package/dist/provider/sampling.js +17 -0
- package/dist/provider/sdk-event-pipeline.d.ts +60 -0
- package/dist/provider/sdk-event-pipeline.js +478 -0
- package/dist/provider/sdk-input-normalization.d.ts +7 -0
- package/dist/provider/sdk-input-normalization.js +113 -0
- package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
- package/dist/provider/sdk-otel-pipeline.js +148 -0
- package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
- package/dist/provider/sdk-runtime-surfaces.js +216 -0
- package/dist/provider/sdk.d.ts +2 -0
- package/dist/provider/sdk.js +614 -0
- package/dist/provider/types.d.ts +323 -0
- package/dist/provider/types.js +1 -0
- package/dist/provider/usage-governor.d.ts +7 -0
- package/dist/provider/usage-governor.js +116 -0
- package/package.json +59 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const CLUEBASE_OTEL_REQUIRED_RESOURCE_KEYS: readonly ["service.name", "cluebase.project_key", "cluebase.runtime_language", "cluebase.service_type"];
|
|
2
|
+
export declare const CLUEBASE_OTEL_SDK_SOURCE_REQUIRED_RESOURCE_KEYS: readonly ["service.name", "cluebase.project_key", "cluebase.runtime_language", "cluebase.service_type", "cluebase.producer_id"];
|
|
3
|
+
export declare const CLUEBASE_OTEL_SDK_SOURCE_RESOURCE_KEYS: readonly ["service.name", "cluebase.project_key", "cluebase.runtime_language", "cluebase.service_type", "cluebase.producer_id", "service.version", "cluebase.sdk.type", "cluebase.sdk.version", "cluebase.sdk.collection_mode"];
|
|
4
|
+
export declare const CLUEBASE_OTEL_ALLOWED_RESOURCE_KEYS: readonly ["service.name", "cluebase.project_key", "cluebase.runtime_language", "cluebase.service_type", "cluebase.producer_id", "service.version", "cluebase.sdk.type", "cluebase.sdk.version", "cluebase.sdk.collection_mode", "deployment.environment", "service.namespace", "cloud.region", "k8s.namespace.name", "k8s.pod.name", "container.id"];
|
|
5
|
+
export declare const CLUEBASE_OTEL_RECOMMENDED_RESOURCE_KEYS: readonly ["deployment.environment", "service.version", "service.namespace", "cluebase.producer_id", "cloud.region", "k8s.namespace.name", "k8s.pod.name", "container.id"];
|
|
6
|
+
export declare const CLUEBASE_OTEL_RESOURCE_KEYS: {
|
|
7
|
+
readonly serviceName: "service.name";
|
|
8
|
+
readonly deploymentEnvironment: "deployment.environment";
|
|
9
|
+
readonly projectKey: "cluebase.project_key";
|
|
10
|
+
readonly producerId: "cluebase.producer_id";
|
|
11
|
+
readonly runtimeLanguage: "cluebase.runtime_language";
|
|
12
|
+
readonly serviceType: "cluebase.service_type";
|
|
13
|
+
readonly serviceVersion: "service.version";
|
|
14
|
+
readonly serviceNamespace: "service.namespace";
|
|
15
|
+
readonly cloudRegion: "cloud.region";
|
|
16
|
+
readonly k8sNamespaceName: "k8s.namespace.name";
|
|
17
|
+
readonly k8sPodName: "k8s.pod.name";
|
|
18
|
+
readonly containerId: "container.id";
|
|
19
|
+
};
|
|
20
|
+
export type CluebaseOtelRequiredResourceKey = (typeof CLUEBASE_OTEL_REQUIRED_RESOURCE_KEYS)[number];
|
|
21
|
+
export type CluebaseOtelRecommendedResourceKey = (typeof CLUEBASE_OTEL_RECOMMENDED_RESOURCE_KEYS)[number];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// 自動生成です。**手で編集しないでください。**
|
|
2
|
+
//
|
|
3
|
+
// 正本: packages/cluebase-backend-contract/src/delivery/otel-resource-keys.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
|
+
export const CLUEBASE_OTEL_REQUIRED_RESOURCE_KEYS = [
|
|
9
|
+
"service.name",
|
|
10
|
+
"cluebase.project_key",
|
|
11
|
+
"cluebase.runtime_language",
|
|
12
|
+
"cluebase.service_type",
|
|
13
|
+
];
|
|
14
|
+
export const CLUEBASE_OTEL_SDK_SOURCE_REQUIRED_RESOURCE_KEYS = [
|
|
15
|
+
"service.name",
|
|
16
|
+
"cluebase.project_key",
|
|
17
|
+
"cluebase.runtime_language",
|
|
18
|
+
"cluebase.service_type",
|
|
19
|
+
"cluebase.producer_id",
|
|
20
|
+
];
|
|
21
|
+
export const CLUEBASE_OTEL_SDK_SOURCE_RESOURCE_KEYS = [
|
|
22
|
+
"service.name",
|
|
23
|
+
"cluebase.project_key",
|
|
24
|
+
"cluebase.runtime_language",
|
|
25
|
+
"cluebase.service_type",
|
|
26
|
+
"cluebase.producer_id",
|
|
27
|
+
"service.version",
|
|
28
|
+
"cluebase.sdk.type",
|
|
29
|
+
"cluebase.sdk.version",
|
|
30
|
+
"cluebase.sdk.collection_mode",
|
|
31
|
+
];
|
|
32
|
+
export const CLUEBASE_OTEL_ALLOWED_RESOURCE_KEYS = [
|
|
33
|
+
"service.name",
|
|
34
|
+
"cluebase.project_key",
|
|
35
|
+
"cluebase.runtime_language",
|
|
36
|
+
"cluebase.service_type",
|
|
37
|
+
"cluebase.producer_id",
|
|
38
|
+
"service.version",
|
|
39
|
+
"cluebase.sdk.type",
|
|
40
|
+
"cluebase.sdk.version",
|
|
41
|
+
"cluebase.sdk.collection_mode",
|
|
42
|
+
"deployment.environment",
|
|
43
|
+
"service.namespace",
|
|
44
|
+
"cloud.region",
|
|
45
|
+
"k8s.namespace.name",
|
|
46
|
+
"k8s.pod.name",
|
|
47
|
+
"container.id",
|
|
48
|
+
];
|
|
49
|
+
export const CLUEBASE_OTEL_RECOMMENDED_RESOURCE_KEYS = [
|
|
50
|
+
"deployment.environment",
|
|
51
|
+
"service.version",
|
|
52
|
+
"service.namespace",
|
|
53
|
+
"cluebase.producer_id",
|
|
54
|
+
"cloud.region",
|
|
55
|
+
"k8s.namespace.name",
|
|
56
|
+
"k8s.pod.name",
|
|
57
|
+
"container.id",
|
|
58
|
+
];
|
|
59
|
+
export const CLUEBASE_OTEL_RESOURCE_KEYS = {
|
|
60
|
+
serviceName: "service.name",
|
|
61
|
+
deploymentEnvironment: "deployment.environment",
|
|
62
|
+
projectKey: "cluebase.project_key",
|
|
63
|
+
producerId: "cluebase.producer_id",
|
|
64
|
+
runtimeLanguage: "cluebase.runtime_language",
|
|
65
|
+
serviceType: "cluebase.service_type",
|
|
66
|
+
serviceVersion: "service.version",
|
|
67
|
+
serviceNamespace: "service.namespace",
|
|
68
|
+
cloudRegion: "cloud.region",
|
|
69
|
+
k8sNamespaceName: "k8s.namespace.name",
|
|
70
|
+
k8sPodName: "k8s.pod.name",
|
|
71
|
+
containerId: "container.id",
|
|
72
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 計測の区間 (span) を送信の形へ移す時の、時刻と属性の読み方の**正本**。
|
|
3
|
+
*
|
|
4
|
+
* ここは配る部品 2 つ(サーバー側・画面側)へ**書き出される元**です。
|
|
5
|
+
* `scripts/generate-shared-contract-tables.mjs` が、この中身をそのまま
|
|
6
|
+
* 各部品の `src/_generated/otel-span-conversion.ts` へ出します。
|
|
7
|
+
* 出した先を手で直しても、`--check` が落とします。
|
|
8
|
+
*
|
|
9
|
+
* なぜ写しではなく生成なのか。配る部品は顧客の手元で `tsc` だけで組み立てられ、
|
|
10
|
+
* まとめ込み (bundle) をしません。社内の共有部品は `private` なので、
|
|
11
|
+
* 「配る部品から共有部品を取り込む」形は取れません(顧客の手元で解決できない
|
|
12
|
+
* 取り込みになります)。**これが写しが存在していた理由です。**
|
|
13
|
+
*
|
|
14
|
+
* 中身の型を計測の枠組みから取らず、必要な形だけを書いています。そうしないと、
|
|
15
|
+
* 出した先の 2 つで属性の型の名前が違う(`SdkSignalAttributes` と
|
|
16
|
+
* `CanonicalJsonObject`)ため、同じ 1 つの文章にできません。
|
|
17
|
+
*/
|
|
18
|
+
/** 計測の枠組みが渡す時刻。[秒, ナノ秒] の 2 つ組。 */
|
|
19
|
+
type SpanHrTime = readonly [number, number] | undefined;
|
|
20
|
+
/** 属性の入れ物。中身の型は読む側が決めるので、ここでは問いません。 */
|
|
21
|
+
type SpanAttributes = {
|
|
22
|
+
readonly [key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
/** 親の区間を持つかもしれない区間。 */
|
|
25
|
+
type SpanWithParent = {
|
|
26
|
+
readonly parentSpanContext?: {
|
|
27
|
+
readonly spanId?: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 時刻をナノ秒の文字列にする。
|
|
32
|
+
*
|
|
33
|
+
* 時刻が無い・数でない場合は「いま」で埋める。**送る形が壊れるより、
|
|
34
|
+
* 近い時刻で送るほうがよい**(受け取る側は時刻の欠けを直せない)。
|
|
35
|
+
*/
|
|
36
|
+
export declare function hrTimeToUnixNano(time: SpanHrTime): string;
|
|
37
|
+
/** ナノ秒の文字列を、読める時刻の文字列にする。 */
|
|
38
|
+
export declare function nanosToIso(nanos: string): string;
|
|
39
|
+
/** 属性から「中身のある文字列」を取り出す。空白だけなら無いものとして扱う。 */
|
|
40
|
+
export declare function attrString(attributes: SpanAttributes, key: string): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* 親の区間の識別子。
|
|
43
|
+
*
|
|
44
|
+
* 属性に明示されていればそれを採る。計測の枠組みが持つ親より属性を優先するのは、
|
|
45
|
+
* 画面と受付をまたぐ繋がりを、こちらで組み直して入れているため。
|
|
46
|
+
*/
|
|
47
|
+
export declare function parentSpanId(span: SpanWithParent, attributes: SpanAttributes): string | null;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* 計測の区間 (span) を送信の形へ移す時の、時刻と属性の読み方の**正本**。
|
|
10
|
+
*
|
|
11
|
+
* ここは配る部品 2 つ(サーバー側・画面側)へ**書き出される元**です。
|
|
12
|
+
* `scripts/generate-shared-contract-tables.mjs` が、この中身をそのまま
|
|
13
|
+
* 各部品の `src/_generated/otel-span-conversion.ts` へ出します。
|
|
14
|
+
* 出した先を手で直しても、`--check` が落とします。
|
|
15
|
+
*
|
|
16
|
+
* なぜ写しではなく生成なのか。配る部品は顧客の手元で `tsc` だけで組み立てられ、
|
|
17
|
+
* まとめ込み (bundle) をしません。社内の共有部品は `private` なので、
|
|
18
|
+
* 「配る部品から共有部品を取り込む」形は取れません(顧客の手元で解決できない
|
|
19
|
+
* 取り込みになります)。**これが写しが存在していた理由です。**
|
|
20
|
+
*
|
|
21
|
+
* 中身の型を計測の枠組みから取らず、必要な形だけを書いています。そうしないと、
|
|
22
|
+
* 出した先の 2 つで属性の型の名前が違う(`SdkSignalAttributes` と
|
|
23
|
+
* `CanonicalJsonObject`)ため、同じ 1 つの文章にできません。
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* 時刻をナノ秒の文字列にする。
|
|
27
|
+
*
|
|
28
|
+
* 時刻が無い・数でない場合は「いま」で埋める。**送る形が壊れるより、
|
|
29
|
+
* 近い時刻で送るほうがよい**(受け取る側は時刻の欠けを直せない)。
|
|
30
|
+
*/
|
|
31
|
+
export function hrTimeToUnixNano(time) {
|
|
32
|
+
if (!time)
|
|
33
|
+
return `${Date.now()}000000`;
|
|
34
|
+
const [seconds, nanos] = time;
|
|
35
|
+
if (typeof seconds !== "number" || typeof nanos !== "number") {
|
|
36
|
+
return `${Date.now()}000000`;
|
|
37
|
+
}
|
|
38
|
+
return (BigInt(seconds) * 1000000000n + BigInt(nanos)).toString();
|
|
39
|
+
}
|
|
40
|
+
/** ナノ秒の文字列を、読める時刻の文字列にする。 */
|
|
41
|
+
export function nanosToIso(nanos) {
|
|
42
|
+
try {
|
|
43
|
+
const ms = Number(BigInt(nanos) / 1000000n);
|
|
44
|
+
if (Number.isFinite(ms) && ms > 0)
|
|
45
|
+
return new Date(ms).toISOString();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// 試験の身代わりが壊れた時刻を渡すことがある。送る形は保つ。
|
|
49
|
+
}
|
|
50
|
+
return new Date().toISOString();
|
|
51
|
+
}
|
|
52
|
+
/** 属性から「中身のある文字列」を取り出す。空白だけなら無いものとして扱う。 */
|
|
53
|
+
export function attrString(attributes, key) {
|
|
54
|
+
const value = attributes[key];
|
|
55
|
+
return typeof value === "string" && value.trim().length > 0
|
|
56
|
+
? value.trim()
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 親の区間の識別子。
|
|
61
|
+
*
|
|
62
|
+
* 属性に明示されていればそれを採る。計測の枠組みが持つ親より属性を優先するのは、
|
|
63
|
+
* 画面と受付をまたぐ繋がりを、こちらで組み直して入れているため。
|
|
64
|
+
*/
|
|
65
|
+
export function parentSpanId(span, attributes) {
|
|
66
|
+
return (attrString(attributes, "cluebase.parent_span_id") ??
|
|
67
|
+
span.parentSpanContext?.spanId ??
|
|
68
|
+
null);
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* その節は、値そのもの(葉)か。**降りる枝ではないか。**
|
|
3
|
+
*
|
|
4
|
+
* 誤ると、値のつもりだったものの中へ降り、その鍵を項目名として扱います。
|
|
5
|
+
* 降りる側の 1 つは伏せる判定なので、**間違いは伏せ漏れになります。**
|
|
6
|
+
*
|
|
7
|
+
* 型を受けずに `unknown` で受けるのは、この file が正本の型を取り込まないためです
|
|
8
|
+
* (取り込むと正本 → 生成物 → 正本の輪になります)。**型を付けるのは呼ぶ側**で、
|
|
9
|
+
* 受付側も配る部品も薄い包みを 1 つ書きます。
|
|
10
|
+
*/
|
|
11
|
+
export declare const hasParameterSnapshotLeafShape: (value: unknown) => boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// 自動生成です。**手で編集しないでください。**
|
|
2
|
+
//
|
|
3
|
+
// 正本: packages/cluebase-schema/src/semantic/observation-classification.schema.ts
|
|
4
|
+
// 出し直す: node scripts/generate-shared-contract-tables.mjs
|
|
5
|
+
//
|
|
6
|
+
// 正本を直したらこの file も一緒に commit してください。CI が
|
|
7
|
+
// `node scripts/generate-shared-contract-tables.mjs --check` で照合し、ずれていれば落とします。
|
|
8
|
+
/**
|
|
9
|
+
* その節は、値そのもの(葉)か。**降りる枝ではないか。**
|
|
10
|
+
*
|
|
11
|
+
* 誤ると、値のつもりだったものの中へ降り、その鍵を項目名として扱います。
|
|
12
|
+
* 降りる側の 1 つは伏せる判定なので、**間違いは伏せ漏れになります。**
|
|
13
|
+
*
|
|
14
|
+
* 型を受けずに `unknown` で受けるのは、この file が正本の型を取り込まないためです
|
|
15
|
+
* (取り込むと正本 → 生成物 → 正本の輪になります)。**型を付けるのは呼ぶ側**で、
|
|
16
|
+
* 受付側も配る部品も薄い包みを 1 つ書きます。
|
|
17
|
+
*/
|
|
18
|
+
export const hasParameterSnapshotLeafShape = (value) => {
|
|
19
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
20
|
+
return false;
|
|
21
|
+
const node = value;
|
|
22
|
+
// 自分が持つ鍵だけを見ます(受け継いだ鍵は葉の名乗りではありません)。
|
|
23
|
+
// **`Object.hasOwn` は使いません。** 配る部品は ES2020 を的にしており、
|
|
24
|
+
// 顧客の古い閲覧環境で落ちます。これは同じ意味で、どこでも動く書き方です
|
|
25
|
+
const owns = (key) => Object.prototype.hasOwnProperty.call(node, key);
|
|
26
|
+
if (!owns("type"))
|
|
27
|
+
return false;
|
|
28
|
+
if (!owns("value"))
|
|
29
|
+
return false;
|
|
30
|
+
if (!owns("capture_mode"))
|
|
31
|
+
return false;
|
|
32
|
+
if (typeof node["type"] !== "string")
|
|
33
|
+
return false;
|
|
34
|
+
if (typeof node["capture_mode"] !== "string")
|
|
35
|
+
return false;
|
|
36
|
+
return true;
|
|
37
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// 自動生成です。**手で編集しないでください。**
|
|
2
|
+
//
|
|
3
|
+
// 正本: packages/cluebase-backend-contract/src/delivery/sdk-runtime-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
|
+
/** 送信をやり直す時の待ち方。数の持ち主は契約の正本 1 つだけです。 */
|
|
9
|
+
export const SDK_RETRY_POLICY = {
|
|
10
|
+
maxAttempts: 3,
|
|
11
|
+
baseDelayMs: 300,
|
|
12
|
+
maxDelayMs: 5000,
|
|
13
|
+
maxAdditiveJitterMs: 200,
|
|
14
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 費用の上限に達したときに止める対象。 */
|
|
2
|
+
export declare const COST_CAPABILITIES: readonly ["event_collection", "replay_storage", "ai_generation", "raw_observation_storage", "retention_growth"];
|
|
3
|
+
export type CostCapability = (typeof COST_CAPABILITIES)[number];
|
|
4
|
+
/** 上限の種類。 */
|
|
5
|
+
export declare const LIMIT_KINDS: readonly ["monthly_overage_amount_limit", "internal_cost_limit", "internal_limit", "operator_global_monthly_cost_limit"];
|
|
6
|
+
export type LimitKind = (typeof LIMIT_KINDS)[number];
|
|
7
|
+
/** 上限へ近づいたことを知らせる段階。 */
|
|
8
|
+
export declare const THRESHOLD_STAGES: readonly ["info", "warning", "urgent", "reached"];
|
|
9
|
+
export type ThresholdStage = (typeof THRESHOLD_STAGES)[number];
|
|
10
|
+
/** 顧客へ見せる請求の計器。 */
|
|
11
|
+
export declare const CUSTOMER_BILLING_METER_KEYS: readonly ["monthly_base_fee", "cluebase_events", "replay_storage", "ai_credits", "data_retention"];
|
|
12
|
+
export type CustomerBillingMeterKey = (typeof CUSTOMER_BILLING_METER_KEYS)[number];
|
|
13
|
+
/** 使った分だけ請求される計器(上の一覧から、定額のものを引いたもの)。 */
|
|
14
|
+
export declare const BILLABLE_USAGE_METER_KEYS: readonly ["cluebase_events", "replay_storage", "ai_credits", "data_retention"];
|
|
15
|
+
export type BillableUsageMeterKey = (typeof BILLABLE_USAGE_METER_KEYS)[number];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 自動生成です。**手で編集しないでください。**
|
|
2
|
+
//
|
|
3
|
+
// 正本: packages/cluebase-shared/src/ingest/usage-governor-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 COST_CAPABILITIES = [
|
|
10
|
+
"event_collection",
|
|
11
|
+
"replay_storage",
|
|
12
|
+
"ai_generation",
|
|
13
|
+
"raw_observation_storage",
|
|
14
|
+
"retention_growth",
|
|
15
|
+
];
|
|
16
|
+
/** 上限の種類。 */
|
|
17
|
+
export const LIMIT_KINDS = [
|
|
18
|
+
"monthly_overage_amount_limit",
|
|
19
|
+
"internal_cost_limit",
|
|
20
|
+
"internal_limit",
|
|
21
|
+
"operator_global_monthly_cost_limit",
|
|
22
|
+
];
|
|
23
|
+
/** 上限へ近づいたことを知らせる段階。 */
|
|
24
|
+
export const THRESHOLD_STAGES = [
|
|
25
|
+
"info",
|
|
26
|
+
"warning",
|
|
27
|
+
"urgent",
|
|
28
|
+
"reached",
|
|
29
|
+
];
|
|
30
|
+
/** 顧客へ見せる請求の計器。 */
|
|
31
|
+
export const CUSTOMER_BILLING_METER_KEYS = [
|
|
32
|
+
"monthly_base_fee",
|
|
33
|
+
"cluebase_events",
|
|
34
|
+
"replay_storage",
|
|
35
|
+
"ai_credits",
|
|
36
|
+
"data_retention",
|
|
37
|
+
];
|
|
38
|
+
/** 使った分だけ請求される計器(上の一覧から、定額のものを引いたもの)。 */
|
|
39
|
+
export const BILLABLE_USAGE_METER_KEYS = [
|
|
40
|
+
"cluebase_events",
|
|
41
|
+
"replay_storage",
|
|
42
|
+
"ai_credits",
|
|
43
|
+
"data_retention",
|
|
44
|
+
];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 画面側の保管先を 1 か所で決める。
|
|
3
|
+
*
|
|
4
|
+
* 4 つの入れ物(利用者・組織・滞在・タブ)が同じ「使えるか試して、駄目なら
|
|
5
|
+
* メモリへ落とす」規則をそれぞれ持っていた。規則が 4 通りに分かれると、
|
|
6
|
+
* 5 つ目の入れ物を足す人がどれを写すか選べない(CLAUDE.md §3)。
|
|
7
|
+
*/
|
|
8
|
+
export type StorageLike = Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
9
|
+
/** 保管先が使えないときの落とし先。ページを離れると消える。 */
|
|
10
|
+
export declare const createMemoryStorage: () => StorageLike;
|
|
11
|
+
/**
|
|
12
|
+
* 実際に 1 度書いて消してみてから返す。存在の確認だけでは足りない
|
|
13
|
+
* (private browsing や容量上限では、存在しても `setItem` が投げる)。
|
|
14
|
+
*/
|
|
15
|
+
export declare const resolveWebStorage: (kind: "local" | "session", probeKey: string) => StorageLike;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 画面側の保管先を 1 か所で決める。
|
|
3
|
+
*
|
|
4
|
+
* 4 つの入れ物(利用者・組織・滞在・タブ)が同じ「使えるか試して、駄目なら
|
|
5
|
+
* メモリへ落とす」規則をそれぞれ持っていた。規則が 4 通りに分かれると、
|
|
6
|
+
* 5 つ目の入れ物を足す人がどれを写すか選べない(CLAUDE.md §3)。
|
|
7
|
+
*/
|
|
8
|
+
/** 保管先が使えないときの落とし先。ページを離れると消える。 */
|
|
9
|
+
export const createMemoryStorage = () => {
|
|
10
|
+
const store = new Map();
|
|
11
|
+
return {
|
|
12
|
+
getItem: (key) => store.get(key) ?? null,
|
|
13
|
+
setItem: (key, value) => {
|
|
14
|
+
store.set(key, value);
|
|
15
|
+
},
|
|
16
|
+
removeItem: (key) => {
|
|
17
|
+
store.delete(key);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 実際に 1 度書いて消してみてから返す。存在の確認だけでは足りない
|
|
23
|
+
* (private browsing や容量上限では、存在しても `setItem` が投げる)。
|
|
24
|
+
*/
|
|
25
|
+
export const resolveWebStorage = (kind, probeKey) => {
|
|
26
|
+
try {
|
|
27
|
+
const storage = kind === "local" ? globalThis.localStorage : globalThis.sessionStorage;
|
|
28
|
+
if (typeof storage !== "undefined") {
|
|
29
|
+
storage.setItem(probeKey, "1");
|
|
30
|
+
storage.removeItem(probeKey);
|
|
31
|
+
return storage;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Ignore and fallback.
|
|
36
|
+
}
|
|
37
|
+
return createMemoryStorage();
|
|
38
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CanonicalJsonValue } from "../provider/types.js";
|
|
2
|
+
type EnvironmentOptions = {
|
|
3
|
+
frontendRelease: string | null;
|
|
4
|
+
contextTraitsProvider?: () => Record<string, CanonicalJsonValue>;
|
|
5
|
+
};
|
|
6
|
+
export type EnvironmentContextValue = {
|
|
7
|
+
frontendRelease: string | null;
|
|
8
|
+
contextTraits: Record<string, CanonicalJsonValue>;
|
|
9
|
+
locale: string | null;
|
|
10
|
+
country: string | null;
|
|
11
|
+
deviceType: string | null;
|
|
12
|
+
browser: string | null;
|
|
13
|
+
os: string | null;
|
|
14
|
+
referrer: string | null;
|
|
15
|
+
browserFamily: string | null;
|
|
16
|
+
browserMajorVersion: string | null;
|
|
17
|
+
browserEngine: string | null;
|
|
18
|
+
osFamily: string | null;
|
|
19
|
+
osMajorVersion: string | null;
|
|
20
|
+
timezone: string | null;
|
|
21
|
+
pointerTypeCandidate: string | null;
|
|
22
|
+
touchCapable: boolean | null;
|
|
23
|
+
cookiesEnabled: boolean | null;
|
|
24
|
+
localStorageAvailable: boolean | null;
|
|
25
|
+
sessionStorageAvailable: boolean | null;
|
|
26
|
+
serviceWorkerSupported: boolean | null;
|
|
27
|
+
webglSupported: boolean | null;
|
|
28
|
+
reducedMotion: string | null;
|
|
29
|
+
colorScheme: string | null;
|
|
30
|
+
};
|
|
31
|
+
export declare class EnvironmentManager {
|
|
32
|
+
private readonly options;
|
|
33
|
+
constructor(options: EnvironmentOptions);
|
|
34
|
+
getContext(): EnvironmentContextValue;
|
|
35
|
+
private safeContextTraits;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { toPathTemplate } from "../exporter/path-template.js";
|
|
2
|
+
import { detectIdentityPII } from "../exporter/sanitize.js";
|
|
3
|
+
import { stripQueryAndHashFromUrl } from "../provider/sdk-input-normalization.js";
|
|
4
|
+
const firstMatch = (userAgent, pattern) => {
|
|
5
|
+
const match = pattern.exec(userAgent);
|
|
6
|
+
return match?.[1] ?? null;
|
|
7
|
+
};
|
|
8
|
+
const detectBrowserInfo = (userAgent) => {
|
|
9
|
+
if (/Edg\//.test(userAgent)) {
|
|
10
|
+
return {
|
|
11
|
+
family: "edge",
|
|
12
|
+
majorVersion: firstMatch(userAgent, /Edg\/(\d+)/),
|
|
13
|
+
engine: "blink",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (/Chrome\//.test(userAgent)) {
|
|
17
|
+
return {
|
|
18
|
+
family: "chrome",
|
|
19
|
+
majorVersion: firstMatch(userAgent, /Chrome\/(\d+)/),
|
|
20
|
+
engine: "blink",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (/Safari\//.test(userAgent) && /Version\//.test(userAgent)) {
|
|
24
|
+
return {
|
|
25
|
+
family: "safari",
|
|
26
|
+
majorVersion: firstMatch(userAgent, /Version\/(\d+)/),
|
|
27
|
+
engine: "webkit",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (/Firefox\//.test(userAgent)) {
|
|
31
|
+
return {
|
|
32
|
+
family: "firefox",
|
|
33
|
+
majorVersion: firstMatch(userAgent, /Firefox\/(\d+)/),
|
|
34
|
+
engine: "gecko",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
family: null,
|
|
39
|
+
majorVersion: null,
|
|
40
|
+
engine: null,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const detectOsInfo = (userAgent) => {
|
|
44
|
+
if (/Windows/.test(userAgent)) {
|
|
45
|
+
return {
|
|
46
|
+
family: "windows",
|
|
47
|
+
majorVersion: firstMatch(userAgent, /Windows NT (\d+)/),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (/Mac OS X/.test(userAgent)) {
|
|
51
|
+
return {
|
|
52
|
+
family: "macos",
|
|
53
|
+
majorVersion: firstMatch(userAgent, /Mac OS X (\d+)[_.]/),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (/Android/.test(userAgent)) {
|
|
57
|
+
return {
|
|
58
|
+
family: "android",
|
|
59
|
+
majorVersion: firstMatch(userAgent, /Android (\d+)/),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (/(iPhone|iPad|iPod)/.test(userAgent)) {
|
|
63
|
+
return {
|
|
64
|
+
family: "ios",
|
|
65
|
+
majorVersion: firstMatch(userAgent, /OS (\d+)[_.]/),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (/Linux/.test(userAgent)) {
|
|
69
|
+
return {
|
|
70
|
+
family: "linux",
|
|
71
|
+
majorVersion: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
family: null,
|
|
76
|
+
majorVersion: null,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const detectDeviceType = (userAgent) => {
|
|
80
|
+
if (/Tablet|iPad/.test(userAgent)) {
|
|
81
|
+
return "tablet";
|
|
82
|
+
}
|
|
83
|
+
if (/Mobile|Android|iPhone|iPod/.test(userAgent)) {
|
|
84
|
+
return "mobile";
|
|
85
|
+
}
|
|
86
|
+
return "desktop";
|
|
87
|
+
};
|
|
88
|
+
const detectTimezone = () => {
|
|
89
|
+
try {
|
|
90
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const storageAvailable = (storageName) => {
|
|
97
|
+
try {
|
|
98
|
+
const storage = globalThis[storageName];
|
|
99
|
+
if (!storage) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const key = "__cluebase_storage_probe__";
|
|
103
|
+
storage.setItem(key, "1");
|
|
104
|
+
storage.removeItem(key);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const detectPointerType = () => {
|
|
112
|
+
if (typeof globalThis.matchMedia !== "function") {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (globalThis.matchMedia("(pointer: coarse)").matches) {
|
|
116
|
+
return "coarse";
|
|
117
|
+
}
|
|
118
|
+
if (globalThis.matchMedia("(pointer: fine)").matches) {
|
|
119
|
+
return "fine";
|
|
120
|
+
}
|
|
121
|
+
return "unknown";
|
|
122
|
+
};
|
|
123
|
+
const detectMediaPreference = (query, matchesValue, fallbackValue) => {
|
|
124
|
+
if (typeof globalThis.matchMedia !== "function") {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return globalThis.matchMedia(query).matches ? matchesValue : fallbackValue;
|
|
128
|
+
};
|
|
129
|
+
const sanitizeReferrer = (value) => {
|
|
130
|
+
const stripped = stripQueryAndHashFromUrl(value);
|
|
131
|
+
if (stripped === null)
|
|
132
|
+
return null;
|
|
133
|
+
try {
|
|
134
|
+
const url = new URL(stripped);
|
|
135
|
+
const maskedPath = url.pathname
|
|
136
|
+
.split("/")
|
|
137
|
+
.map((segment) => {
|
|
138
|
+
try {
|
|
139
|
+
return detectIdentityPII(decodeURIComponent(segment)) === null
|
|
140
|
+
? segment
|
|
141
|
+
: ":id";
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return ":id";
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.join("/");
|
|
148
|
+
return `${url.origin}${toPathTemplate(maskedPath)}`;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return toPathTemplate(stripped);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const detectWebglSupport = () => {
|
|
155
|
+
try {
|
|
156
|
+
if (typeof globalThis.document === "undefined") {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (!("WebGLRenderingContext" in globalThis) &&
|
|
160
|
+
!("WebGL2RenderingContext" in globalThis)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const canvas = globalThis.document.createElement("canvas");
|
|
164
|
+
return Boolean(canvas.getContext("webgl"));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
export class EnvironmentManager {
|
|
171
|
+
constructor(options) {
|
|
172
|
+
this.options = options;
|
|
173
|
+
}
|
|
174
|
+
getContext() {
|
|
175
|
+
const userAgent = typeof globalThis.navigator !== "undefined"
|
|
176
|
+
? globalThis.navigator.userAgent
|
|
177
|
+
: "";
|
|
178
|
+
const locale = typeof globalThis.navigator !== "undefined"
|
|
179
|
+
? globalThis.navigator.language || null
|
|
180
|
+
: null;
|
|
181
|
+
const referrer = sanitizeReferrer(typeof globalThis.document !== "undefined"
|
|
182
|
+
? globalThis.document.referrer || null
|
|
183
|
+
: null);
|
|
184
|
+
const browserInfo = detectBrowserInfo(userAgent);
|
|
185
|
+
const osInfo = detectOsInfo(userAgent);
|
|
186
|
+
return {
|
|
187
|
+
frontendRelease: this.options.frontendRelease,
|
|
188
|
+
contextTraits: this.safeContextTraits(),
|
|
189
|
+
locale,
|
|
190
|
+
country: null,
|
|
191
|
+
deviceType: detectDeviceType(userAgent),
|
|
192
|
+
browser: browserInfo.family,
|
|
193
|
+
os: osInfo.family,
|
|
194
|
+
referrer,
|
|
195
|
+
browserFamily: browserInfo.family,
|
|
196
|
+
browserMajorVersion: browserInfo.majorVersion,
|
|
197
|
+
browserEngine: browserInfo.engine,
|
|
198
|
+
osFamily: osInfo.family,
|
|
199
|
+
osMajorVersion: osInfo.majorVersion,
|
|
200
|
+
timezone: detectTimezone(),
|
|
201
|
+
pointerTypeCandidate: detectPointerType(),
|
|
202
|
+
touchCapable: typeof globalThis.navigator !== "undefined"
|
|
203
|
+
? globalThis.navigator.maxTouchPoints > 0
|
|
204
|
+
: null,
|
|
205
|
+
cookiesEnabled: typeof globalThis.navigator !== "undefined"
|
|
206
|
+
? globalThis.navigator.cookieEnabled
|
|
207
|
+
: null,
|
|
208
|
+
localStorageAvailable: storageAvailable("localStorage"),
|
|
209
|
+
sessionStorageAvailable: storageAvailable("sessionStorage"),
|
|
210
|
+
serviceWorkerSupported: typeof globalThis.navigator !== "undefined"
|
|
211
|
+
? "serviceWorker" in globalThis.navigator
|
|
212
|
+
: null,
|
|
213
|
+
webglSupported: detectWebglSupport(),
|
|
214
|
+
reducedMotion: detectMediaPreference("(prefers-reduced-motion: reduce)", "reduce", "no-preference"),
|
|
215
|
+
colorScheme: detectMediaPreference("(prefers-color-scheme: dark)", "dark", "light"),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
safeContextTraits() {
|
|
219
|
+
try {
|
|
220
|
+
const traits = this.options.contextTraitsProvider?.() ?? {};
|
|
221
|
+
return typeof traits === "object" && traits !== null && !Array.isArray(traits)
|
|
222
|
+
? traits
|
|
223
|
+
: {};
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|