@obsunified/collector 2.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/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-UGWDOGGH.js +319 -0
- package/dist/chunk-UGWDOGGH.js.map +1 -0
- package/dist/chunk-XKQTVIRE.js +671 -0
- package/dist/chunk-XKQTVIRE.js.map +1 -0
- package/dist/index.d.ts +1197 -0
- package/dist/index.js +17707 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
- package/dist/openai-FJYOQ7WO.js +384 -0
- package/dist/openai-FJYOQ7WO.js.map +1 -0
- package/package.json +73 -0
- package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
- package/src/analyses/derive.ts +642 -0
- package/src/analyses/index.ts +50 -0
- package/src/analyses/investigations.ts +277 -0
- package/src/analyses/tier0.ts +568 -0
- package/src/auth/dashboard-auth.ts +269 -0
- package/src/auth/ingest-auth.ts +147 -0
- package/src/durable-objects/tail-hub.test.ts +32 -0
- package/src/durable-objects/tail-hub.ts +158 -0
- package/src/framework/collector.ts +467 -0
- package/src/framework/env.ts +90 -0
- package/src/framework/logger.ts +81 -0
- package/src/index.ts +211 -0
- package/src/lib/action-aggregates.ts +1031 -0
- package/src/lib/action-identity.ts +106 -0
- package/src/lib/actions-store.ts +397 -0
- package/src/lib/ai-pricing.ts +80 -0
- package/src/lib/ai-store/calls.ts +148 -0
- package/src/lib/ai-store/evaluations.test.ts +47 -0
- package/src/lib/ai-store/evaluations.ts +125 -0
- package/src/lib/ai-store/helpers.ts +37 -0
- package/src/lib/ai-store/retention.ts +17 -0
- package/src/lib/ai-store/sessions.ts +277 -0
- package/src/lib/ai-store/spans.ts +109 -0
- package/src/lib/ai-store/types.ts +112 -0
- package/src/lib/ai-store.ts +76 -0
- package/src/lib/alerts-store.ts +458 -0
- package/src/lib/analyses-runner.ts +439 -0
- package/src/lib/analyses-store.test.ts +243 -0
- package/src/lib/analyses-store.ts +454 -0
- package/src/lib/ask.test.ts +302 -0
- package/src/lib/ask.ts +390 -0
- package/src/lib/blob-store-s3.ts +213 -0
- package/src/lib/blob-store.ts +366 -0
- package/src/lib/eval-cases-store.test.ts +171 -0
- package/src/lib/eval-cases-store.ts +1033 -0
- package/src/lib/evidence-references.test.ts +10 -0
- package/src/lib/evidence-references.ts +684 -0
- package/src/lib/hash.ts +26 -0
- package/src/lib/identity-index/action-lookups.ts +805 -0
- package/src/lib/identity-index/constants.ts +1 -0
- package/src/lib/identity-index/key-lookups.ts +199 -0
- package/src/lib/identity-index/mappers.ts +349 -0
- package/src/lib/identity-index/types.ts +196 -0
- package/src/lib/identity-index/user-lookup.ts +137 -0
- package/src/lib/identity-index.test.ts +624 -0
- package/src/lib/identity-index.ts +85 -0
- package/src/lib/json.ts +39 -0
- package/src/lib/llm.ts +263 -0
- package/src/lib/logs-store.ts +188 -0
- package/src/lib/metrics-store.test.ts +189 -0
- package/src/lib/metrics-store.ts +312 -0
- package/src/lib/narrate-gate.test.ts +260 -0
- package/src/lib/narrate-gate.ts +253 -0
- package/src/lib/openai.test.ts +300 -0
- package/src/lib/openai.ts +512 -0
- package/src/lib/otlp.ts +212 -0
- package/src/lib/parse-pprof.test.ts +64 -0
- package/src/lib/parse-pprof.ts +184 -0
- package/src/lib/payload-capture.ts +22 -0
- package/src/lib/projects-store.ts +207 -0
- package/src/lib/propagation-metric.test.ts +196 -0
- package/src/lib/propagation-metric.ts +175 -0
- package/src/lib/sql-db-dialect.test.ts +22 -0
- package/src/lib/sql-db-postgres.ts +229 -0
- package/src/lib/sql-db.ts +166 -0
- package/src/lib/stage6.test.ts +216 -0
- package/src/lib/store/export.ts +65 -0
- package/src/lib/store/helpers.ts +367 -0
- package/src/lib/store/ingest.ts +68 -0
- package/src/lib/store/issues.ts +158 -0
- package/src/lib/store/overview.ts +127 -0
- package/src/lib/store/retention.ts +10 -0
- package/src/lib/store/service-map.ts +384 -0
- package/src/lib/store/trace-detail.bench.ts +76 -0
- package/src/lib/store/trace-detail.test.ts +410 -0
- package/src/lib/store/trace-detail.ts +427 -0
- package/src/lib/store/trace-queries.test.ts +29 -0
- package/src/lib/store/trace-queries.ts +89 -0
- package/src/lib/store-overview.test.ts +77 -0
- package/src/lib/store.ts +96 -0
- package/src/lib/tail-publisher.ts +64 -0
- package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
- package/src/lib/test-utils/mem-sql-db.ts +86 -0
- package/src/lib/ua-parser.ts +61 -0
- package/src/lib/usage-store.ts +567 -0
- package/src/migrations/001_telemetry_spans.sql +28 -0
- package/src/migrations/002_usage_events.sql +24 -0
- package/src/migrations/003_usage_analytics_columns.sql +13 -0
- package/src/migrations/004_logs.sql +19 -0
- package/src/migrations/005_ai_calls.sql +25 -0
- package/src/migrations/006_user_profiles.sql +11 -0
- package/src/migrations/007_session_replay_metadata.sql +10 -0
- package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
- package/src/migrations/009_projects_and_keys.sql +22 -0
- package/src/migrations/010_signal_project_id.sql +15 -0
- package/src/migrations/011_default_project_seed.sql +3 -0
- package/src/migrations/012_alert_rules.sql +17 -0
- package/src/migrations/013_alert_evaluations.sql +13 -0
- package/src/migrations/014_alert_state.sql +9 -0
- package/src/migrations/015_metrics.sql +58 -0
- package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
- package/src/migrations/017_span_dropped_counts.sql +11 -0
- package/src/migrations/018_log_flags.sql +8 -0
- package/src/migrations/019_ai_span_payloads.sql +30 -0
- package/src/migrations/020_ai_span_evaluations.sql +33 -0
- package/src/migrations/021_ai_span_session.sql +13 -0
- package/src/migrations/022_span_log_session_id.sql +13 -0
- package/src/migrations/023_service_map_indexes.sql +32 -0
- package/src/migrations/024_analysis_results.sql +49 -0
- package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
- package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
- package/src/migrations/027_identity_propagation.sql +50 -0
- package/src/migrations/028_profile_blobs.sql +57 -0
- package/src/migrations/029_telemetry_sdk_name.sql +20 -0
- package/src/migrations/030_analysis_claim.sql +18 -0
- package/src/migrations/031_agent_action_graph.sql +154 -0
- package/src/migrations/032_payload_capture_flags.sql +8 -0
- package/src/migrations/033_eval_cases.sql +52 -0
- package/src/migrations/034_eval_case_results.sql +23 -0
- package/src/migrations/035_metric_exemplars.sql +33 -0
- package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations/037_eval_runs.sql +37 -0
- package/src/migrations/038_tool_call_evidence.sql +11 -0
- package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
- package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
- package/src/migrations-postgres/002_usage_events.sql +24 -0
- package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
- package/src/migrations-postgres/004_logs.sql +19 -0
- package/src/migrations-postgres/005_ai_calls.sql +25 -0
- package/src/migrations-postgres/006_user_profiles.sql +11 -0
- package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
- package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
- package/src/migrations-postgres/010_signal_project_id.sql +18 -0
- package/src/migrations-postgres/011_default_project_seed.sql +5 -0
- package/src/migrations-postgres/012_alert_rules.sql +17 -0
- package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
- package/src/migrations-postgres/014_alert_state.sql +9 -0
- package/src/migrations-postgres/015_metrics.sql +47 -0
- package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
- package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
- package/src/migrations-postgres/018_log_flags.sql +2 -0
- package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
- package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
- package/src/migrations-postgres/021_ai_span_session.sql +7 -0
- package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
- package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
- package/src/migrations-postgres/024_analysis_results.sql +41 -0
- package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
- package/src/migrations-postgres/027_identity_propagation.sql +27 -0
- package/src/migrations-postgres/028_profile_blobs.sql +48 -0
- package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
- package/src/migrations-postgres/030_analysis_claim.sql +5 -0
- package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
- package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
- package/src/migrations-postgres/033_eval_cases.sql +55 -0
- package/src/migrations-postgres/034_eval_case_results.sql +26 -0
- package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
- package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations-postgres/037_eval_runs.sql +40 -0
- package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
- package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
- package/src/migrations-postgres/README.md +40 -0
- package/src/otlp/acceptance.test.ts +373 -0
- package/src/otlp/decode/body.ts +127 -0
- package/src/otlp/decode/logs.ts +96 -0
- package/src/otlp/decode/metrics.ts +402 -0
- package/src/otlp/decode/traces.ts +92 -0
- package/src/otlp/decode/values.ts +150 -0
- package/src/otlp/decode.test.ts +800 -0
- package/src/otlp/decode.ts +11 -0
- package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
- package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
- package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
- package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
- package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
- package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
- package/src/otlp/response.test.ts +158 -0
- package/src/otlp/response.ts +129 -0
- package/src/plugins/_context.ts +15 -0
- package/src/plugins/action-graph-processor/enrichers.ts +45 -0
- package/src/plugins/action-graph-processor/redaction.ts +84 -0
- package/src/plugins/action-graph-processor.test.ts +1023 -0
- package/src/plugins/action-graph-processor.ts +1172 -0
- package/src/plugins/action-routes.test.ts +716 -0
- package/src/plugins/action-routes.ts +343 -0
- package/src/plugins/ai-receiver.ts +254 -0
- package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
- package/src/plugins/ai-span-payloads-processor.ts +180 -0
- package/src/plugins/alerts-evaluator.test.ts +91 -0
- package/src/plugins/alerts-evaluator.ts +328 -0
- package/src/plugins/alerts-routes.ts +101 -0
- package/src/plugins/analyses-routes.ts +138 -0
- package/src/plugins/ask-routes.ts +182 -0
- package/src/plugins/bot-filter.ts +19 -0
- package/src/plugins/connected-routes/manifest.ts +531 -0
- package/src/plugins/connected-routes.test.ts +1791 -0
- package/src/plugins/connected-routes.ts +1103 -0
- package/src/plugins/dashboard-routes.ts +41 -0
- package/src/plugins/default-span-enrichment.ts +92 -0
- package/src/plugins/eval-cases-routes.test.ts +391 -0
- package/src/plugins/eval-cases-routes.ts +529 -0
- package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
- package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
- package/src/plugins/evidence-retrieval-routes.ts +2179 -0
- package/src/plugins/gen-ai-normalizer.test.ts +680 -0
- package/src/plugins/gen-ai-normalizer.ts +302 -0
- package/src/plugins/identity-receiver.ts +96 -0
- package/src/plugins/issue-enrichment.ts +132 -0
- package/src/plugins/issue-insights.ts +81 -0
- package/src/plugins/logs-receiver.ts +160 -0
- package/src/plugins/metrics-receiver.ts +91 -0
- package/src/plugins/onboarding-routes.ts +54 -0
- package/src/plugins/otlp-receiver.ts +124 -0
- package/src/plugins/platform-routes.ts +165 -0
- package/src/plugins/profile-routes.ts +468 -0
- package/src/plugins/projects-routes.ts +92 -0
- package/src/plugins/query-routes.ts +220 -0
- package/src/plugins/redaction-processor.ts +132 -0
- package/src/plugins/replay-query-routes.test.ts +38 -0
- package/src/plugins/replay-query-routes.ts +199 -0
- package/src/plugins/replay-receiver.ts +81 -0
- package/src/plugins/tail-routes.test.ts +47 -0
- package/src/plugins/tail-routes.ts +42 -0
- package/src/plugins/timeline-routes.ts +244 -0
- package/src/plugins/ua-enrichment.ts +27 -0
- package/src/plugins/usage-normalization.ts +46 -0
- package/src/plugins/usage-privacy.ts +55 -0
- package/src/plugins/usage-query-routes.ts +102 -0
- package/src/plugins/usage-receiver.ts +129 -0
- package/src/plugins/users-query-routes.ts +78 -0
- package/src/plugins/utm-enrichment.ts +36 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3-compatible adapter for `BlobStore`. Works against AWS S3, MinIO,
|
|
3
|
+
* Cloudflare R2's S3 API, Backblaze B2, etc.
|
|
4
|
+
*
|
|
5
|
+
* Uses `@aws-sdk/client-s3` v3. The adapter is intentionally thin —
|
|
6
|
+
* just enough to satisfy `BlobStore` for replay + pprof payloads. We
|
|
7
|
+
* deliberately do NOT expose pre-signed URLs, ACLs, or storage-class
|
|
8
|
+
* tuning — those belong in a richer storage adapter for users that
|
|
9
|
+
* outgrow the default.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
BlobListOptions,
|
|
14
|
+
BlobListResult,
|
|
15
|
+
BlobObject,
|
|
16
|
+
BlobPutOptions,
|
|
17
|
+
BlobStore,
|
|
18
|
+
} from "./blob-store";
|
|
19
|
+
|
|
20
|
+
type S3Body = Uint8Array | ArrayBuffer | ReadableStream<Uint8Array>;
|
|
21
|
+
type S3CommandBody = Exclude<S3Body, ArrayBuffer>;
|
|
22
|
+
type S3ResponseBody =
|
|
23
|
+
| ReadableStream<Uint8Array>
|
|
24
|
+
| AsyncIterable<Uint8Array | ArrayBuffer | ArrayBufferView | string>
|
|
25
|
+
| {
|
|
26
|
+
transformToWebStream(): ReadableStream<Uint8Array>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface S3ClientShape {
|
|
30
|
+
send(command: unknown): Promise<unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface S3SdkConstructors {
|
|
34
|
+
// Loose typing so the consumer can pass the AWS SDK's CommandInput types
|
|
35
|
+
// without the BlobStore adapter dragging in a hard dep on
|
|
36
|
+
// @aws-sdk/client-s3. The runtime contract is just "construct with an
|
|
37
|
+
// object literal of S3 params."
|
|
38
|
+
PutObjectCommand: new (input: {
|
|
39
|
+
Bucket: string;
|
|
40
|
+
Key: string;
|
|
41
|
+
Body?: S3CommandBody;
|
|
42
|
+
ContentType?: string;
|
|
43
|
+
ContentEncoding?: string;
|
|
44
|
+
Metadata?: Record<string, string>;
|
|
45
|
+
}) => unknown;
|
|
46
|
+
GetObjectCommand: new (input: { Bucket: string; Key: string }) => unknown;
|
|
47
|
+
DeleteObjectCommand: new (input: { Bucket: string; Key: string }) => unknown;
|
|
48
|
+
ListObjectsV2Command: new (input: {
|
|
49
|
+
Bucket: string;
|
|
50
|
+
Prefix: string;
|
|
51
|
+
MaxKeys?: number;
|
|
52
|
+
ContinuationToken?: string;
|
|
53
|
+
}) => unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface S3BlobStoreOptions {
|
|
57
|
+
client: S3ClientShape;
|
|
58
|
+
commands: S3SdkConstructors;
|
|
59
|
+
bucket: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class S3BlobStore implements BlobStore {
|
|
63
|
+
constructor(private readonly opts: S3BlobStoreOptions) {}
|
|
64
|
+
|
|
65
|
+
async put(
|
|
66
|
+
key: string,
|
|
67
|
+
body: S3Body,
|
|
68
|
+
options?: BlobPutOptions,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const commandBody: S3CommandBody =
|
|
71
|
+
body instanceof ArrayBuffer ? new Uint8Array(body) : body;
|
|
72
|
+
const cmd = new this.opts.commands.PutObjectCommand({
|
|
73
|
+
Bucket: this.opts.bucket,
|
|
74
|
+
Key: key,
|
|
75
|
+
Body: commandBody,
|
|
76
|
+
ContentType: options?.httpMetadata?.contentType,
|
|
77
|
+
ContentEncoding: options?.httpMetadata?.contentEncoding,
|
|
78
|
+
Metadata: options?.customMetadata,
|
|
79
|
+
});
|
|
80
|
+
await this.opts.client.send(cmd);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async get(key: string): Promise<BlobObject | null> {
|
|
84
|
+
const cmd = new this.opts.commands.GetObjectCommand({
|
|
85
|
+
Bucket: this.opts.bucket,
|
|
86
|
+
Key: key,
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
const r = (await this.opts.client.send(cmd)) as {
|
|
90
|
+
Body: S3ResponseBody;
|
|
91
|
+
ContentLength?: number;
|
|
92
|
+
ContentType?: string;
|
|
93
|
+
ContentEncoding?: string;
|
|
94
|
+
Metadata?: Record<string, string>;
|
|
95
|
+
};
|
|
96
|
+
const body = toWebReadableStream(r.Body);
|
|
97
|
+
return {
|
|
98
|
+
body,
|
|
99
|
+
size: r.ContentLength,
|
|
100
|
+
bytes: () => streamToBytes(body),
|
|
101
|
+
httpMetadata: {
|
|
102
|
+
contentType: r.ContentType,
|
|
103
|
+
contentEncoding: r.ContentEncoding,
|
|
104
|
+
},
|
|
105
|
+
customMetadata: r.Metadata,
|
|
106
|
+
};
|
|
107
|
+
} catch (err: unknown) {
|
|
108
|
+
if (isNotFound(err)) return null;
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async delete(key: string): Promise<void> {
|
|
114
|
+
const cmd = new this.opts.commands.DeleteObjectCommand({
|
|
115
|
+
Bucket: this.opts.bucket,
|
|
116
|
+
Key: key,
|
|
117
|
+
});
|
|
118
|
+
await this.opts.client.send(cmd);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async list(
|
|
122
|
+
prefix: string,
|
|
123
|
+
options?: BlobListOptions,
|
|
124
|
+
): Promise<BlobListResult> {
|
|
125
|
+
const cmd = new this.opts.commands.ListObjectsV2Command({
|
|
126
|
+
Bucket: this.opts.bucket,
|
|
127
|
+
Prefix: prefix,
|
|
128
|
+
MaxKeys: options?.limit,
|
|
129
|
+
ContinuationToken: options?.cursor,
|
|
130
|
+
});
|
|
131
|
+
const r = (await this.opts.client.send(cmd)) as {
|
|
132
|
+
Contents?: Array<{ Key?: string; Size?: number; LastModified?: Date }>;
|
|
133
|
+
NextContinuationToken?: string;
|
|
134
|
+
IsTruncated?: boolean;
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
objects: (r.Contents ?? [])
|
|
138
|
+
.filter((o): o is { Key: string; Size: number; LastModified: Date } =>
|
|
139
|
+
Boolean(o.Key && o.Size !== undefined && o.LastModified),
|
|
140
|
+
)
|
|
141
|
+
.map((o) => ({
|
|
142
|
+
key: o.Key,
|
|
143
|
+
size: o.Size,
|
|
144
|
+
uploaded: o.LastModified,
|
|
145
|
+
})),
|
|
146
|
+
cursor: r.NextContinuationToken,
|
|
147
|
+
truncated: Boolean(r.IsTruncated),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const isNotFound = (err: unknown): boolean => {
|
|
153
|
+
if (!err || typeof err !== "object") return false;
|
|
154
|
+
const e = err as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
155
|
+
return e.name === "NoSuchKey" || e.$metadata?.httpStatusCode === 404;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const toWebReadableStream = (
|
|
159
|
+
body: S3ResponseBody,
|
|
160
|
+
): ReadableStream<Uint8Array> => {
|
|
161
|
+
if (body instanceof ReadableStream) return body;
|
|
162
|
+
if ("transformToWebStream" in body) return body.transformToWebStream();
|
|
163
|
+
if (Symbol.asyncIterator in body) return asyncIterableToStream(body);
|
|
164
|
+
throw new Error("Unsupported S3 response body type");
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const asyncIterableToStream = (
|
|
168
|
+
iterable: AsyncIterable<Uint8Array | ArrayBuffer | ArrayBufferView | string>,
|
|
169
|
+
): ReadableStream<Uint8Array> => {
|
|
170
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
171
|
+
return new ReadableStream<Uint8Array>({
|
|
172
|
+
async pull(controller) {
|
|
173
|
+
const { value, done } = await iterator.next();
|
|
174
|
+
if (done) {
|
|
175
|
+
controller.close();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
controller.enqueue(toUint8Array(value));
|
|
179
|
+
},
|
|
180
|
+
async cancel(reason) {
|
|
181
|
+
await iterator.return?.(reason);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const toUint8Array = (
|
|
187
|
+
value: Uint8Array | ArrayBuffer | ArrayBufferView | string,
|
|
188
|
+
): Uint8Array => {
|
|
189
|
+
if (typeof value === "string") return new TextEncoder().encode(value);
|
|
190
|
+
if (value instanceof Uint8Array) return value;
|
|
191
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
192
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const streamToBytes = async (
|
|
196
|
+
stream: ReadableStream<Uint8Array>,
|
|
197
|
+
): Promise<Uint8Array> => {
|
|
198
|
+
const reader = stream.getReader();
|
|
199
|
+
const chunks: Uint8Array[] = [];
|
|
200
|
+
while (true) {
|
|
201
|
+
const { value, done } = await reader.read();
|
|
202
|
+
if (done) break;
|
|
203
|
+
if (value) chunks.push(value);
|
|
204
|
+
}
|
|
205
|
+
const total = chunks.reduce((n, c) => n + c.byteLength, 0);
|
|
206
|
+
const out = new Uint8Array(total);
|
|
207
|
+
let offset = 0;
|
|
208
|
+
for (const c of chunks) {
|
|
209
|
+
out.set(c, offset);
|
|
210
|
+
offset += c.byteLength;
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
};
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage seam for opaque blob payloads — replay rrweb chunks and
|
|
3
|
+
* pprof profiles.
|
|
4
|
+
*
|
|
5
|
+
* Two implementations:
|
|
6
|
+
* - `R2BlobStore` — Cloudflare R2 binding (default on Workers).
|
|
7
|
+
* - `S3BlobStore` (sibling file) — AWS S3 / MinIO / any S3-compatible.
|
|
8
|
+
*
|
|
9
|
+
* Replay chunks live under `replays/<session_id>/<chunk_id>`; pprof
|
|
10
|
+
* profiles live under `profiles/<service>/<trace_id>/<span_id>`. The
|
|
11
|
+
* adapter never sees these paths — they're constructed by the callers
|
|
12
|
+
* (`replay-receiver`, `profile-routes`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface BlobStore {
|
|
16
|
+
put(
|
|
17
|
+
key: string,
|
|
18
|
+
body: ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>,
|
|
19
|
+
options?: BlobPutOptions,
|
|
20
|
+
): Promise<void>;
|
|
21
|
+
get(key: string): Promise<BlobObject | null>;
|
|
22
|
+
delete(key: string): Promise<void>;
|
|
23
|
+
list(prefix: string, options?: BlobListOptions): Promise<BlobListResult>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BlobPutOptions {
|
|
27
|
+
httpMetadata?: { contentType?: string; contentEncoding?: string };
|
|
28
|
+
customMetadata?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BlobListOptions {
|
|
32
|
+
limit?: number;
|
|
33
|
+
cursor?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BlobObject {
|
|
37
|
+
body: ReadableStream<Uint8Array>;
|
|
38
|
+
bytes(): Promise<Uint8Array>;
|
|
39
|
+
/**
|
|
40
|
+
* Object size in bytes when the store knows it cheaply (S3 `ContentLength`,
|
|
41
|
+
* R2 `size`). Undefined when it can't be determined without reading the body.
|
|
42
|
+
*/
|
|
43
|
+
size?: number;
|
|
44
|
+
httpMetadata?: { contentType?: string; contentEncoding?: string };
|
|
45
|
+
customMetadata?: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BlobListResult {
|
|
49
|
+
objects: Array<{ key: string; size: number; uploaded: Date }>;
|
|
50
|
+
cursor?: string;
|
|
51
|
+
truncated: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const toExactArrayBuffer = (bytes: Uint8Array): ArrayBuffer => {
|
|
55
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
56
|
+
copy.set(bytes);
|
|
57
|
+
return copy.buffer;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const emptyChecksums: R2Checksums = {
|
|
61
|
+
toJSON: () => ({}),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const writeMetadata = (
|
|
65
|
+
metadata: R2HTTPMetadata | undefined,
|
|
66
|
+
headers: Headers,
|
|
67
|
+
): void => {
|
|
68
|
+
if (!metadata) return;
|
|
69
|
+
if (metadata.contentType) headers.set("content-type", metadata.contentType);
|
|
70
|
+
if (metadata.contentLanguage)
|
|
71
|
+
headers.set("content-language", metadata.contentLanguage);
|
|
72
|
+
if (metadata.contentDisposition)
|
|
73
|
+
headers.set("content-disposition", metadata.contentDisposition);
|
|
74
|
+
if (metadata.contentEncoding)
|
|
75
|
+
headers.set("content-encoding", metadata.contentEncoding);
|
|
76
|
+
if (metadata.cacheControl)
|
|
77
|
+
headers.set("cache-control", metadata.cacheControl);
|
|
78
|
+
if (metadata.cacheExpiry)
|
|
79
|
+
headers.set("expires", metadata.cacheExpiry.toUTCString());
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const metadataFrom = (
|
|
83
|
+
metadata: R2HTTPMetadata | Headers | undefined,
|
|
84
|
+
): R2HTTPMetadata | undefined => {
|
|
85
|
+
if (!metadata) return undefined;
|
|
86
|
+
if (metadata instanceof Headers) {
|
|
87
|
+
return {
|
|
88
|
+
contentType: metadata.get("content-type") ?? undefined,
|
|
89
|
+
contentLanguage: metadata.get("content-language") ?? undefined,
|
|
90
|
+
contentDisposition: metadata.get("content-disposition") ?? undefined,
|
|
91
|
+
contentEncoding: metadata.get("content-encoding") ?? undefined,
|
|
92
|
+
cacheControl: metadata.get("cache-control") ?? undefined,
|
|
93
|
+
cacheExpiry: metadata.get("expires")
|
|
94
|
+
? new Date(metadata.get("expires") as string)
|
|
95
|
+
: undefined,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return metadata;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* R2 adapter. R2's binding shape is already very close to this
|
|
103
|
+
* interface; the adapter normalizes the differences (cursor type,
|
|
104
|
+
* list pagination shape).
|
|
105
|
+
*/
|
|
106
|
+
export class R2BlobStore implements BlobStore {
|
|
107
|
+
constructor(private readonly bucket: R2Bucket) {}
|
|
108
|
+
|
|
109
|
+
async put(
|
|
110
|
+
key: string,
|
|
111
|
+
body: ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>,
|
|
112
|
+
options?: BlobPutOptions,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
await this.bucket.put(key, body as ArrayBuffer | ReadableStream, {
|
|
115
|
+
httpMetadata: options?.httpMetadata,
|
|
116
|
+
customMetadata: options?.customMetadata,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async get(key: string): Promise<BlobObject | null> {
|
|
121
|
+
const obj = await this.bucket.get(key);
|
|
122
|
+
if (!obj) return null;
|
|
123
|
+
return {
|
|
124
|
+
body: obj.body,
|
|
125
|
+
bytes: () => obj.arrayBuffer().then((b) => new Uint8Array(b)),
|
|
126
|
+
size: obj.size,
|
|
127
|
+
httpMetadata: obj.httpMetadata,
|
|
128
|
+
customMetadata: obj.customMetadata,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async delete(key: string): Promise<void> {
|
|
133
|
+
await this.bucket.delete(key);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async list(
|
|
137
|
+
prefix: string,
|
|
138
|
+
options?: BlobListOptions,
|
|
139
|
+
): Promise<BlobListResult> {
|
|
140
|
+
const r = await this.bucket.list({
|
|
141
|
+
prefix,
|
|
142
|
+
limit: options?.limit,
|
|
143
|
+
cursor: options?.cursor,
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
objects: r.objects.map((o) => ({
|
|
147
|
+
key: o.key,
|
|
148
|
+
size: o.size,
|
|
149
|
+
uploaded: o.uploaded,
|
|
150
|
+
})),
|
|
151
|
+
cursor: r.truncated ? r.cursor : undefined,
|
|
152
|
+
truncated: r.truncated,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class BlobStoreToR2Adapter implements R2Bucket {
|
|
158
|
+
constructor(private readonly store: BlobStore) {}
|
|
159
|
+
|
|
160
|
+
async put(
|
|
161
|
+
key: string,
|
|
162
|
+
value:
|
|
163
|
+
| ReadableStream
|
|
164
|
+
| ArrayBuffer
|
|
165
|
+
| ArrayBufferView
|
|
166
|
+
| string
|
|
167
|
+
| Blob
|
|
168
|
+
| null,
|
|
169
|
+
options?: R2PutOptions,
|
|
170
|
+
): Promise<R2Object> {
|
|
171
|
+
let body: ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>;
|
|
172
|
+
if (value === null) {
|
|
173
|
+
body = new Uint8Array(0);
|
|
174
|
+
} else if (typeof value === "string") {
|
|
175
|
+
body = new TextEncoder().encode(value);
|
|
176
|
+
} else if (value instanceof ReadableStream) {
|
|
177
|
+
body = value as ReadableStream<Uint8Array>;
|
|
178
|
+
} else if (value instanceof ArrayBuffer) {
|
|
179
|
+
body = value;
|
|
180
|
+
} else if (ArrayBuffer.isView(value)) {
|
|
181
|
+
body = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
182
|
+
} else if (value instanceof Blob) {
|
|
183
|
+
body = new Uint8Array(await value.arrayBuffer());
|
|
184
|
+
} else {
|
|
185
|
+
throw new Error("Unsupported value type for S3 put");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const size =
|
|
189
|
+
body instanceof ReadableStream
|
|
190
|
+
? 0
|
|
191
|
+
: body instanceof ArrayBuffer
|
|
192
|
+
? body.byteLength
|
|
193
|
+
: body.byteLength;
|
|
194
|
+
const httpMetadata = metadataFrom(options?.httpMetadata);
|
|
195
|
+
await this.store.put(key, body, {
|
|
196
|
+
httpMetadata,
|
|
197
|
+
customMetadata: options?.customMetadata,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
key,
|
|
202
|
+
version: "",
|
|
203
|
+
size,
|
|
204
|
+
etag: "",
|
|
205
|
+
httpEtag: "",
|
|
206
|
+
checksums: emptyChecksums,
|
|
207
|
+
uploaded: new Date(),
|
|
208
|
+
httpMetadata,
|
|
209
|
+
customMetadata: options?.customMetadata,
|
|
210
|
+
storageClass: options?.storageClass ?? "Standard",
|
|
211
|
+
writeHttpMetadata: (headers: Headers) =>
|
|
212
|
+
writeMetadata(httpMetadata, headers),
|
|
213
|
+
} as unknown as R2Object;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async get(
|
|
217
|
+
key: string,
|
|
218
|
+
_options?: R2GetOptions,
|
|
219
|
+
): Promise<R2ObjectBody | null> {
|
|
220
|
+
const obj = await this.store.get(key);
|
|
221
|
+
if (!obj) return null;
|
|
222
|
+
|
|
223
|
+
// One underlying stream backs both `body` and the buffered accessors.
|
|
224
|
+
// Callers consume exactly one of them — the profile download streams
|
|
225
|
+
// `body` via `new Response(obj.body)`, while replay/profile-filter read
|
|
226
|
+
// `json()`/`arrayBuffer()`. Teeing would force the unread branch to
|
|
227
|
+
// buffer the entire blob in memory (defeating the streaming download for
|
|
228
|
+
// large pprof profiles), so we share a single stream instead. This also
|
|
229
|
+
// keeps `bodyUsed` honest: the buffered accessors lock `body` directly.
|
|
230
|
+
const body = obj.body;
|
|
231
|
+
let bytesPromise: Promise<Uint8Array> | null = null;
|
|
232
|
+
const bytes = async (): Promise<Uint8Array> => {
|
|
233
|
+
if (!bytesPromise) bytesPromise = streamToBytes(body);
|
|
234
|
+
return bytesPromise;
|
|
235
|
+
};
|
|
236
|
+
const httpMetadata = obj.httpMetadata;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
key,
|
|
240
|
+
version: "",
|
|
241
|
+
size: obj.size ?? 0,
|
|
242
|
+
etag: "",
|
|
243
|
+
httpEtag: "",
|
|
244
|
+
checksums: emptyChecksums,
|
|
245
|
+
uploaded: new Date(),
|
|
246
|
+
httpMetadata,
|
|
247
|
+
customMetadata: obj.customMetadata,
|
|
248
|
+
storageClass: "Standard",
|
|
249
|
+
body,
|
|
250
|
+
get bodyUsed() {
|
|
251
|
+
return body.locked;
|
|
252
|
+
},
|
|
253
|
+
arrayBuffer: async () => {
|
|
254
|
+
const data = await bytes();
|
|
255
|
+
return toExactArrayBuffer(data);
|
|
256
|
+
},
|
|
257
|
+
bytes: async () => {
|
|
258
|
+
const data = await bytes();
|
|
259
|
+
return new Uint8Array(data);
|
|
260
|
+
},
|
|
261
|
+
json: async <T>() => {
|
|
262
|
+
const data = await bytes();
|
|
263
|
+
const text = new TextDecoder().decode(data);
|
|
264
|
+
return JSON.parse(text) as T;
|
|
265
|
+
},
|
|
266
|
+
text: async () => {
|
|
267
|
+
const data = await bytes();
|
|
268
|
+
return new TextDecoder().decode(data);
|
|
269
|
+
},
|
|
270
|
+
blob: async () => {
|
|
271
|
+
const data = await bytes();
|
|
272
|
+
return new Blob([toExactArrayBuffer(data)], {
|
|
273
|
+
type: obj.httpMetadata?.contentType,
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
writeHttpMetadata: (headers: Headers) =>
|
|
277
|
+
writeMetadata(httpMetadata, headers),
|
|
278
|
+
} as unknown as R2ObjectBody;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async delete(keys: string | string[]): Promise<void> {
|
|
282
|
+
const keysArr = Array.isArray(keys) ? keys : [keys];
|
|
283
|
+
await Promise.all(keysArr.map((k) => this.store.delete(k)));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async list(options?: R2ListOptions): Promise<R2Objects> {
|
|
287
|
+
const res = await this.store.list(options?.prefix ?? "", {
|
|
288
|
+
limit: options?.limit,
|
|
289
|
+
cursor: options?.cursor,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const objects = res.objects.map((o) => ({
|
|
293
|
+
key: o.key,
|
|
294
|
+
size: o.size,
|
|
295
|
+
uploaded: o.uploaded,
|
|
296
|
+
etag: "",
|
|
297
|
+
httpEtag: "",
|
|
298
|
+
checksums: emptyChecksums,
|
|
299
|
+
version: "",
|
|
300
|
+
storageClass: "Standard",
|
|
301
|
+
writeHttpMetadata: () => {},
|
|
302
|
+
})) as unknown as R2Object[];
|
|
303
|
+
|
|
304
|
+
return res.truncated
|
|
305
|
+
? ({
|
|
306
|
+
objects,
|
|
307
|
+
truncated: true,
|
|
308
|
+
cursor: res.cursor ?? "",
|
|
309
|
+
delimitedPrefixes: [],
|
|
310
|
+
} as R2Objects)
|
|
311
|
+
: ({ objects, truncated: false, delimitedPrefixes: [] } as R2Objects);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async head(key: string): Promise<R2Object | null> {
|
|
315
|
+
const obj = await this.store.get(key);
|
|
316
|
+
if (!obj) return null;
|
|
317
|
+
// BlobStore has no metadata-only HEAD, so this issues a GET under the
|
|
318
|
+
// hood. Release the response body immediately so S3 can reuse the socket.
|
|
319
|
+
await obj.body.cancel().catch(() => {});
|
|
320
|
+
return {
|
|
321
|
+
key,
|
|
322
|
+
version: "",
|
|
323
|
+
size: obj.size ?? 0,
|
|
324
|
+
etag: "",
|
|
325
|
+
httpEtag: "",
|
|
326
|
+
checksums: emptyChecksums,
|
|
327
|
+
uploaded: new Date(),
|
|
328
|
+
httpMetadata: obj.httpMetadata,
|
|
329
|
+
customMetadata: obj.customMetadata,
|
|
330
|
+
storageClass: "Standard",
|
|
331
|
+
writeHttpMetadata: (headers: Headers) =>
|
|
332
|
+
writeMetadata(obj.httpMetadata, headers),
|
|
333
|
+
} as unknown as R2Object;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async createMultipartUpload(
|
|
337
|
+
_key: string,
|
|
338
|
+
_options?: R2MultipartOptions,
|
|
339
|
+
): Promise<R2MultipartUpload> {
|
|
340
|
+
throw new Error("Multipart upload not implemented for S3 wrapper");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
resumeMultipartUpload(_key: string, _uploadId: string): R2MultipartUpload {
|
|
344
|
+
throw new Error("Multipart upload not implemented for S3 wrapper");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const streamToBytes = async (
|
|
349
|
+
stream: ReadableStream<Uint8Array>,
|
|
350
|
+
): Promise<Uint8Array> => {
|
|
351
|
+
const reader = stream.getReader();
|
|
352
|
+
const chunks: Uint8Array[] = [];
|
|
353
|
+
while (true) {
|
|
354
|
+
const { value, done } = await reader.read();
|
|
355
|
+
if (done) break;
|
|
356
|
+
if (value) chunks.push(value);
|
|
357
|
+
}
|
|
358
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
359
|
+
const out = new Uint8Array(total);
|
|
360
|
+
let offset = 0;
|
|
361
|
+
for (const chunk of chunks) {
|
|
362
|
+
out.set(chunk, offset);
|
|
363
|
+
offset += chunk.byteLength;
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
};
|