@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,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const ingestRetryStrategySchema: z.ZodEnum<{
|
|
3
|
+
split_batch: "split_batch";
|
|
4
|
+
retry_same_batch: "retry_same_batch";
|
|
5
|
+
stop_and_report: "stop_and_report";
|
|
6
|
+
mark_failed: "mark_failed";
|
|
7
|
+
drop_event: "drop_event";
|
|
8
|
+
}>;
|
|
9
|
+
export declare const ingestPrivacyPolicySchema: z.ZodObject<{
|
|
10
|
+
token: z.ZodString;
|
|
11
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
12
|
+
method: z.ZodString;
|
|
13
|
+
path_template: z.ZodString;
|
|
14
|
+
direction: z.ZodEnum<{
|
|
15
|
+
request: "request";
|
|
16
|
+
response: "response";
|
|
17
|
+
}>;
|
|
18
|
+
field_path: z.ZodString;
|
|
19
|
+
}, z.core.$strict>>;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
export type IngestPrivacyPolicy = z.output<typeof ingestPrivacyPolicySchema>;
|
|
22
|
+
export declare const observationSourceEventBatchResponseSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
23
|
+
accepted: z.ZodLiteral<true>;
|
|
24
|
+
observation_ingest_batch_id: z.ZodString;
|
|
25
|
+
status: z.ZodLiteral<"received">;
|
|
26
|
+
submitted_event_count: z.ZodNumber;
|
|
27
|
+
accepted_event_count: z.ZodNumber;
|
|
28
|
+
dropped_event_count: z.ZodNumber;
|
|
29
|
+
dropped_events: z.ZodArray<z.ZodObject<{
|
|
30
|
+
event_index: z.ZodNumber;
|
|
31
|
+
reason: z.ZodString;
|
|
32
|
+
source_event_payload_fingerprint: z.ZodNullable<z.ZodString>;
|
|
33
|
+
}, z.core.$strict>>;
|
|
34
|
+
duplicate: z.ZodBoolean;
|
|
35
|
+
governor: z.ZodOptional<z.ZodObject<{
|
|
36
|
+
disabledCapabilities: z.ZodArray<z.ZodEnum<{
|
|
37
|
+
event_collection: "event_collection";
|
|
38
|
+
replay_storage: "replay_storage";
|
|
39
|
+
ai_generation: "ai_generation";
|
|
40
|
+
raw_observation_storage: "raw_observation_storage";
|
|
41
|
+
retention_growth: "retention_growth";
|
|
42
|
+
}>>;
|
|
43
|
+
limitReasons: z.ZodArray<z.ZodObject<{
|
|
44
|
+
limitKind: z.ZodEnum<{
|
|
45
|
+
monthly_overage_amount_limit: "monthly_overage_amount_limit";
|
|
46
|
+
internal_cost_limit: "internal_cost_limit";
|
|
47
|
+
internal_limit: "internal_limit";
|
|
48
|
+
operator_global_monthly_cost_limit: "operator_global_monthly_cost_limit";
|
|
49
|
+
}>;
|
|
50
|
+
meterKey: z.ZodString;
|
|
51
|
+
thresholdStage: z.ZodEnum<{
|
|
52
|
+
info: "info";
|
|
53
|
+
warning: "warning";
|
|
54
|
+
urgent: "urgent";
|
|
55
|
+
reached: "reached";
|
|
56
|
+
}>;
|
|
57
|
+
currentValue: z.ZodNumber;
|
|
58
|
+
limitValue: z.ZodNumber;
|
|
59
|
+
retryAfterAt: z.ZodNullable<z.ZodString>;
|
|
60
|
+
}, z.core.$strip>>;
|
|
61
|
+
retryAfterAt: z.ZodNullable<z.ZodString>;
|
|
62
|
+
}, z.core.$strip>>;
|
|
63
|
+
privacy_policy: z.ZodOptional<z.ZodObject<{
|
|
64
|
+
token: z.ZodString;
|
|
65
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
66
|
+
method: z.ZodString;
|
|
67
|
+
path_template: z.ZodString;
|
|
68
|
+
direction: z.ZodEnum<{
|
|
69
|
+
request: "request";
|
|
70
|
+
response: "response";
|
|
71
|
+
}>;
|
|
72
|
+
field_path: z.ZodString;
|
|
73
|
+
}, z.core.$strict>>;
|
|
74
|
+
}, z.core.$strict>>;
|
|
75
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
76
|
+
accepted: z.ZodLiteral<false>;
|
|
77
|
+
status: z.ZodEnum<{
|
|
78
|
+
rejected: "rejected";
|
|
79
|
+
failed: "failed";
|
|
80
|
+
}>;
|
|
81
|
+
reason: z.ZodString;
|
|
82
|
+
retry_strategy: z.ZodEnum<{
|
|
83
|
+
split_batch: "split_batch";
|
|
84
|
+
retry_same_batch: "retry_same_batch";
|
|
85
|
+
stop_and_report: "stop_and_report";
|
|
86
|
+
mark_failed: "mark_failed";
|
|
87
|
+
drop_event: "drop_event";
|
|
88
|
+
}>;
|
|
89
|
+
submitted_event_count: z.ZodOptional<z.ZodNumber>;
|
|
90
|
+
accepted_event_count: z.ZodOptional<z.ZodNumber>;
|
|
91
|
+
dropped_event_count: z.ZodOptional<z.ZodNumber>;
|
|
92
|
+
dropped_events: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
93
|
+
event_index: z.ZodNumber;
|
|
94
|
+
reason: z.ZodString;
|
|
95
|
+
source_event_payload_fingerprint: z.ZodNullable<z.ZodString>;
|
|
96
|
+
}, z.core.$strict>>>;
|
|
97
|
+
limits: z.ZodOptional<z.ZodObject<{
|
|
98
|
+
hard_max_payload_bytes: z.ZodNumber;
|
|
99
|
+
max_event_bytes: z.ZodNumber;
|
|
100
|
+
}, z.core.$strip>>;
|
|
101
|
+
}, z.core.$strip>], "accepted">;
|
|
102
|
+
export type IngestRetryStrategy = z.infer<typeof ingestRetryStrategySchema>;
|
|
103
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// 止める対象と上限の種類は正本から生成した部品を読みます。
|
|
2
|
+
import { COST_CAPABILITIES, LIMIT_KINDS } from "../_generated/usage-governor-tables.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const MAX_INGEST_EVENTS_PER_BATCH = 10000;
|
|
5
|
+
const ingestRetryStrategySchema = z.enum([
|
|
6
|
+
"split_batch",
|
|
7
|
+
"retry_same_batch",
|
|
8
|
+
"stop_and_report",
|
|
9
|
+
"mark_failed",
|
|
10
|
+
"drop_event",
|
|
11
|
+
]);
|
|
12
|
+
const ingestDroppedEventSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
event_index: z.number().int().nonnegative(),
|
|
15
|
+
reason: z.string().trim().min(1).max(128),
|
|
16
|
+
source_event_payload_fingerprint: z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
19
|
+
.nullable(),
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
const ingestUsageGovernorFeedbackSchema = z.object({
|
|
23
|
+
disabledCapabilities: z.array(z.enum(COST_CAPABILITIES)),
|
|
24
|
+
limitReasons: z.array(z.object({
|
|
25
|
+
limitKind: z.enum(LIMIT_KINDS),
|
|
26
|
+
meterKey: z.string().trim().min(1),
|
|
27
|
+
thresholdStage: z.enum(["info", "warning", "urgent", "reached"]),
|
|
28
|
+
currentValue: z.number().nonnegative(),
|
|
29
|
+
limitValue: z.number().nonnegative(),
|
|
30
|
+
retryAfterAt: z.string().datetime({ offset: true }).nullable(),
|
|
31
|
+
})),
|
|
32
|
+
retryAfterAt: z.string().datetime({ offset: true }).nullable(),
|
|
33
|
+
});
|
|
34
|
+
const boundedCount = z.number().int().nonnegative().max(MAX_INGEST_EVENTS_PER_BATCH);
|
|
35
|
+
const positiveCount = boundedCount.refine((count) => count > 0);
|
|
36
|
+
// 顧客が画面で平文にした項目。**受付側の合印と違う時だけ載ります。**
|
|
37
|
+
// 封筒の契約は受付側が正本 (`ingest-privacy-policy-delivery.schema.ts`)。この配る
|
|
38
|
+
// 部品は `@cluebase/schema` を実行時に持てない (= 顧客の bundle に入れない) ので、
|
|
39
|
+
// この file が受け取る形を持ちます。ここと正本がずれたら、届いた方針は形が
|
|
40
|
+
// 合わずに捨てられ、**全部伏せた側へ倒れます。**
|
|
41
|
+
export const ingestPrivacyPolicySchema = z
|
|
42
|
+
.object({
|
|
43
|
+
token: z.string().trim().min(1).max(200),
|
|
44
|
+
entries: z
|
|
45
|
+
.array(z
|
|
46
|
+
.object({
|
|
47
|
+
method: z.string().trim().min(1).max(200),
|
|
48
|
+
path_template: z.string().trim().min(1).max(1024),
|
|
49
|
+
direction: z.enum(["request", "response"]),
|
|
50
|
+
field_path: z.string().trim().min(1).max(1024),
|
|
51
|
+
})
|
|
52
|
+
.strict())
|
|
53
|
+
.max(500),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
const observationSourceEventBatchAcceptedResponseSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
accepted: z.literal(true),
|
|
59
|
+
observation_ingest_batch_id: z.string().trim().min(1),
|
|
60
|
+
status: z.literal("received"),
|
|
61
|
+
submitted_event_count: positiveCount,
|
|
62
|
+
accepted_event_count: positiveCount,
|
|
63
|
+
dropped_event_count: boundedCount,
|
|
64
|
+
dropped_events: z.array(ingestDroppedEventSchema).max(MAX_INGEST_EVENTS_PER_BATCH),
|
|
65
|
+
duplicate: z.boolean(),
|
|
66
|
+
governor: ingestUsageGovernorFeedbackSchema.optional(),
|
|
67
|
+
privacy_policy: ingestPrivacyPolicySchema.optional(),
|
|
68
|
+
})
|
|
69
|
+
.superRefine((value, ctx) => {
|
|
70
|
+
if (value.submitted_event_count !== value.accepted_event_count + value.dropped_event_count) {
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: z.ZodIssueCode.custom,
|
|
73
|
+
path: ["submitted_event_count"],
|
|
74
|
+
message: "submitted count must equal accepted plus dropped counts",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!value.duplicate || value.dropped_events.length > 0) {
|
|
78
|
+
validateDroppedEventDetails(value.submitted_event_count, value.dropped_event_count, value.dropped_events, ctx);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const observationSourceEventBatchRejectedResponseSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
accepted: z.literal(false),
|
|
84
|
+
status: z.enum(["rejected", "failed"]),
|
|
85
|
+
reason: z.string().trim().min(1),
|
|
86
|
+
retry_strategy: ingestRetryStrategySchema,
|
|
87
|
+
submitted_event_count: positiveCount.optional(),
|
|
88
|
+
accepted_event_count: boundedCount.optional(),
|
|
89
|
+
dropped_event_count: boundedCount.optional(),
|
|
90
|
+
dropped_events: z.array(ingestDroppedEventSchema).max(MAX_INGEST_EVENTS_PER_BATCH).optional(),
|
|
91
|
+
limits: z
|
|
92
|
+
.object({
|
|
93
|
+
hard_max_payload_bytes: z.number().int().positive(),
|
|
94
|
+
max_event_bytes: z.number().int().positive(),
|
|
95
|
+
})
|
|
96
|
+
.refine((limits) => limits.max_event_bytes <= limits.hard_max_payload_bytes)
|
|
97
|
+
.optional(),
|
|
98
|
+
})
|
|
99
|
+
.superRefine((value, ctx) => {
|
|
100
|
+
const counts = [value.submitted_event_count, value.accepted_event_count, value.dropped_event_count];
|
|
101
|
+
const hasAnyCount = counts.some((count) => count !== undefined);
|
|
102
|
+
const hasAllCounts = counts.every((count) => count !== undefined);
|
|
103
|
+
if (hasAnyCount !== hasAllCounts) {
|
|
104
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["submitted_event_count"], message: "rejected response counts must be all present or all absent" });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (hasAllCounts) {
|
|
108
|
+
const [submitted, accepted, dropped] = counts;
|
|
109
|
+
if (accepted > submitted || dropped > submitted || accepted + dropped > submitted) {
|
|
110
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dropped_event_count"], message: "rejected counts must not exceed submitted count" });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (value.retry_strategy === "drop_event") {
|
|
114
|
+
if (!hasAllCounts) {
|
|
115
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["submitted_event_count"], message: "drop rejection requires all counts" });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const [submitted, accepted, dropped] = counts;
|
|
119
|
+
if (accepted !== 0 || dropped !== submitted) {
|
|
120
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dropped_event_count"], message: "drop rejection must reject every submitted event" });
|
|
121
|
+
}
|
|
122
|
+
validateDroppedEventDetails(submitted, dropped, value.dropped_events ?? [], ctx);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if ((value.dropped_events?.length ?? 0) > 0) {
|
|
126
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dropped_events"], message: "non-drop rejection must not contain dropped event details" });
|
|
127
|
+
}
|
|
128
|
+
if ((value.retry_strategy === "split_batch") !== (value.limits !== undefined)) {
|
|
129
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["limits"], message: "byte limits are valid only for split rejection" });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
export const observationSourceEventBatchResponseSchema = z.discriminatedUnion("accepted", [
|
|
133
|
+
observationSourceEventBatchAcceptedResponseSchema,
|
|
134
|
+
observationSourceEventBatchRejectedResponseSchema,
|
|
135
|
+
]);
|
|
136
|
+
function validateDroppedEventDetails(submittedCount, droppedCount, droppedEvents, ctx) {
|
|
137
|
+
if (droppedEvents.length !== droppedCount) {
|
|
138
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dropped_events"], message: "dropped event details must match dropped count" });
|
|
139
|
+
}
|
|
140
|
+
const indexes = new Set();
|
|
141
|
+
for (const [detailIndex, detail] of droppedEvents.entries()) {
|
|
142
|
+
if (detail.event_index >= submittedCount || indexes.has(detail.event_index)) {
|
|
143
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dropped_events", detailIndex, "event_index"], message: "dropped event indexes must be valid and unique" });
|
|
144
|
+
}
|
|
145
|
+
indexes.add(detail.event_index);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type IngestRetryStrategy } from "./delivery-response-contract.js";
|
|
2
|
+
import type { IngestUsageGovernorFeedback } from "../provider/types.js";
|
|
3
|
+
export declare const BROWSER_REJECTION_STRATEGIES: Readonly<Record<string, IngestRetryStrategy>>;
|
|
4
|
+
export type { IngestRetryStrategy } from "./delivery-response-contract.js";
|
|
5
|
+
export type BrowserDeliveryDecision = {
|
|
6
|
+
readonly ok: boolean;
|
|
7
|
+
/** 受付側が返した方針。合印が同じなら来ない。 */
|
|
8
|
+
readonly privacyPolicy?: unknown;
|
|
9
|
+
readonly retryStrategy: IngestRetryStrategy | null;
|
|
10
|
+
readonly governor: IngestUsageGovernorFeedback | null;
|
|
11
|
+
readonly observationIngestBatchId?: string;
|
|
12
|
+
readonly droppedEventCount: number;
|
|
13
|
+
readonly acceptedEventCount: number;
|
|
14
|
+
readonly acceptedPartial: boolean;
|
|
15
|
+
readonly reason: string | null;
|
|
16
|
+
};
|
|
17
|
+
export declare const evaluateBrowserDeliveryResponse: (input: {
|
|
18
|
+
readonly status: number;
|
|
19
|
+
readonly bodyText: string;
|
|
20
|
+
readonly localSubmittedCount: number;
|
|
21
|
+
}) => BrowserDeliveryDecision;
|
|
22
|
+
/**
|
|
23
|
+
* Decide from the HTTP status alone whether a failed exchange with Cluebase is
|
|
24
|
+
* worth replaying.
|
|
25
|
+
*
|
|
26
|
+
* This is the single owner of that judgement. Batch delivery consults it when
|
|
27
|
+
* the body carries no usable verdict, and browser-token acquisition consults it
|
|
28
|
+
* because that endpoint carries no verdict at all. Without one owner the same
|
|
29
|
+
* status is permanent on one path and transient on the other, and a request
|
|
30
|
+
* that can never succeed is replayed until the customer's events expire.
|
|
31
|
+
*
|
|
32
|
+
* A status is only replayed when it says "the same request may succeed later".
|
|
33
|
+
* That is the rule the SDK's own core already applies — OpenTelemetry's OTLP
|
|
34
|
+
* exporter retries 429/502/503/504 and nothing else (`isExportRetryable`) —
|
|
35
|
+
* widened here to the statuses this ingest contract additionally defines.
|
|
36
|
+
*/
|
|
37
|
+
export declare const classifyIngestHttpStatus: (status: number) => IngestRetryStrategy;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { observationSourceEventBatchResponseSchema, } from "./delivery-response-contract.js";
|
|
2
|
+
import { parseGovernorFeedbackEnvelope } from "../provider/usage-governor.js";
|
|
3
|
+
export const BROWSER_REJECTION_STRATEGIES = {
|
|
4
|
+
invalid_request: "stop_and_report",
|
|
5
|
+
unsupported_source_schema_version: "stop_and_report",
|
|
6
|
+
missing_project_key: "stop_and_report",
|
|
7
|
+
project_key_mismatch: "stop_and_report",
|
|
8
|
+
unresolved_project_or_environment: "stop_and_report",
|
|
9
|
+
missing_browser_token: "stop_and_report",
|
|
10
|
+
invalid_browser_token: "stop_and_report",
|
|
11
|
+
expired_browser_token: "retry_same_batch",
|
|
12
|
+
stale_browser_token: "retry_same_batch",
|
|
13
|
+
origin_not_allowed: "stop_and_report",
|
|
14
|
+
authorization_denied: "stop_and_report",
|
|
15
|
+
idempotency_conflict: "mark_failed",
|
|
16
|
+
batch_too_large: "split_batch",
|
|
17
|
+
all_events_dropped: "drop_event",
|
|
18
|
+
usage_governor_denied: "retry_same_batch",
|
|
19
|
+
all_events_rejected_by_ingest_controls: "retry_same_batch",
|
|
20
|
+
route_throttled: "retry_same_batch",
|
|
21
|
+
raw_ingest_produce_failed: "retry_same_batch",
|
|
22
|
+
raw_ingest_ack_unknown: "retry_same_batch",
|
|
23
|
+
raw_ingest_receipt_metadata_failed: "retry_same_batch",
|
|
24
|
+
ingest_config_changed_retryable: "retry_same_batch",
|
|
25
|
+
usage_meter_unavailable: "retry_same_batch",
|
|
26
|
+
};
|
|
27
|
+
export const evaluateBrowserDeliveryResponse = (input) => {
|
|
28
|
+
const decoded = decode(input.bodyText);
|
|
29
|
+
if (decoded !== null && hasAcceptedDiscriminator(decoded)) {
|
|
30
|
+
const parsed = observationSourceEventBatchResponseSchema.safeParse(decoded);
|
|
31
|
+
if (parsed.success &&
|
|
32
|
+
submittedCountMatches(parsed.data, input.localSubmittedCount) &&
|
|
33
|
+
hasPublicDropDetails(parsed.data)) {
|
|
34
|
+
if (parsed.data.accepted) {
|
|
35
|
+
if (input.status < 200 || input.status >= 300) {
|
|
36
|
+
return invalidResponse();
|
|
37
|
+
}
|
|
38
|
+
const partial = parsed.data.dropped_event_count > 0;
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
retryStrategy: null,
|
|
42
|
+
privacyPolicy: parsed.data.privacy_policy,
|
|
43
|
+
governor: parseGovernorFeedbackEnvelope(parsed.data),
|
|
44
|
+
observationIngestBatchId: parsed.data.observation_ingest_batch_id,
|
|
45
|
+
droppedEventCount: parsed.data.dropped_event_count,
|
|
46
|
+
acceptedEventCount: parsed.data.accepted_event_count,
|
|
47
|
+
acceptedPartial: partial,
|
|
48
|
+
reason: partial ? "accepted_partial" : null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (BROWSER_REJECTION_STRATEGIES[parsed.data.reason] !==
|
|
52
|
+
parsed.data.retry_strategy) {
|
|
53
|
+
return invalidResponse();
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
retryStrategy: parsed.data.retry_strategy,
|
|
58
|
+
governor: null,
|
|
59
|
+
droppedEventCount: parsed.data.dropped_event_count ?? 0,
|
|
60
|
+
acceptedEventCount: parsed.data.accepted_event_count ?? 0,
|
|
61
|
+
acceptedPartial: false,
|
|
62
|
+
reason: parsed.data.reason,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return invalidResponse();
|
|
66
|
+
}
|
|
67
|
+
return fallback(input.status);
|
|
68
|
+
};
|
|
69
|
+
const submittedCountMatches = (value, localSubmittedCount) => value.submitted_event_count === undefined ||
|
|
70
|
+
value.submitted_event_count === localSubmittedCount;
|
|
71
|
+
const hasPublicDropDetails = (value) => (value.dropped_events ?? []).every((detail) => detail.source_event_payload_fingerprint === null);
|
|
72
|
+
const decode = (bodyText) => {
|
|
73
|
+
if (!bodyText.trim())
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(bodyText);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const hasAcceptedDiscriminator = (value) => value !== null &&
|
|
83
|
+
typeof value === "object" &&
|
|
84
|
+
!Array.isArray(value) &&
|
|
85
|
+
Object.prototype.hasOwnProperty.call(value, "accepted");
|
|
86
|
+
const invalidResponse = () => ({
|
|
87
|
+
ok: false,
|
|
88
|
+
retryStrategy: "retry_same_batch",
|
|
89
|
+
governor: null,
|
|
90
|
+
droppedEventCount: 0,
|
|
91
|
+
acceptedEventCount: 0,
|
|
92
|
+
acceptedPartial: false,
|
|
93
|
+
reason: "invalid_response",
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* Decide from the HTTP status alone whether a failed exchange with Cluebase is
|
|
97
|
+
* worth replaying.
|
|
98
|
+
*
|
|
99
|
+
* This is the single owner of that judgement. Batch delivery consults it when
|
|
100
|
+
* the body carries no usable verdict, and browser-token acquisition consults it
|
|
101
|
+
* because that endpoint carries no verdict at all. Without one owner the same
|
|
102
|
+
* status is permanent on one path and transient on the other, and a request
|
|
103
|
+
* that can never succeed is replayed until the customer's events expire.
|
|
104
|
+
*
|
|
105
|
+
* A status is only replayed when it says "the same request may succeed later".
|
|
106
|
+
* That is the rule the SDK's own core already applies — OpenTelemetry's OTLP
|
|
107
|
+
* exporter retries 429/502/503/504 and nothing else (`isExportRetryable`) —
|
|
108
|
+
* widened here to the statuses this ingest contract additionally defines.
|
|
109
|
+
*/
|
|
110
|
+
export const classifyIngestHttpStatus = (status) => {
|
|
111
|
+
// A 2xx that got this far means the body did not match the contract. That can
|
|
112
|
+
// be a transient rewrite in transit, so it stays replayable.
|
|
113
|
+
if (status >= 200 && status < 300)
|
|
114
|
+
return "retry_same_batch";
|
|
115
|
+
if (status === 409)
|
|
116
|
+
return "mark_failed";
|
|
117
|
+
if (status === 413)
|
|
118
|
+
return "split_batch";
|
|
119
|
+
// Deterministic refusals: the request is wrong, unauthorized, or aimed at
|
|
120
|
+
// something that is not there. Replaying it changes nothing.
|
|
121
|
+
if ([400, 401, 403, 404, 405, 406, 410, 415].includes(status)) {
|
|
122
|
+
return "stop_and_report";
|
|
123
|
+
}
|
|
124
|
+
return "retry_same_batch";
|
|
125
|
+
};
|
|
126
|
+
const fallback = (status) => {
|
|
127
|
+
const retryStrategy = classifyIngestHttpStatus(status);
|
|
128
|
+
if (status >= 200 && status < 300)
|
|
129
|
+
return invalidResponse();
|
|
130
|
+
if (retryStrategy === "retry_same_batch")
|
|
131
|
+
return retry(status);
|
|
132
|
+
return terminal(retryStrategy, status);
|
|
133
|
+
};
|
|
134
|
+
const retry = (status) => ({
|
|
135
|
+
ok: false,
|
|
136
|
+
retryStrategy: "retry_same_batch",
|
|
137
|
+
governor: null,
|
|
138
|
+
droppedEventCount: 0,
|
|
139
|
+
acceptedEventCount: 0,
|
|
140
|
+
acceptedPartial: false,
|
|
141
|
+
reason: `http_${status}`,
|
|
142
|
+
});
|
|
143
|
+
const terminal = (retryStrategy, status) => ({
|
|
144
|
+
ok: false,
|
|
145
|
+
retryStrategy,
|
|
146
|
+
governor: null,
|
|
147
|
+
droppedEventCount: 0,
|
|
148
|
+
acceptedEventCount: 0,
|
|
149
|
+
acceptedPartial: false,
|
|
150
|
+
reason: `http_${status}`,
|
|
151
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IngestRetryStrategy } from "./delivery-response.js";
|
|
2
|
+
export type DiagnosticCode = "batch_split" | "consent_persistence_failed" | "consent_purge" | "consent_purge_failed" | "consent_state_unavailable" | "durable_outbox_unavailable" | "duplicate_drop_details_unavailable" | "event_oversize" | "intentional_drop" | "invalid_response" | "legacy_pre_revocation_event_blocked" | "legacy_bridge_capacity_reached" | "legacy_writer_detected" | "non_canonical_success_status" | "outbox_key_collision" | "outbox_record_corrupt" | "protocol_redirect" | "queue_overflow" | "retry_exhausted" | "retry_scheduled" | "shutdown_failure" | "terminal_rejection" | "unknown_status";
|
|
3
|
+
export type DiagnosticLifecycle = "open" | "closing" | "closing_failed" | "closed";
|
|
4
|
+
export type DiagnosticFlushOutcome = "clean" | "failed" | null;
|
|
5
|
+
export type DiagnosticCloseOutcome = "clean" | "retryable_failure" | "terminal_failure" | null;
|
|
6
|
+
export type DiagnosticRecentEntry = {
|
|
7
|
+
readonly occurred_at: string;
|
|
8
|
+
readonly code: DiagnosticCode;
|
|
9
|
+
readonly strategy?: IngestRetryStrategy;
|
|
10
|
+
readonly http_status?: number;
|
|
11
|
+
readonly reason?: string;
|
|
12
|
+
readonly attempt?: number;
|
|
13
|
+
readonly count?: number;
|
|
14
|
+
readonly batch_fingerprint?: string;
|
|
15
|
+
readonly event_fingerprint?: string;
|
|
16
|
+
};
|
|
17
|
+
export type DiagnosticsSnapshot = {
|
|
18
|
+
readonly schema_version: 1;
|
|
19
|
+
readonly lifecycle: DiagnosticLifecycle;
|
|
20
|
+
readonly last_flush_outcome: DiagnosticFlushOutcome;
|
|
21
|
+
readonly last_close_outcome: DiagnosticCloseOutcome;
|
|
22
|
+
readonly counters: Readonly<Record<DiagnosticCode, number>>;
|
|
23
|
+
readonly recent: readonly DiagnosticRecentEntry[];
|
|
24
|
+
};
|
|
25
|
+
export declare class DiagnosticStore {
|
|
26
|
+
private key;
|
|
27
|
+
private lifecycle;
|
|
28
|
+
private lastFlushOutcome;
|
|
29
|
+
private lastCloseOutcome;
|
|
30
|
+
private readonly counters;
|
|
31
|
+
private recent;
|
|
32
|
+
constructor(key?: Uint8Array);
|
|
33
|
+
record(entry: Omit<DiagnosticRecentEntry, "occurred_at">): void;
|
|
34
|
+
setLifecycle(value: DiagnosticLifecycle): void;
|
|
35
|
+
setFlushOutcome(value: DiagnosticFlushOutcome): void;
|
|
36
|
+
setCloseOutcome(value: DiagnosticCloseOutcome): void;
|
|
37
|
+
rotateForConsentPurge(count: number): void;
|
|
38
|
+
batchFingerprint(ownedCanonicalBytes: string): string;
|
|
39
|
+
eventFingerprint(ownedCanonicalBytes: string): string;
|
|
40
|
+
snapshot(): DiagnosticsSnapshot;
|
|
41
|
+
private fingerprint;
|
|
42
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { hmac } from "@noble/hashes/hmac";
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
3
|
+
import { bytesToHex, randomBytes, utf8ToBytes } from "@noble/hashes/utils";
|
|
4
|
+
const CODES = [
|
|
5
|
+
"batch_split",
|
|
6
|
+
"consent_persistence_failed",
|
|
7
|
+
"consent_purge",
|
|
8
|
+
"consent_purge_failed",
|
|
9
|
+
"consent_state_unavailable",
|
|
10
|
+
"durable_outbox_unavailable",
|
|
11
|
+
"duplicate_drop_details_unavailable",
|
|
12
|
+
"event_oversize",
|
|
13
|
+
"intentional_drop",
|
|
14
|
+
"invalid_response",
|
|
15
|
+
"legacy_pre_revocation_event_blocked",
|
|
16
|
+
"legacy_bridge_capacity_reached",
|
|
17
|
+
"legacy_writer_detected",
|
|
18
|
+
"non_canonical_success_status",
|
|
19
|
+
"outbox_key_collision",
|
|
20
|
+
"outbox_record_corrupt",
|
|
21
|
+
"protocol_redirect",
|
|
22
|
+
"queue_overflow",
|
|
23
|
+
"retry_exhausted",
|
|
24
|
+
"retry_scheduled",
|
|
25
|
+
"shutdown_failure",
|
|
26
|
+
"terminal_rejection",
|
|
27
|
+
"unknown_status",
|
|
28
|
+
];
|
|
29
|
+
const CODE_SET = new Set(CODES);
|
|
30
|
+
const STRATEGIES = new Set([
|
|
31
|
+
"split_batch",
|
|
32
|
+
"retry_same_batch",
|
|
33
|
+
"stop_and_report",
|
|
34
|
+
"mark_failed",
|
|
35
|
+
"drop_event",
|
|
36
|
+
]);
|
|
37
|
+
const SAFE_REASONS = new Set([
|
|
38
|
+
"all_events_dropped",
|
|
39
|
+
"all_events_rejected_by_ingest_controls",
|
|
40
|
+
"authorization_denied",
|
|
41
|
+
"batch_too_large",
|
|
42
|
+
"expired_browser_token",
|
|
43
|
+
"idempotency_conflict",
|
|
44
|
+
"invalid_api_key",
|
|
45
|
+
"invalid_browser_token",
|
|
46
|
+
"invalid_request",
|
|
47
|
+
"missing_browser_token",
|
|
48
|
+
"missing_project_key",
|
|
49
|
+
"origin_not_allowed",
|
|
50
|
+
"project_key_mismatch",
|
|
51
|
+
"raw_ingest_ack_unknown",
|
|
52
|
+
"raw_ingest_produce_failed",
|
|
53
|
+
"raw_ingest_receipt_metadata_failed",
|
|
54
|
+
"route_throttled",
|
|
55
|
+
"stale_browser_token",
|
|
56
|
+
"unresolved_project_or_environment",
|
|
57
|
+
"unsupported_source_schema_version",
|
|
58
|
+
"usage_governor_denied",
|
|
59
|
+
"usage_meter_unavailable",
|
|
60
|
+
]);
|
|
61
|
+
const FINGERPRINT = /^diag_hmac_v1_[0-9a-f]{64}$/;
|
|
62
|
+
const RECENT_LIMIT = 100;
|
|
63
|
+
export class DiagnosticStore {
|
|
64
|
+
constructor(key = randomBytes(32)) {
|
|
65
|
+
this.lifecycle = "open";
|
|
66
|
+
this.lastFlushOutcome = null;
|
|
67
|
+
this.lastCloseOutcome = null;
|
|
68
|
+
this.counters = Object.fromEntries(CODES.map((code) => [code, 0]));
|
|
69
|
+
this.recent = [];
|
|
70
|
+
if (key.byteLength !== 32)
|
|
71
|
+
throw new Error("diagnostic_key_invalid");
|
|
72
|
+
this.key = new Uint8Array(key);
|
|
73
|
+
}
|
|
74
|
+
record(entry) {
|
|
75
|
+
const safe = normalize(entry);
|
|
76
|
+
if (!safe)
|
|
77
|
+
return;
|
|
78
|
+
this.counters[safe.code] += safe.count ?? 1;
|
|
79
|
+
this.recent = [...this.recent.slice(-(RECENT_LIMIT - 1)), safe];
|
|
80
|
+
}
|
|
81
|
+
setLifecycle(value) {
|
|
82
|
+
this.lifecycle = value;
|
|
83
|
+
}
|
|
84
|
+
setFlushOutcome(value) {
|
|
85
|
+
this.lastFlushOutcome = value;
|
|
86
|
+
}
|
|
87
|
+
setCloseOutcome(value) {
|
|
88
|
+
this.lastCloseOutcome = value;
|
|
89
|
+
}
|
|
90
|
+
rotateForConsentPurge(count) {
|
|
91
|
+
this.key.fill(0);
|
|
92
|
+
this.key = randomBytes(32);
|
|
93
|
+
this.recent = [];
|
|
94
|
+
if (count > 0)
|
|
95
|
+
this.record({ code: "consent_purge", count });
|
|
96
|
+
}
|
|
97
|
+
batchFingerprint(ownedCanonicalBytes) {
|
|
98
|
+
return this.fingerprint("batch", ownedCanonicalBytes);
|
|
99
|
+
}
|
|
100
|
+
eventFingerprint(ownedCanonicalBytes) {
|
|
101
|
+
return this.fingerprint("event", ownedCanonicalBytes);
|
|
102
|
+
}
|
|
103
|
+
snapshot() {
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
schema_version: 1,
|
|
106
|
+
lifecycle: this.lifecycle,
|
|
107
|
+
last_flush_outcome: this.lastFlushOutcome,
|
|
108
|
+
last_close_outcome: this.lastCloseOutcome,
|
|
109
|
+
counters: Object.freeze({ ...this.counters }),
|
|
110
|
+
recent: Object.freeze(this.recent.map((entry) => Object.freeze({ ...entry }))),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
fingerprint(domain, value) {
|
|
114
|
+
return `diag_hmac_v1_${bytesToHex(hmac(sha256, this.key, utf8ToBytes(`${domain}\0${value}`)))}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const normalize = (input) => {
|
|
118
|
+
if (!CODE_SET.has(input.code))
|
|
119
|
+
return null;
|
|
120
|
+
const entry = { occurred_at: new Date().toISOString(), code: input.code };
|
|
121
|
+
if (input.strategy && STRATEGIES.has(input.strategy)) {
|
|
122
|
+
entry.strategy = input.strategy;
|
|
123
|
+
}
|
|
124
|
+
const httpStatus = input.http_status;
|
|
125
|
+
if (typeof httpStatus === "number" &&
|
|
126
|
+
Number.isInteger(httpStatus) &&
|
|
127
|
+
httpStatus >= 100 &&
|
|
128
|
+
httpStatus <= 599) {
|
|
129
|
+
entry.http_status = httpStatus;
|
|
130
|
+
}
|
|
131
|
+
if (input.reason && SAFE_REASONS.has(input.reason))
|
|
132
|
+
entry.reason = input.reason;
|
|
133
|
+
const attempt = input.attempt;
|
|
134
|
+
if (typeof attempt === "number" && Number.isSafeInteger(attempt) && attempt > 0) {
|
|
135
|
+
entry.attempt = attempt;
|
|
136
|
+
}
|
|
137
|
+
const count = input.count;
|
|
138
|
+
if (typeof count === "number" && Number.isSafeInteger(count) && count >= 0) {
|
|
139
|
+
entry.count = count;
|
|
140
|
+
}
|
|
141
|
+
if (input.batch_fingerprint && FINGERPRINT.test(input.batch_fingerprint)) {
|
|
142
|
+
entry.batch_fingerprint = input.batch_fingerprint;
|
|
143
|
+
}
|
|
144
|
+
if (input.event_fingerprint && FINGERPRINT.test(input.event_fingerprint)) {
|
|
145
|
+
entry.event_fingerprint = input.event_fingerprint;
|
|
146
|
+
}
|
|
147
|
+
return Object.freeze(entry);
|
|
148
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const generateMonotonicId: (prefix?: string) => string;
|
|
2
|
+
export declare const generateTraceId: () => string;
|
|
3
|
+
export declare const generateEventId: () => string;
|
|
4
|
+
export declare const generateAnonymousId: () => string;
|
|
5
|
+
export declare const generateSessionId: () => string;
|
|
6
|
+
export declare const generateTabId: () => string;
|
|
7
|
+
export declare const generateRequestId: () => string;
|
|
8
|
+
export declare const generateRequestSpanId: () => string;
|
|
9
|
+
export declare const generateInteractionId: () => string;
|
|
10
|
+
export declare const generateViewId: () => string;
|