From 41f29ef71588aad3995487889e1c988a8aab2dda Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 03:54:36 +0200 Subject: [PATCH 01/56] feat(otel): ClickHouse-first OTEL metrics storage with full-fidelity decode Add a native ClickHouse metrics path alongside the existing TimescaleDB one. When TEMPS_CLICKHOUSE_* is configured, OTLP metrics are decoded at full fidelity (temporality, monotonicity, explicit/exponential histograms, summaries, exemplars, typed labels) and stored in a native `metrics` MergeTree; query_metrics/list_metric_names and the anomaly-detector helpers run natively against it. The service_metrics alerting bridge and the default (no-ClickHouse) TimescaleDB path are unchanged. Proven end-to-end against a live ClickHouse testcontainer: store -> query -> read-back round-trips with fidelity (4 CH storage tests + 1 decode->store fidelity test + 263 lib unit tests + 7 e2e + 15 timescale, all green). Correctness fixes uncovered by the live integration tests: - ReplacingMergeTree ORDER BY now includes a MATERIALIZED label fingerprint (sipHash64 of the sorted label set) and a full-precision timestamp. Previously distinct label-series sharing a coarse per-second timestamp silently collapsed into one row (data loss across series). - Bucket expression uses toInt64(toUnixTimestamp(...))*1000; the prior toUnixTimestamp64Milli() over a DateTime was an illegal-type error. - Aggregates wrap value in assumeNotNull so Nullable(Float64) results match the f64 row read (RowBinary type-width mismatch otherwise). Test infrastructure: fix the ClickHouse testcontainer wait strategy (HTTP /ping via the http_wait_plain feature) and credentials so the integration tests actually execute instead of silently skipping. Frontend: scaffold a metrics explorer page (mirrors Traces) wired to the generated SDK; the richer Phase C query params and the Observe metric kind await an SDK regen against a running server. Deferred follow-ups: rate() delta-vs-cumulative handling, histogram quantile reconstruction, live-CH coverage for exp-histogram/summary/exemplar columns, and TimescaleDB parity for the new fidelity. --- Cargo.lock | 97 +- crates/temps-otel/Cargo.toml | 5 + .../migrations/clickhouse/0003_metrics.sql | 133 ++ .../temps-otel/src/handlers/ingest_handler.rs | 27 +- .../temps-otel/src/handlers/query_handler.rs | 113 +- crates/temps-otel/src/ingest/decode.rs | 435 +++++- crates/temps-otel/src/plugin.rs | 7 +- .../temps-otel/src/services/otel_service.rs | 46 +- .../src/storage/clickhouse/migrations.rs | 4 + .../temps-otel/src/storage/clickhouse/mod.rs | 1359 ++++++++++++++++- crates/temps-otel/src/storage/timescaledb.rs | 8 +- crates/temps-otel/src/types.rs | 512 ++++++- .../tests/clickhouse_storage_test.rs | 389 +++++ .../temps-otel/tests/decode_to_store_test.rs | 377 +++++ .../tests/timescaledb_storage_test.rs | 27 +- web/src/components/dashboard/Sidebar.tsx | 2 + web/src/components/observe/types.ts | 31 + web/src/pages/Metrics.tsx | 20 + web/src/pages/MetricsExplorer.tsx | 662 ++++++++ web/src/pages/ProjectDetail.tsx | 5 + 20 files changed, 4103 insertions(+), 156 deletions(-) create mode 100644 crates/temps-otel/migrations/clickhouse/0003_metrics.sql create mode 100644 crates/temps-otel/tests/clickhouse_storage_test.rs create mode 100644 crates/temps-otel/tests/decode_to_store_test.rs create mode 100644 web/src/pages/Metrics.tsx create mode 100644 web/src/pages/MetricsExplorer.tsx diff --git a/Cargo.lock b/Cargo.lock index 42e2fc770..01d0cb5e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2150,7 +2150,7 @@ checksum = "1678b3295890df5895480a7c080430e73df2b7101f1763f62a3b32dd532f1d37" dependencies = [ "chrono", "http 1.4.0", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_urlencoded", @@ -2168,7 +2168,7 @@ source = "git+https://github.com/cloudflare/cloudflare-rs?rev=85fc25c#85fc25c185 dependencies = [ "chrono", "http 1.4.0", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_urlencoded", @@ -6594,7 +6594,7 @@ dependencies = [ "getrandom 0.2.17", "http 1.4.0", "rand 0.8.6", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_path_to_error", @@ -8666,6 +8666,41 @@ dependencies = [ "webpki-roots 1.0.7", ] +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2 0.4.14", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reserve-port" version = "2.4.0" @@ -10532,7 +10567,7 @@ dependencies = [ "ipnet", "parking_lot", "rand 0.8.6", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "sha2 0.10.9", @@ -10618,7 +10653,7 @@ dependencies = [ "http 1.4.0", "http-body-util", "hyper", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", @@ -10824,7 +10859,7 @@ dependencies = [ "openidconnect", "qrcode", "rand 0.8.6", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_derive", @@ -10927,7 +10962,7 @@ dependencies = [ "http 1.4.0", "once_cell", "rand 0.8.6", - "reqwest", + "reqwest 0.12.28", "schemars 0.8.22", "sea-orm", "serde", @@ -10987,7 +11022,7 @@ dependencies = [ "libc", "mime_guess", "rand 0.8.6", - "reqwest", + "reqwest 0.12.28", "rustls", "sea-orm", "serde", @@ -11166,7 +11201,7 @@ dependencies = [ "http-body-util", "mockall 0.12.1", "num_cpus", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serial_test", @@ -11208,7 +11243,7 @@ dependencies = [ "mockall 0.12.1", "octocrab", "rand 0.8.6", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_derive", @@ -11266,7 +11301,7 @@ dependencies = [ "hmac 0.12.1", "mockall 0.13.1", "quick-xml", - "reqwest", + "reqwest 0.12.28", "rsa", "sea-orm", "serde", @@ -11298,7 +11333,7 @@ dependencies = [ "hickory-proto", "hickory-resolver", "hickory-server", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "tempfile", @@ -11331,7 +11366,7 @@ dependencies = [ "instant-acme", "log", "rcgen 0.13.2", - "reqwest", + "reqwest 0.12.28", "rustls", "rustls-native-certs", "rustls-pemfile", @@ -11381,7 +11416,7 @@ dependencies = [ "pingora-openssl", "pingora-proxy", "rand 0.8.6", - "reqwest", + "reqwest 0.12.28", "rustls-pemfile", "sea-orm", "sea-orm-migration", @@ -11417,7 +11452,7 @@ dependencies = [ "http-body-util", "lettre", "md5", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", @@ -11451,7 +11486,7 @@ dependencies = [ "hex", "hmac 0.12.1", "lol_html", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", @@ -11584,7 +11619,7 @@ dependencies = [ "chrono", "include_dir", "mime_guess", - "reqwest", + "reqwest 0.12.28", "scraper", "sea-orm", "serde", @@ -11684,7 +11719,7 @@ dependencies = [ "octocrab", "rand 0.8.6", "regex", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_derive", @@ -11714,7 +11749,7 @@ version = "0.1.0-beta.39" dependencies = [ "chrono", "nix 0.29.0", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "tempfile", @@ -11735,7 +11770,7 @@ dependencies = [ "jsonwebtoken", "mime_guess", "rand 0.8.6", - "reqwest", + "reqwest 0.12.28", "rsa", "sea-orm", "serde", @@ -11821,7 +11856,7 @@ dependencies = [ "chrono", "include_dir", "mime_guess", - "reqwest", + "reqwest 0.12.28", "scraper", "sea-orm", "serde", @@ -11848,7 +11883,7 @@ dependencies = [ "hickory-resolver", "log", "parking_lot", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "temps-auth", @@ -11927,7 +11962,7 @@ dependencies = [ "futures", "futures-util", "hex", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", @@ -11991,7 +12026,7 @@ dependencies = [ "libc", "mongodb", "redis 0.28.2", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", @@ -12082,7 +12117,7 @@ dependencies = [ "futures-util", "lettre", "log", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", @@ -12158,6 +12193,7 @@ dependencies = [ "temps-entities", "temps-metrics", "temps-migrations", + "testcontainers", "thiserror 2.0.18", "tokio", "tokio-util", @@ -12284,7 +12320,7 @@ dependencies = [ "rand 0.8.6", "redis 0.28.2", "regex", - "reqwest", + "reqwest 0.12.28", "schemars 0.8.22", "sea-orm", "serde", @@ -12611,7 +12647,7 @@ dependencies = [ "base64 0.21.7", "headless_chrome", "mockito", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", @@ -12651,7 +12687,7 @@ dependencies = [ "futures", "hex", "http 1.4.0", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_derive", @@ -12678,7 +12714,7 @@ version = "0.1.0-beta.39" dependencies = [ "async-trait", "chrono", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", @@ -12732,7 +12768,7 @@ dependencies = [ "chrono", "hex", "hmac 0.12.1", - "reqwest", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", @@ -12826,6 +12862,7 @@ dependencies = [ "memchr", "parse-display", "pin-project-lite", + "reqwest 0.13.4", "serde", "serde_json", "serde_with 3.20.0", diff --git a/crates/temps-otel/Cargo.toml b/crates/temps-otel/Cargo.toml index b5b56e06c..0af3c1e8c 100644 --- a/crates/temps-otel/Cargo.toml +++ b/crates/temps-otel/Cargo.toml @@ -78,3 +78,8 @@ tokio = { workspace = true, features = ["test-util", "macros"] } tower = { workspace = true } http-body-util = { workspace = true } temps-migrations = { path = "../temps-migrations" } +# Real-ClickHouse integration tests (clickhouse_storage_test.rs). Pinned to the +# workspace version to avoid the transitive-tree conflict resolved at 0.27.2. +# `http_wait_plain` enables the HTTP /ping readiness wait — the clickhouse-server +# image never logs "Ready for connections" to stdout/stderr, only to its log file. +testcontainers = { workspace = true, features = ["http_wait_plain"] } diff --git a/crates/temps-otel/migrations/clickhouse/0003_metrics.sql b/crates/temps-otel/migrations/clickhouse/0003_metrics.sql new file mode 100644 index 000000000..d7fb2602a --- /dev/null +++ b/crates/temps-otel/migrations/clickhouse/0003_metrics.sql @@ -0,0 +1,133 @@ +-- OTel metrics table: system-of-record for metric telemetry when ClickHouse is enabled. +-- +-- Design decisions (ADR-016, Phase B): +-- +-- ENGINE: ReplacingMergeTree(_version) +-- Like spans, OTLP exporters retry on transient failures. _version (Unix ms +-- timestamp at ingest) lets the engine deduplicate retried batches by keeping +-- the highest _version for each ORDER BY tuple. Use FINAL on reads where dedup +-- correctness matters; skip FINAL on hot-path aggregate reads where eventual +-- consistency is acceptable. +-- +-- PARTITION BY toYYYYMM(timestamp) +-- Monthly partitions allow ClickHouse to drop whole TTL-expired partitions in +-- one DROP PARTITION call instead of row-level deletes. +-- +-- ORDER BY (project_id, metric_name, service_name, toUnixTimestamp(timestamp)) +-- project_id first: every query is project-scoped so the primary index prefix +-- eliminates all other projects cheaply. +-- metric_name second: list_metric_names (SELECT DISTINCT metric_name) and +-- metric-filtered time-series reads read a contiguous block. +-- service_name third: most queries filter by service. +-- timestamp fourth (full DateTime64 ms): time-range scans within a metric/ +-- service read sequentially, at millisecond granularity. +-- attributes_hash last: a fingerprint of the label set. Without it, two data +-- points that differ ONLY by their labels (e.g. http.method=GET vs POST) share +-- an ORDER BY tuple and the ReplacingMergeTree silently collapses them into a +-- single row — dropping a whole series. The hash keeps distinct label-sets as +-- distinct rows while still deduplicating true retries (same series + same +-- timestamp + same labels, distinguished by the higher _version). +-- +-- TTL toDateTime(timestamp) + INTERVAL 90 DAY +-- Matches the existing TimescaleDB retention policy. +-- +-- LowCardinality columns: service_name, service_version, deployment_environment, +-- metric_type, temporality, unit — all bounded value sets. Stored as a dictionary +-- + integer references, cutting storage and improving GROUP BY. +-- +-- attributes Map(String,String) +-- The metric data-point labels. A native Map is queryable directly +-- (attributes['key']) without JSONExtract, and the ingest layer already caps +-- label count/size and strips temps.* keys at the trust boundary. +-- +-- value Nullable(Float64) +-- Only Gauge/Sum points carry a scalar value; Histogram/Summary/ExpHistogram +-- leave it NULL (a synthetic sum/count value is also written by ingest so the +-- anomaly detector has a number to read). +-- +-- Histogram / exponential-histogram / summary arrays +-- Stored as Array columns (empty array = sentinel, never Nullable). This keeps +-- full OTLP fidelity for later quantile reconstruction without a table rewrite. +-- +-- NOTE on comments: this file is processed by execute_multi which strips +-- whole-line -- comments then splits on ';'. Do NOT put a ';' in any inline +-- after-code comment (a prior metrics migration crashed at boot exactly this way). +CREATE TABLE IF NOT EXISTS metrics +( + -- Tenant + deployment context + project_id Int32, + deployment_id Nullable(Int32), + + -- Resource / service identity (denormalized at ingest) + service_name LowCardinality(String), + service_version LowCardinality(String), + deployment_environment LowCardinality(String), + + -- Metric identity + semantics + metric_name String, + metric_type LowCardinality(String), + temporality LowCardinality(String) DEFAULT 'unspecified', + is_monotonic Nullable(UInt8), + unit LowCardinality(String) DEFAULT '', + description String DEFAULT '', + + -- Timing + timestamp DateTime64(3, 'UTC'), + start_time Nullable(DateTime64(3, 'UTC')), + flags UInt32 DEFAULT 0, + + -- Scalar (Gauge / Sum) + value Nullable(Float64), + + -- Explicit histogram / summary aggregate fields + histogram_count Nullable(UInt64), + histogram_sum Nullable(Float64), + histogram_min Nullable(Float64), + histogram_max Nullable(Float64), + histogram_bounds Array(Float64) DEFAULT [], + histogram_bucket_counts Array(UInt64) DEFAULT [], + + -- Exponential-histogram fields + exp_scale Nullable(Int32), + exp_zero_count Nullable(UInt64), + exp_zero_threshold Nullable(Float64), + exp_positive_offset Nullable(Int32), + exp_positive_counts Array(UInt64) DEFAULT [], + exp_negative_offset Nullable(Int32), + exp_negative_counts Array(UInt64) DEFAULT [], + + -- Summary quantiles: (quantile, value) pairs + summary_quantiles Array(Tuple(Float64, Float64)) DEFAULT [], + + -- Exemplars: (trace_id, span_id, value, timestamp) tuples + exemplars Array(Tuple(String, String, Float64, DateTime64(3, 'UTC'))) DEFAULT [], + + -- Data-point labels + attributes Map(String, String), + + -- Series fingerprint: a deterministic hash of the label set, included in the + -- ORDER BY so that data points which differ ONLY by their labels are distinct + -- rows and are never deduplicated into one another by the ReplacingMergeTree. + -- MATERIALIZED so ClickHouse derives it from attributes (it can never drift + -- from the map) and it is not part of the positional insert row. + attributes_hash UInt64 MATERIALIZED sipHash64(arraySort(arrayZip(mapKeys(attributes), mapValues(attributes)))), + + -- Dedup key: Unix millisecond timestamp set at ingest time. + _version UInt64 DEFAULT toUnixTimestamp64Milli(now64()) +) +ENGINE = ReplacingMergeTree(_version) +PARTITION BY toYYYYMM(timestamp) +ORDER BY (project_id, metric_name, service_name, timestamp, attributes_hash) +TTL toDateTime(timestamp) + INTERVAL 90 DAY +SETTINGS index_granularity = 8192; + +-- Compression CODECs. timestamp clusters tightly (Delta), _version is monotonic +-- ms (DoubleDelta), value is a float metric (Gorilla). +ALTER TABLE metrics MODIFY COLUMN timestamp DateTime64(3, 'UTC') CODEC(Delta, ZSTD(1)); + +ALTER TABLE metrics MODIFY COLUMN _version UInt64 DEFAULT toUnixTimestamp64Milli(now64()) CODEC(DoubleDelta, ZSTD(1)); + +ALTER TABLE metrics MODIFY COLUMN value Nullable(Float64) CODEC(Gorilla, ZSTD(1)); + +-- Bloom filter index on service_name for service-filtered metric queries. +ALTER TABLE metrics ADD INDEX IF NOT EXISTS idx_metrics_service_name service_name TYPE bloom_filter GRANULARITY 4; diff --git a/crates/temps-otel/src/handlers/ingest_handler.rs b/crates/temps-otel/src/handlers/ingest_handler.rs index 5d523b671..9653f0733 100644 --- a/crates/temps-otel/src/handlers/ingest_handler.rs +++ b/crates/temps-otel/src/handlers/ingest_handler.rs @@ -1134,23 +1134,18 @@ mod tests { use std::collections::BTreeMap; fn make_otlp_point(metric_type: MetricType, value: f64) -> OtlpMetricPoint { - OtlpMetricPoint { - project_id: 1, - deployment_id: Some(42), - resource: ResourceInfo::default(), - metric_name: "test.metric".to_string(), + let mut p = OtlpMetricPoint::skeleton( + 1, + Some(42), + ResourceInfo::default(), + "test.metric".to_string(), metric_type, - unit: "count".to_string(), - timestamp: chrono::Utc::now(), - value: Some(value), - histogram_count: None, - histogram_sum: None, - histogram_min: None, - histogram_max: None, - histogram_bounds: None, - histogram_bucket_counts: None, - attributes: BTreeMap::new(), - } + "count".to_string(), + chrono::Utc::now(), + BTreeMap::new(), + ); + p.value = Some(value); + p } /// CORRECTNESS(metrics-correctness-B): OTLP Sum must be stored as Gauge, diff --git a/crates/temps-otel/src/handlers/query_handler.rs b/crates/temps-otel/src/handlers/query_handler.rs index 8577d5273..d5f3b11e8 100644 --- a/crates/temps-otel/src/handlers/query_handler.rs +++ b/crates/temps-otel/src/handlers/query_handler.rs @@ -29,6 +29,19 @@ pub struct MetricQueryParams { pub end_time: Option, pub bucket_interval: Option, pub limit: Option, + /// Restrict to a single metric type: gauge | sum | histogram | + /// exponential_histogram | summary. Unknown values are ignored (no filter). + pub metric_type: Option, + /// Aggregation applied per bucket: avg (default) | sum | min | max | count | + /// rate | p50/p95/p99 | quantile:0.95. Unknown values fall back to avg. + pub aggregation: Option, + /// Exact-match data-point label filters as comma-separated `key=value` + /// pairs, e.g. `http.method=GET,http.status_code=200`. Keys must match the + /// metric-name allowlist `[a-zA-Z0-9_.:-]`. + pub label_filters: Option, + /// Comma-separated label keys to group the series by, e.g. + /// `http.method,http.route`. Each key must match the allowlist. + pub group_by: Option, } #[derive(Debug, Deserialize)] @@ -84,13 +97,13 @@ pub struct HealthQueryParams { // ── Response DTOs ─────────────────────────────────────────────────── #[derive(Debug, Serialize, ToSchema)] -pub struct MetricsResponse { +pub struct OtelMetricsResponse { pub data: Vec, pub count: usize, } #[derive(Debug, Serialize, ToSchema)] -pub struct MetricNamesResponse { +pub struct OtelMetricNamesResponse { pub names: Vec, } @@ -186,6 +199,32 @@ fn parse_attributes(s: &str) -> BTreeMap { .collect() } +/// Parse a metric type query token into the typed enum. Unknown → `None`. +fn parse_metric_type(s: &str) -> Option { + match s.trim().to_ascii_lowercase().as_str() { + "gauge" => Some(MetricType::Gauge), + "sum" | "counter" => Some(MetricType::Sum), + "histogram" => Some(MetricType::Histogram), + "exponential_histogram" | "exp_histogram" => Some(MetricType::ExponentialHistogram), + "summary" => Some(MetricType::Summary), + _ => None, + } +} + +/// Parse a comma-separated list of label keys, trimming and dropping empties. +fn parse_label_keys(s: &str) -> Vec { + s.split(',') + .map(|k| k.trim()) + .filter(|k| !k.is_empty()) + .map(|k| k.to_string()) + .collect() +} + +/// Parse comma-separated `key=value` label filters into ordered pairs. +fn parse_label_filters(s: &str) -> Vec<(String, String)> { + parse_attributes(s).into_iter().collect() +} + /// Query metrics with time bucketing. #[utoipa::path( tag = "OTel", @@ -200,9 +239,14 @@ fn parse_attributes(s: &str) -> BTreeMap { ("end_time" = Option, Query, description = "End time (RFC 3339)"), ("bucket_interval" = Option, Query, description = "Bucket interval (e.g. '1 hour', '5 minutes')"), ("limit" = Option, Query, description = "Max buckets to return (default: 1000)"), + ("metric_type" = Option, Query, description = "Filter by metric type (gauge, sum, histogram, exponential_histogram, summary)"), + ("aggregation" = Option, Query, description = "Per-bucket aggregation: avg (default), sum, min, max, count, rate, p50/p95/p99, quantile:0.95"), + ("label_filters" = Option, Query, description = "Comma-separated key=value data-point label filters (keys must match [a-zA-Z0-9_.:-])"), + ("group_by" = Option, Query, description = "Comma-separated label keys to group series by"), ), responses( - (status = 200, description = "Metrics data", body = MetricsResponse), + (status = 200, description = "Metrics data", body = OtelMetricsResponse), + (status = 400, description = "Invalid label key", body = ProblemDetails), (status = 401, description = "Unauthorized", body = ProblemDetails), (status = 403, description = "Insufficient permissions", body = ProblemDetails), (status = 500, description = "Internal server error", body = ProblemDetails), @@ -225,12 +269,28 @@ pub async fn query_metrics( end_time: params.end_time.as_deref().and_then(parse_datetime), bucket_interval: params.bucket_interval, limit: params.limit, + metric_type: params.metric_type.as_deref().and_then(parse_metric_type), + label_filters: params + .label_filters + .as_deref() + .map(parse_label_filters) + .unwrap_or_default(), + group_by: params + .group_by + .as_deref() + .map(parse_label_keys) + .unwrap_or_default(), + aggregation: params + .aggregation + .as_deref() + .map(MetricAggregation::parse) + .unwrap_or_default(), }; let data = state.otel_service.query_metrics(query).await?; let count = data.len(); - Ok(Json(MetricsResponse { data, count })) + Ok(Json(OtelMetricsResponse { data, count })) } /// List distinct metric names for a project. @@ -242,7 +302,7 @@ pub async fn query_metrics( ("project_id" = i32, Path, description = "Project ID"), ), responses( - (status = 200, description = "List of metric names", body = MetricNamesResponse), + (status = 200, description = "List of metric names", body = OtelMetricNamesResponse), (status = 401, description = "Unauthorized", body = ProblemDetails), (status = 403, description = "Insufficient permissions", body = ProblemDetails), (status = 500, description = "Internal server error", body = ProblemDetails), @@ -257,7 +317,7 @@ pub async fn list_metric_names( permission_guard!(auth, OtelRead); let names = state.otel_service.list_metric_names(project_id).await?; - Ok(Json(MetricNamesResponse { names })) + Ok(Json(OtelMetricNamesResponse { names })) } /// Query trace spans with optional filters. @@ -793,4 +853,45 @@ mod tests { assert_eq!(result.get("valid").unwrap(), "ok"); assert_eq!(result.get("good").unwrap(), "yes"); } + + // ── Metric query param parsing ────────────────────────────────── + + #[test] + fn test_parse_metric_type() { + assert_eq!(parse_metric_type("gauge"), Some(MetricType::Gauge)); + assert_eq!(parse_metric_type("Sum"), Some(MetricType::Sum)); + assert_eq!(parse_metric_type("counter"), Some(MetricType::Sum)); + assert_eq!(parse_metric_type("histogram"), Some(MetricType::Histogram)); + assert_eq!( + parse_metric_type("exponential_histogram"), + Some(MetricType::ExponentialHistogram) + ); + assert_eq!(parse_metric_type("summary"), Some(MetricType::Summary)); + assert_eq!(parse_metric_type("nonsense"), None); + } + + #[test] + fn test_parse_label_keys() { + let keys = parse_label_keys("http.method, http.route ,,"); + assert_eq!( + keys, + vec!["http.method".to_string(), "http.route".to_string()] + ); + assert!(parse_label_keys("").is_empty()); + assert!(parse_label_keys(" , , ").is_empty()); + } + + #[test] + fn test_parse_label_filters() { + let filters = parse_label_filters("http.method=GET,http.status_code=200"); + // parse_attributes returns a BTreeMap (sorted), so order is deterministic. + assert_eq!( + filters, + vec![ + ("http.method".to_string(), "GET".to_string()), + ("http.status_code".to_string(), "200".to_string()), + ] + ); + assert!(parse_label_filters("").is_empty()); + } } diff --git a/crates/temps-otel/src/ingest/decode.rs b/crates/temps-otel/src/ingest/decode.rs index 0c35468e9..3d2f07e04 100644 --- a/crates/temps-otel/src/ingest/decode.rs +++ b/crates/temps-otel/src/ingest/decode.rs @@ -232,7 +232,42 @@ fn any_value_to_string(val: &proto::common::v1::AnyValue) -> String { Some(proto::common::v1::any_value::Value::IntValue(i)) => i.to_string(), Some(proto::common::v1::any_value::Value::DoubleValue(d)) => d.to_string(), Some(proto::common::v1::any_value::Value::BytesValue(b)) => hex::encode(b), - _ => String::new(), + // Preserve complex values as JSON text rather than flattening to "". + Some(proto::common::v1::any_value::Value::ArrayValue(_)) + | Some(proto::common::v1::any_value::Value::KvlistValue(_)) => { + any_value_to_json(val).to_string() + } + None => String::new(), + } +} + +/// Render an OTLP `AnyValue` as a `serde_json::Value`, preserving nested +/// arrays and key/value lists so complex attributes survive as JSON text. +fn any_value_to_json(val: &proto::common::v1::AnyValue) -> serde_json::Value { + use serde_json::Value as J; + match &val.value { + Some(proto::common::v1::any_value::Value::StringValue(s)) => J::String(s.clone()), + Some(proto::common::v1::any_value::Value::BoolValue(b)) => J::Bool(*b), + Some(proto::common::v1::any_value::Value::IntValue(i)) => J::Number((*i).into()), + Some(proto::common::v1::any_value::Value::DoubleValue(d)) => { + serde_json::Number::from_f64(*d) + .map(J::Number) + .unwrap_or(J::Null) + } + Some(proto::common::v1::any_value::Value::BytesValue(b)) => J::String(hex::encode(b)), + Some(proto::common::v1::any_value::Value::ArrayValue(arr)) => { + J::Array(arr.values.iter().map(any_value_to_json).collect()) + } + Some(proto::common::v1::any_value::Value::KvlistValue(kvl)) => { + let mut map = serde_json::Map::new(); + for kv in &kvl.values { + if let Some(v) = &kv.value { + map.insert(kv.key.clone(), any_value_to_json(v)); + } + } + J::Object(map) + } + None => J::Null, } } @@ -251,68 +286,125 @@ fn extract_metric_points( deployment_id: Option, points: &mut Vec, ) { - let base = |timestamp: u64, attrs: &[proto::common::v1::KeyValue]| MetricPoint { - project_id, - deployment_id, - resource: resource.clone(), - metric_name: metric.name.clone(), - metric_type: MetricType::Gauge, - unit: metric.unit.clone(), - timestamp: nanos_to_datetime(timestamp), - value: None, - histogram_count: None, - histogram_sum: None, - histogram_min: None, - histogram_max: None, - histogram_bounds: None, - histogram_bucket_counts: None, - attributes: kv_to_string_map(attrs), + let description = if metric.description.is_empty() { + None + } else { + Some(metric.description.clone()) + }; + + // Build a skeleton MetricPoint with the shared per-point fields populated. + // `start_time_unix_nano == 0` is treated as "not set" per the OTLP convention. + let base = |start_time_unix_nano: u64, + time_unix_nano: u64, + attrs: &[proto::common::v1::KeyValue], + metric_type: MetricType, + flags: u32| { + let mut p = MetricPoint::skeleton( + project_id, + deployment_id, + resource.clone(), + metric.name.clone(), + metric_type, + metric.unit.clone(), + nanos_to_datetime(time_unix_nano), + kv_to_string_map(attrs), + ); + p.description = description.clone(); + p.flags = flags; + if start_time_unix_nano > 0 { + p.start_time = Some(nanos_to_datetime(start_time_unix_nano)); + } + p }; match &metric.data { Some(proto::metrics::v1::metric::Data::Gauge(gauge)) => { for dp in &gauge.data_points { - let mut p = base(dp.time_unix_nano, &dp.attributes); - p.metric_type = MetricType::Gauge; + let mut p = base( + dp.start_time_unix_nano, + dp.time_unix_nano, + &dp.attributes, + MetricType::Gauge, + dp.flags, + ); p.value = Some(number_data_point_value(dp)); + p.exemplars = number_exemplars(&dp.exemplars); points.push(p); } } Some(proto::metrics::v1::metric::Data::Sum(sum)) => { + let temporality = AggregationTemporality::from_proto(sum.aggregation_temporality); for dp in &sum.data_points { - let mut p = base(dp.time_unix_nano, &dp.attributes); - p.metric_type = MetricType::Sum; + let mut p = base( + dp.start_time_unix_nano, + dp.time_unix_nano, + &dp.attributes, + MetricType::Sum, + dp.flags, + ); + p.temporality = Some(temporality); + p.is_monotonic = Some(sum.is_monotonic); p.value = Some(number_data_point_value(dp)); + p.exemplars = number_exemplars(&dp.exemplars); points.push(p); } } Some(proto::metrics::v1::metric::Data::Histogram(hist)) => { + let temporality = AggregationTemporality::from_proto(hist.aggregation_temporality); for dp in &hist.data_points { - let mut p = base(dp.time_unix_nano, &dp.attributes); - p.metric_type = MetricType::Histogram; + let mut p = base( + dp.start_time_unix_nano, + dp.time_unix_nano, + &dp.attributes, + MetricType::Histogram, + dp.flags, + ); + p.temporality = Some(temporality); p.histogram_count = Some(dp.count); p.histogram_sum = dp.sum; p.histogram_min = dp.min; p.histogram_max = dp.max; p.histogram_bounds = Some(dp.explicit_bounds.clone()); p.histogram_bucket_counts = Some(dp.bucket_counts.clone()); - // Use mean as the scalar value for aggregation + p.exemplars = number_exemplars(&dp.exemplars); + // Keep writing the synthetic mean as the scalar `value` so the + // anomaly detector / continuous aggregates keep working. A later + // Timescale phase de-synthesizes this once those readers move + // to the structured columns. if dp.count > 0 { p.value = dp.sum.map(|s| s / dp.count as f64); } points.push(p); } } - // ExponentialHistogram and Summary are less common; - // store them as histograms with available data Some(proto::metrics::v1::metric::Data::ExponentialHistogram(eh)) => { + let temporality = AggregationTemporality::from_proto(eh.aggregation_temporality); for dp in &eh.data_points { - let mut p = base(dp.time_unix_nano, &dp.attributes); - p.metric_type = MetricType::Histogram; + let mut p = base( + dp.start_time_unix_nano, + dp.time_unix_nano, + &dp.attributes, + MetricType::ExponentialHistogram, + dp.flags, + ); + p.temporality = Some(temporality); p.histogram_count = Some(dp.count); p.histogram_sum = dp.sum; p.histogram_min = dp.min; p.histogram_max = dp.max; + p.exp_scale = Some(dp.scale); + p.exp_zero_count = Some(dp.zero_count); + p.exp_zero_threshold = Some(dp.zero_threshold); + if let Some(pos) = &dp.positive { + p.exp_positive_offset = Some(pos.offset); + p.exp_positive_counts = Some(pos.bucket_counts.clone()); + } + if let Some(neg) = &dp.negative { + p.exp_negative_offset = Some(neg.offset); + p.exp_negative_counts = Some(neg.bucket_counts.clone()); + } + p.exemplars = number_exemplars(&dp.exemplars); + // Keep the synthetic mean (see Histogram note above). if dp.count > 0 { p.value = dp.sum.map(|s| s / dp.count as f64); } @@ -321,10 +413,22 @@ fn extract_metric_points( } Some(proto::metrics::v1::metric::Data::Summary(summary)) => { for dp in &summary.data_points { - let mut p = base(dp.time_unix_nano, &dp.attributes); - p.metric_type = MetricType::Histogram; + let mut p = base( + dp.start_time_unix_nano, + dp.time_unix_nano, + &dp.attributes, + MetricType::Summary, + dp.flags, + ); p.histogram_count = Some(dp.count); p.histogram_sum = Some(dp.sum); + p.summary_quantiles = Some( + dp.quantile_values + .iter() + .map(|q| (q.quantile, q.value)) + .collect(), + ); + // Keep the synthetic mean (see Histogram note above). if dp.count > 0 { p.value = Some(dp.sum / dp.count as f64); } @@ -343,6 +447,37 @@ fn number_data_point_value(dp: &proto::metrics::v1::NumberDataPoint) -> f64 { } } +fn exemplar_value(ex: &proto::metrics::v1::Exemplar) -> f64 { + match &ex.value { + Some(proto::metrics::v1::exemplar::Value::AsDouble(d)) => *d, + Some(proto::metrics::v1::exemplar::Value::AsInt(i)) => *i as f64, + None => 0.0, + } +} + +/// Convert a slice of OTLP exemplars into our domain `Exemplar` records, +/// hex-encoding trace/span IDs and dropping empty ones. +fn number_exemplars(exemplars: &[proto::metrics::v1::Exemplar]) -> Vec { + exemplars + .iter() + .map(|ex| Exemplar { + timestamp: nanos_to_datetime(ex.time_unix_nano), + value: exemplar_value(ex), + trace_id: if ex.trace_id.is_empty() { + None + } else { + Some(hex::encode(&ex.trace_id)) + }, + span_id: if ex.span_id.is_empty() { + None + } else { + Some(hex::encode(&ex.span_id)) + }, + attributes: kv_to_string_map(&ex.filtered_attributes), + }) + .collect() +} + fn extract_span_record( span: &proto::trace::v1::Span, resource: &ResourceInfo, @@ -807,6 +942,246 @@ mod tests { ); } + fn metrics_request_with( + data: proto::metrics::v1::metric::Data, + name: &str, + unit: &str, + description: &str, + ) -> Vec { + use prost::Message; + let request = proto::collector::metrics::v1::ExportMetricsServiceRequest { + resource_metrics: vec![proto::metrics::v1::ResourceMetrics { + resource: Some(test_support::resource("metrics-svc")), + scope_metrics: vec![proto::metrics::v1::ScopeMetrics { + scope: None, + metrics: vec![proto::metrics::v1::Metric { + name: name.into(), + description: description.into(), + unit: unit.into(), + data: Some(data), + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + request.encode_to_vec() + } + + #[test] + fn test_decode_sum_monotonicity_and_temporality() { + let data = proto::metrics::v1::metric::Data::Sum(proto::metrics::v1::Sum { + data_points: vec![proto::metrics::v1::NumberDataPoint { + start_time_unix_nano: 1_700_000_000_000_000_000, + time_unix_nano: 1_700_000_001_000_000_000, + value: Some(proto::metrics::v1::number_data_point::Value::AsInt(99)), + attributes: vec![test_support::kv("route", "/api")], + ..Default::default() + }], + aggregation_temporality: 2, // CUMULATIVE + is_monotonic: true, + }); + let encoded = metrics_request_with(data, "requests.total", "1", "Total requests"); + let points = decode_metrics_request(&encoded, 5, Some(3)).unwrap(); + + assert_eq!(points.len(), 1); + let p = &points[0]; + assert_eq!(p.metric_type, MetricType::Sum); + assert_eq!(p.value, Some(99.0)); + assert_eq!(p.temporality, Some(AggregationTemporality::Cumulative)); + assert_eq!(p.is_monotonic, Some(true)); + assert_eq!(p.description.as_deref(), Some("Total requests")); + assert!(p.start_time.is_some()); + assert_eq!(p.deployment_id, Some(3)); + assert_eq!(p.attributes.get("route").map(|s| s.as_str()), Some("/api")); + } + + #[test] + fn test_decode_explicit_histogram() { + let data = proto::metrics::v1::metric::Data::Histogram(proto::metrics::v1::Histogram { + data_points: vec![proto::metrics::v1::HistogramDataPoint { + start_time_unix_nano: 0, + time_unix_nano: 1_700_000_000_000_000_000, + count: 10, + sum: Some(123.0), + bucket_counts: vec![2, 3, 5], + explicit_bounds: vec![1.0, 5.0], + min: Some(0.5), + max: Some(9.0), + flags: 0, + ..Default::default() + }], + aggregation_temporality: 1, // DELTA + }); + let encoded = metrics_request_with(data, "latency", "ms", ""); + let points = decode_metrics_request(&encoded, 1, None).unwrap(); + + assert_eq!(points.len(), 1); + let p = &points[0]; + assert_eq!(p.metric_type, MetricType::Histogram); + assert_eq!(p.temporality, Some(AggregationTemporality::Delta)); + assert_eq!(p.histogram_count, Some(10)); + assert_eq!(p.histogram_sum, Some(123.0)); + assert_eq!(p.histogram_min, Some(0.5)); + assert_eq!(p.histogram_max, Some(9.0)); + assert_eq!(p.histogram_bounds.as_deref(), Some(&[1.0, 5.0][..])); + assert_eq!(p.histogram_bucket_counts.as_deref(), Some(&[2, 3, 5][..])); + // Synthetic mean preserved for the anomaly detector. + assert_eq!(p.value, Some(12.3)); + // start_time_unix_nano == 0 is treated as unset. + assert!(p.start_time.is_none()); + // Description empty → None. + assert!(p.description.is_none()); + } + + #[test] + fn test_decode_exponential_histogram() { + let data = proto::metrics::v1::metric::Data::ExponentialHistogram( + proto::metrics::v1::ExponentialHistogram { + data_points: vec![proto::metrics::v1::ExponentialHistogramDataPoint { + start_time_unix_nano: 0, + time_unix_nano: 1_700_000_000_000_000_000, + count: 8, + sum: Some(40.0), + scale: 3, + zero_count: 1, + zero_threshold: 1e-6, + positive: Some( + proto::metrics::v1::exponential_histogram_data_point::Buckets { + offset: 2, + bucket_counts: vec![1, 2, 3], + }, + ), + negative: Some( + proto::metrics::v1::exponential_histogram_data_point::Buckets { + offset: -1, + bucket_counts: vec![4, 5], + }, + ), + min: Some(0.1), + max: Some(10.0), + flags: 0, + ..Default::default() + }], + aggregation_temporality: 2, // CUMULATIVE + }, + ); + let encoded = metrics_request_with(data, "exp.hist", "ms", ""); + let points = decode_metrics_request(&encoded, 1, None).unwrap(); + + assert_eq!(points.len(), 1); + let p = &points[0]; + assert_eq!(p.metric_type, MetricType::ExponentialHistogram); + assert_eq!(p.temporality, Some(AggregationTemporality::Cumulative)); + assert_eq!(p.histogram_count, Some(8)); + assert_eq!(p.histogram_sum, Some(40.0)); + assert_eq!(p.exp_scale, Some(3)); + assert_eq!(p.exp_zero_count, Some(1)); + assert_eq!(p.exp_zero_threshold, Some(1e-6)); + assert_eq!(p.exp_positive_offset, Some(2)); + assert_eq!(p.exp_positive_counts.as_deref(), Some(&[1, 2, 3][..])); + assert_eq!(p.exp_negative_offset, Some(-1)); + assert_eq!(p.exp_negative_counts.as_deref(), Some(&[4, 5][..])); + // Synthetic mean preserved. + assert_eq!(p.value, Some(5.0)); + } + + #[test] + fn test_decode_summary_quantiles() { + use proto::metrics::v1::summary_data_point::ValueAtQuantile; + let data = proto::metrics::v1::metric::Data::Summary(proto::metrics::v1::Summary { + data_points: vec![proto::metrics::v1::SummaryDataPoint { + start_time_unix_nano: 0, + time_unix_nano: 1_700_000_000_000_000_000, + count: 4, + sum: 20.0, + quantile_values: vec![ + ValueAtQuantile { + quantile: 0.5, + value: 4.0, + }, + ValueAtQuantile { + quantile: 0.99, + value: 9.0, + }, + ], + flags: 0, + ..Default::default() + }], + }); + let encoded = metrics_request_with(data, "rpc.duration", "ms", ""); + let points = decode_metrics_request(&encoded, 1, None).unwrap(); + + assert_eq!(points.len(), 1); + let p = &points[0]; + assert_eq!(p.metric_type, MetricType::Summary); + assert_eq!(p.histogram_count, Some(4)); + assert_eq!(p.histogram_sum, Some(20.0)); + assert_eq!(p.summary_quantiles, Some(vec![(0.5, 4.0), (0.99, 9.0)])); + // Synthetic mean preserved. + assert_eq!(p.value, Some(5.0)); + } + + #[test] + fn test_decode_metric_exemplars() { + let trace_id = vec![0xaa; 16]; + let span_id = vec![0xbb; 8]; + let data = proto::metrics::v1::metric::Data::Sum(proto::metrics::v1::Sum { + data_points: vec![proto::metrics::v1::NumberDataPoint { + time_unix_nano: 1_700_000_000_000_000_000, + value: Some(proto::metrics::v1::number_data_point::Value::AsDouble(7.0)), + exemplars: vec![proto::metrics::v1::Exemplar { + time_unix_nano: 1_700_000_000_500_000_000, + value: Some(proto::metrics::v1::exemplar::Value::AsDouble(7.0)), + span_id: span_id.clone(), + trace_id: trace_id.clone(), + filtered_attributes: vec![test_support::kv("k", "v")], + }], + ..Default::default() + }], + aggregation_temporality: 1, + is_monotonic: false, + }); + let encoded = metrics_request_with(data, "with.exemplar", "1", ""); + let points = decode_metrics_request(&encoded, 1, None).unwrap(); + + assert_eq!(points.len(), 1); + let p = &points[0]; + assert_eq!(p.is_monotonic, Some(false)); + assert_eq!(p.exemplars.len(), 1); + let ex = &p.exemplars[0]; + assert_eq!(ex.value, 7.0); + assert_eq!( + ex.trace_id.as_deref(), + Some(hex::encode(&trace_id).as_str()) + ); + assert_eq!(ex.span_id.as_deref(), Some(hex::encode(&span_id).as_str())); + assert_eq!(ex.attributes.get("k").map(|s| s.as_str()), Some("v")); + } + + #[test] + fn test_complex_attribute_value_preserved_as_json() { + // An array-valued attribute should survive as JSON text, not "". + let arr = proto::common::v1::AnyValue { + value: Some(proto::common::v1::any_value::Value::ArrayValue( + proto::common::v1::ArrayValue { + values: vec![ + proto::common::v1::AnyValue { + value: Some(proto::common::v1::any_value::Value::IntValue(1)), + }, + proto::common::v1::AnyValue { + value: Some(proto::common::v1::any_value::Value::StringValue( + "two".into(), + )), + }, + ], + }, + )), + }; + let rendered = any_value_to_string(&arr); + assert_eq!(rendered, "[1,\"two\"]"); + } + // ── Logs decode tests ─────────────────────────────────────────── #[test] diff --git a/crates/temps-otel/src/plugin.rs b/crates/temps-otel/src/plugin.rs index 9ff59f0d8..60969ad18 100644 --- a/crates/temps-otel/src/plugin.rs +++ b/crates/temps-otel/src/plugin.rs @@ -164,8 +164,8 @@ impl OtelConfig { ), components( schemas( - query_handler::MetricsResponse, - query_handler::MetricNamesResponse, + query_handler::OtelMetricsResponse, + query_handler::OtelMetricNamesResponse, query_handler::TracesResponse, query_handler::TraceSummariesResponse, crate::types::TraceSummary, @@ -175,6 +175,9 @@ impl OtelConfig { query_handler::QuotaResponse, query_handler::PipelineStatsResponse, crate::types::MetricBucket, + crate::types::HistogramSummary, + crate::types::MetricAggregation, + crate::types::AggregationTemporality, crate::types::SpanRecord, crate::types::SpanEvent, crate::types::SpanKind, diff --git a/crates/temps-otel/src/services/otel_service.rs b/crates/temps-otel/src/services/otel_service.rs index 2d49bc6ad..c92e094ae 100644 --- a/crates/temps-otel/src/services/otel_service.rs +++ b/crates/temps-otel/src/services/otel_service.rs @@ -260,6 +260,24 @@ impl OtelService { // ── Query operations ──────────────────────────────────────────── pub async fn query_metrics(&self, query: MetricQuery) -> Result, OtelError> { + // Validate every label key used for filtering or grouping against the + // same allowlist the ingest path enforces on metric/attribute names. + // Keys flow into the store's SQL (as bound map indices); rejecting bad + // keys here is a defence-in-depth trust boundary on top of the store's + // own check. Values are always bound, never validated for content. + for key in query + .group_by + .iter() + .chain(query.label_filters.iter().map(|(k, _)| k)) + { + if temps_metrics::validate_metric_name(key).is_err() { + return Err(OtelError::Validation { + message: format!( + "metric query label key '{key}' contains characters outside the allowed set [a-zA-Z0-9_.:-]" + ), + }); + } + } self.storage.query_metrics(query).await } @@ -614,23 +632,17 @@ mod tests { let mock = MockOtelStorage::new(); let (svc, storage) = make_service(mock); - let point = MetricPoint { - project_id: 1, - deployment_id: None, - resource: ResourceInfo::default(), - metric_name: "http.duration".into(), - metric_type: MetricType::Gauge, - unit: "ms".into(), - timestamp: chrono::Utc::now(), - value: Some(42.5), - histogram_count: None, - histogram_sum: None, - histogram_min: None, - histogram_max: None, - histogram_bounds: None, - histogram_bucket_counts: None, - attributes: Default::default(), - }; + let mut point = MetricPoint::skeleton( + 1, + None, + ResourceInfo::default(), + "http.duration".into(), + MetricType::Gauge, + "ms".into(), + chrono::Utc::now(), + Default::default(), + ); + point.value = Some(42.5); let stored = svc.ingest_metrics(vec![point]).await.unwrap(); assert_eq!(stored, 1); diff --git a/crates/temps-otel/src/storage/clickhouse/migrations.rs b/crates/temps-otel/src/storage/clickhouse/migrations.rs index 6d3ec4634..169301eef 100644 --- a/crates/temps-otel/src/storage/clickhouse/migrations.rs +++ b/crates/temps-otel/src/storage/clickhouse/migrations.rs @@ -30,6 +30,10 @@ const MIGRATIONS: &[Migration] = &[ name: "0002_spans_codecs", sql: include_str!("../../../migrations/clickhouse/0002_spans_codecs.sql"), }, + Migration { + name: "0003_metrics", + sql: include_str!("../../../migrations/clickhouse/0003_metrics.sql"), + }, ]; /// SQL for the migration tracking table. Created on first run. diff --git a/crates/temps-otel/src/storage/clickhouse/mod.rs b/crates/temps-otel/src/storage/clickhouse/mod.rs index e1dfa6ccf..882906163 100644 --- a/crates/temps-otel/src/storage/clickhouse/mod.rs +++ b/crates/temps-otel/src/storage/clickhouse/mod.rs @@ -46,13 +46,15 @@ use async_trait::async_trait; use serde::{Deserialize, Serialize}; use tracing::debug; +use temps_metrics::validate_metric_name; + use crate::error::OtelError; use crate::storage::timescaledb::TimescaleDbStorage; use crate::storage::{BaselinePoint, DeployEvent, MinuteAggregate, OtelStorage, StorageResult}; use crate::types::{ GenAiEvent, GenAiSpanDetail, GenAiTraceSummary, HealthSummary, Insight, InsightStatus, - LogQuery, LogRecord, MetricBucket, MetricPoint, MetricQuery, SpanEvent, SpanKind, SpanRecord, - SpanStatusCode, StorageQuota, TraceQuery, TraceSummary, + LogQuery, LogRecord, MetricAggregation, MetricBucket, MetricPoint, MetricQuery, SpanEvent, + SpanKind, SpanRecord, SpanStatusCode, StorageQuota, TraceQuery, TraceSummary, }; // ── Client configuration ──────────────────────────────────────────────────── @@ -364,6 +366,256 @@ impl From for SpanRecord { } } +// ── Metric row type ─────────────────────────────────────────────────────── + +/// ClickHouse row matching the `metrics` table DDL in `0003_metrics.sql`. +/// +/// **Field order must match the DDL column order exactly.** The `clickhouse` +/// crate serialises fields positionally (binary protocol over HTTP); any +/// reordering here relative to the DDL silently corrupts inserts. A unit test +/// (`ch_metric_row_field_order_matches_ddl`) guards the column count. +/// +/// ## Type mapping +/// +/// | DDL type | Rust type | +/// |---------------------------------------|------------------------------------| +/// | `Int32` | `i32` | +/// | `Nullable(Int32)` | `Option` | +/// | `LowCardinality(String)` / `String` | `String` | +/// | `Nullable(UInt8)` | `Option` (0/1 for is_monotonic)| +/// | `DateTime64(3, 'UTC')` | `i64` (Unix milliseconds) | +/// | `Nullable(DateTime64(3, 'UTC'))` | `Option` (Unix milliseconds) | +/// | `UInt32` | `u32` | +/// | `Nullable(Float64)` | `Option` | +/// | `Nullable(UInt64)` | `Option` | +/// | `Array(Float64)` | `Vec` | +/// | `Array(UInt64)` | `Vec` | +/// | `Array(Tuple(Float64, Float64))` | `Vec<(f64, f64)>` | +/// | `Array(Tuple(String,String,Float64,DateTime64))` | `Vec<(String,String,f64,i64)>` | +/// | `Map(String, String)` | `Vec<(String, String)>` | +/// | `UInt64` | `u64` | +/// +/// Timestamps are stored as Unix milliseconds (`i64`), the same encoding used by +/// [`ChSpanRow`]. The `clickhouse` crate represents `Map(K, V)` as a `Vec<(K, V)>` +/// of key/value pairs in row-binary, so `attributes` is modelled that way. +#[derive(::clickhouse::Row, Serialize, Deserialize, Debug, Clone)] +pub struct ChMetricRow { + // ── Tenant + deployment context ───────────────────────────────────────── + /// project_id Int32 + pub project_id: i32, + /// deployment_id Nullable(Int32) + pub deployment_id: Option, + + // ── Resource / service identity ───────────────────────────────────────── + /// service_name LowCardinality(String) + pub service_name: String, + /// service_version LowCardinality(String) + pub service_version: String, + /// deployment_environment LowCardinality(String) + pub deployment_environment: String, + + // ── Metric identity + semantics ───────────────────────────────────────── + /// metric_name String + pub metric_name: String, + /// metric_type LowCardinality(String) + pub metric_type: String, + /// temporality LowCardinality(String) DEFAULT 'unspecified' + pub temporality: String, + /// is_monotonic Nullable(UInt8) (0/1; None for non-Sum) + pub is_monotonic: Option, + /// unit LowCardinality(String) DEFAULT '' + pub unit: String, + /// description String DEFAULT '' + pub description: String, + + // ── Timing ────────────────────────────────────────────────────────────── + /// timestamp DateTime64(3, 'UTC') — Unix milliseconds + pub timestamp: i64, + /// start_time Nullable(DateTime64(3, 'UTC')) — Unix milliseconds + pub start_time: Option, + /// flags UInt32 DEFAULT 0 + pub flags: u32, + + // ── Scalar (Gauge / Sum) ──────────────────────────────────────────────── + /// value Nullable(Float64) + pub value: Option, + + // ── Explicit histogram / summary aggregate fields ─────────────────────── + /// histogram_count Nullable(UInt64) + pub histogram_count: Option, + /// histogram_sum Nullable(Float64) + pub histogram_sum: Option, + /// histogram_min Nullable(Float64) + pub histogram_min: Option, + /// histogram_max Nullable(Float64) + pub histogram_max: Option, + /// histogram_bounds Array(Float64) DEFAULT [] + pub histogram_bounds: Vec, + /// histogram_bucket_counts Array(UInt64) DEFAULT [] + pub histogram_bucket_counts: Vec, + + // ── Exponential-histogram fields ──────────────────────────────────────── + /// exp_scale Nullable(Int32) + pub exp_scale: Option, + /// exp_zero_count Nullable(UInt64) + pub exp_zero_count: Option, + /// exp_zero_threshold Nullable(Float64) + pub exp_zero_threshold: Option, + /// exp_positive_offset Nullable(Int32) + pub exp_positive_offset: Option, + /// exp_positive_counts Array(UInt64) DEFAULT [] + pub exp_positive_counts: Vec, + /// exp_negative_offset Nullable(Int32) + pub exp_negative_offset: Option, + /// exp_negative_counts Array(UInt64) DEFAULT [] + pub exp_negative_counts: Vec, + + // ── Summary quantiles ─────────────────────────────────────────────────── + /// summary_quantiles Array(Tuple(Float64, Float64)) DEFAULT [] + pub summary_quantiles: Vec<(f64, f64)>, + + // ── Exemplars ─────────────────────────────────────────────────────────── + /// exemplars Array(Tuple(String, String, Float64, DateTime64(3,'UTC'))) + /// Tuple shape: (trace_id, span_id, value, timestamp_ms). + pub exemplars: Vec<(String, String, f64, i64)>, + + // ── Data-point labels ─────────────────────────────────────────────────── + /// attributes Map(String, String) — row-binary as key/value pairs. + pub attributes: Vec<(String, String)>, + + // ── Dedup key ─────────────────────────────────────────────────────────── + /// _version UInt64 DEFAULT toUnixTimestamp64Milli(now64()) + pub _version: u64, +} + +/// The number of named columns in the `metrics` DDL (`0003_metrics.sql`), +/// excluding the `_version` dedup sentinel. The [`ChMetricRow`] struct must have +/// exactly this many domain fields, in the same order. Bump together with the +/// DDL when the schema changes. Used by the field-order guard test. +#[allow(dead_code)] +pub(crate) const CH_METRIC_ROW_FIELD_COUNT: usize = 31; + +impl From<&MetricPoint> for ChMetricRow { + fn from(p: &MetricPoint) -> Self { + // attributes Map: BTreeMap -> ordered key/value pairs. Caller (ingest) + // has already capped count/size and stripped temps.* keys at the trust + // boundary, so we serialise verbatim here. + let attributes: Vec<(String, String)> = p + .attributes + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + + let exemplars: Vec<(String, String, f64, i64)> = p + .exemplars + .iter() + .map(|e| { + ( + e.trace_id.clone().unwrap_or_default(), + e.span_id.clone().unwrap_or_default(), + e.value, + e.timestamp.timestamp_millis(), + ) + }) + .collect(); + + // _version: Unix ms timestamp used as the ReplacingMergeTree dedup key. + let version = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + + Self { + project_id: p.project_id, + deployment_id: p.deployment_id, + service_name: p.resource.service_name.clone(), + service_version: p.resource.service_version.clone().unwrap_or_default(), + deployment_environment: p + .resource + .deployment_environment + .clone() + .unwrap_or_default(), + metric_name: p.metric_name.clone(), + metric_type: p.metric_type.to_string(), + temporality: p + .temporality + .map(|t| t.to_string()) + .unwrap_or_else(|| "unspecified".to_string()), + is_monotonic: p.is_monotonic.map(|b| b as u8), + unit: p.unit.clone(), + description: p.description.clone().unwrap_or_default(), + timestamp: p.timestamp.timestamp_millis(), + start_time: p.start_time.map(|t| t.timestamp_millis()), + flags: p.flags, + value: p.value, + histogram_count: p.histogram_count, + histogram_sum: p.histogram_sum, + histogram_min: p.histogram_min, + histogram_max: p.histogram_max, + histogram_bounds: p.histogram_bounds.clone().unwrap_or_default(), + histogram_bucket_counts: p.histogram_bucket_counts.clone().unwrap_or_default(), + exp_scale: p.exp_scale, + exp_zero_count: p.exp_zero_count, + exp_zero_threshold: p.exp_zero_threshold, + exp_positive_offset: p.exp_positive_offset, + exp_positive_counts: p.exp_positive_counts.clone().unwrap_or_default(), + exp_negative_offset: p.exp_negative_offset, + exp_negative_counts: p.exp_negative_counts.clone().unwrap_or_default(), + summary_quantiles: p.summary_quantiles.clone().unwrap_or_default(), + exemplars, + attributes, + _version: version, + } + } +} + +// ── Metric read-side row types ──────────────────────────────────────────── + +/// Row returned by `query_metrics` — one time-bucketed aggregate. +/// +/// Field order MUST match the SELECT column order in `query_metrics` +/// (positional row-binary). `agg_value` carries the requested +/// [`MetricAggregation`]; `series_values` carries the grouped label values in +/// `group_by` order (empty when the query is ungrouped). +#[derive(::clickhouse::Row, Deserialize, Debug)] +struct ChMetricBucketRow { + /// Bucket start, Unix milliseconds (toStartOfInterval(...)). + bucket_ms: i64, + avg_value: f64, + min_value: f64, + max_value: f64, + count: u64, + /// The value of the requested aggregation for this bucket. + agg_value: f64, + /// Grouped label values, in `group_by` order (empty when ungrouped). + series_values: Vec, +} + +/// Row returned by `list_metric_names` — a single distinct name. +#[derive(::clickhouse::Row, Deserialize, Debug)] +struct ChMetricNameRow { + metric_name: String, +} + +/// Row returned by `get_metric_baseline` — hour/day-bucketed stats. +#[derive(::clickhouse::Row, Deserialize, Debug)] +struct ChBaselineRow { + hour_of_day: i32, + day_of_week: i32, + avg_value: f64, + stddev_value: f64, + sample_count: u64, +} + +/// Row returned by `get_recent_minute_aggregates`. +#[derive(::clickhouse::Row, Deserialize, Debug)] +struct ChMinuteAggregateRow { + /// Bucket start, Unix milliseconds (toStartOfMinute(...)). + bucket_ms: i64, + avg_value: f64, + count: u64, +} + // ── Enum ↔ string helpers ─────────────────────────────────────────────────── /// Map [`SpanKind`] to the string stored in the CH `kind` column. @@ -494,6 +746,80 @@ pub(crate) fn escape_like_pattern(pattern: &str) -> String { .replace('_', "\\_") } +// ── Metric bucket-interval translation ────────────────────────────────────── + +/// Translate a free-form Postgres-style interval string (e.g. `"1 hour"`, +/// `"5 minutes"`, `"1 day"`) into a ClickHouse `INTERVAL ` fragment +/// that is safe to interpolate into a `toStartOfInterval(timestamp, INTERVAL …)` +/// expression. +/// +/// ClickHouse has no `time_bucket()`; bucketing is done with +/// `toStartOfInterval(ts, INTERVAL n unit)`. The `clickhouse` crate cannot bind +/// an `INTERVAL` literal as a parameter, so the fragment must be built as a +/// string. To keep that injection-safe we parse the input strictly: +/// +/// - the count must be a positive integer (`1..=100000`), +/// - the unit must be one of a fixed allowlist (second/minute/hour/day/week), +/// +/// and we re-emit a canonical fragment built only from the parsed integer and a +/// hard-coded unit keyword — no user bytes survive into the SQL. Anything that +/// does not parse falls back to the default `INTERVAL 1 HOUR`. +pub(crate) fn translate_bucket_interval(interval: &str) -> String { + const DEFAULT: &str = "INTERVAL 1 HOUR"; + let trimmed = interval.trim(); + let mut parts = trimmed.split_whitespace(); + let (Some(count_str), Some(unit_raw)) = (parts.next(), parts.next()) else { + return DEFAULT.to_string(); + }; + // Reject anything with extra tokens (e.g. "1 hour; DROP"). + if parts.next().is_some() { + return DEFAULT.to_string(); + } + let Ok(count) = count_str.parse::() else { + return DEFAULT.to_string(); + }; + if count == 0 || count > 100_000 { + return DEFAULT.to_string(); + } + // Normalise the unit (accept singular and plural) to a fixed keyword. + let unit = match unit_raw.to_ascii_lowercase().as_str() { + "second" | "seconds" | "sec" | "secs" => "SECOND", + "minute" | "minutes" | "min" | "mins" => "MINUTE", + "hour" | "hours" | "hr" | "hrs" => "HOUR", + "day" | "days" => "DAY", + "week" | "weeks" => "WEEK", + _ => return DEFAULT.to_string(), + }; + format!("INTERVAL {count} {unit}") +} + +/// Derive the bucket width in whole seconds from a canonical interval fragment +/// produced by [`translate_bucket_interval`] (e.g. `"INTERVAL 5 MINUTE"`). +/// +/// Used only by the `RatePerSec` aggregation to convert a per-bucket counter +/// delta into a per-second rate. The input is always our own controlled +/// fragment (never raw user input), so parse failures fall back to one hour. +pub(crate) fn interval_seconds(interval_sql: &str) -> i64 { + const HOUR: i64 = 3600; + let mut parts = interval_sql.split_whitespace(); + // Expect: INTERVAL + if parts.next() != Some("INTERVAL") { + return HOUR; + } + let Some(count) = parts.next().and_then(|c| c.parse::().ok()) else { + return HOUR; + }; + let unit_secs = match parts.next() { + Some("SECOND") => 1, + Some("MINUTE") => 60, + Some("HOUR") => HOUR, + Some("DAY") => 86_400, + Some("WEEK") => 604_800, + _ => return HOUR, + }; + (count * unit_secs).max(1) +} + // ── OtelError helpers ─────────────────────────────────────────────────────── /// Wrap a ClickHouse ingest error into an [`OtelError::Storage`] with context. @@ -1574,27 +1900,311 @@ impl OtelStorage for ClickHouseOtelStorage { Ok(events) } - // ── Non-span methods — delegate to TimescaleDB unconditionally ─────────── - // (ADR-016 Phases 2–4 will replace these with CH implementations) + // ── Metric write (ClickHouse — sole sink for the OtelStorage path) ─────── + // + // ADR-016 Phase B: when ClickHouse is enabled, native CH `metrics` is the + // single source of truth for the OtelStorage metric path (otel_metrics on + // Timescale no longer receives this write). The independent + // `service_metrics` alerting bridge (TimescaleMetricsStore / metrics_write_tx + // in the ingest handler) is untouched and still targets TimescaleDB. + /// Batch-insert metric points directly into the ClickHouse `metrics` table. + /// + /// Mirrors `store_spans`: chunked `insert` + per-row `write` + `end()`, + /// `ReplacingMergeTree(_version)` dedups retried OTLP payloads. + /// + /// Trust boundary: although the OTLP ingest handler already validates metric + /// names and caps labels before producing `MetricPoint`s, this method is the + /// last gate before bytes hit the store. We re-apply the metric-name + /// allowlist here (defence in depth) and skip — rather than abort — any point + /// whose name is outside `[a-zA-Z0-9_.:-]`. The returned count reflects only + /// the points actually written. async fn store_metrics(&self, points: Vec) -> StorageResult { - self.inner.store_metrics(points).await - } + /// Maximum number of metric rows per ClickHouse HTTP insert request. + const MAX_METRIC_INSERT_BATCH: usize = 10_000; - async fn store_logs(&self, records: Vec) -> StorageResult { - self.inner.store_logs(records).await - } + if points.is_empty() { + return Ok(0); + } - async fn archive_logs(&self, records: Vec) -> StorageResult { - self.inner.archive_logs(records).await + // Filter at the trust boundary: drop names outside the allowlist. + let safe: Vec<&MetricPoint> = points + .iter() + .filter(|p| { + if validate_metric_name(&p.metric_name).is_err() { + tracing::warn!( + project_id = p.project_id, + metric_name = %p.metric_name, + "ClickHouse store_metrics: dropping metric, name outside allowlist (possible injection attempt)" + ); + false + } else { + true + } + }) + .collect(); + + if safe.is_empty() { + return Ok(0); + } + let total = safe.len() as u64; + + for chunk in safe.chunks(MAX_METRIC_INSERT_BATCH) { + let mut inserter = self + .ch + .insert::("metrics") + .map_err(|e| ch_ingest_err("store_metrics (inserter setup)", e))?; + + for point in chunk { + let row = ChMetricRow::from(*point); + inserter + .write(&row) + .await + .map_err(|e| ch_ingest_err("store_metrics (write)", e))?; + } + + inserter + .end() + .await + .map_err(|e| ch_ingest_err("store_metrics (end)", e))?; + } + + debug!(total, "ClickHouseOtelStorage: stored metrics"); + Ok(total) } + // ── Metric reads (ClickHouse — native) ────────────────────────────────── + + /// Time-bucketed metric aggregates over the scalar `value` column. + /// + /// Honours the store-neutral [`MetricQuery`] contract: + /// - `bucket_interval` → `toStartOfInterval(timestamp, INTERVAL …)` (CH has + /// no `time_bucket`), translated via [`translate_bucket_interval`] from a + /// fixed allowlist — no user bytes reach the SQL. + /// - `metric_type` → `metric_type = ?` (bound). + /// - `label_filters` → `attributes[?] = ?` per pair, the **key bound** via a + /// parameter (never concatenated) after passing the metric-name allowlist. + /// - `group_by` → one series per distinct label-set; the grouped label values + /// are returned as an `Array(String)` and paired back with the (allowlisted) + /// keys to form `series_key`. + /// - `aggregation` → the requested reducer drives `agg_value`: avg/sum/min/ + /// max/count, a `quantile(q)(value)`, or `RatePerSec` as + /// `(max-min)/window_seconds` (cumulative monotonic delta; we never + /// re-delta a series, matching the ingest temporality semantics). + /// + /// All filter values are bound via `?` placeholders. async fn query_metrics(&self, query: MetricQuery) -> StorageResult> { - self.inner.query_metrics(query).await + use chrono::TimeZone; + + let interval_sql = + translate_bucket_interval(query.bucket_interval.as_deref().unwrap_or("1 hour")); + let limit = query.limit.unwrap_or(1000).min(10_000); + + // Validate label keys (group_by + label_filters) against the ingest + // allowlist. Reject the whole query on a bad key rather than silently + // dropping a filter — a bad key signals a malformed/abusive request. + for key in query + .group_by + .iter() + .chain(query.label_filters.iter().map(|(k, _)| k)) + { + if validate_metric_name(key).is_err() { + return Err(OtelError::Storage { + message: format!( + "query_metrics: label key '{key}' is outside the allowed character set [a-zA-Z0-9_.:-]" + ), + }); + } + } + + // The aggregation expression over the scalar `value` column. For + // RatePerSec we compute the per-bucket delta divided by the bucket width + // in seconds; the divisor is derived from the (already-validated) + // interval fragment, not user input. + // `value` is Nullable(Float64); every aggregate input is wrapped in + // assumeNotNull (safe — the WHERE clause filters `value IS NOT NULL`) so + // the result type is a non-nullable Float64 that matches the `f64` + // ChMetricBucketRow read field. Without this, min/max/sum/quantile over a + // Nullable column return Nullable(Float64) and the RowBinary read fails + // with "row type mismatches a database schema". + let agg_expr = match query.aggregation { + MetricAggregation::Avg => "avg(assumeNotNull(value))".to_string(), + MetricAggregation::Sum => "sum(assumeNotNull(value))".to_string(), + MetricAggregation::Min => "min(assumeNotNull(value))".to_string(), + MetricAggregation::Max => "max(assumeNotNull(value))".to_string(), + MetricAggregation::Count => "toFloat64(count())".to_string(), + MetricAggregation::RatePerSec => { + let secs = interval_seconds(&interval_sql).max(1); + // Cumulative counters are stored raw (not re-deltaed); within a + // bucket the increase is (max - min). Guard against zero width. + format!("(max(assumeNotNull(value)) - min(assumeNotNull(value))) / {secs}.0") + } + MetricAggregation::Quantile(q) => { + let qc = q.clamp(0.0, 1.0); + // quantile() takes its level as a parameter in parentheses; we + // emit the clamped float literal (not user bytes). + format!("quantile({qc})(assumeNotNull(value))") + } + }; + + // Grouped label values as an Array(String), in group_by order. CH binds + // the key via the `?` map-index parameter, never string interpolation. + let group_select = if query.group_by.is_empty() { + "[] AS series_values".to_string() + } else { + let parts: Vec = query + .group_by + .iter() + .map(|_| "attributes[?]".to_string()) + .collect(); + format!("[{}] AS series_values", parts.join(", ")) + }; + let group_by_extra = if query.group_by.is_empty() { + String::new() + } else { + ", series_values".to_string() + }; + + // Build WHERE with `?` placeholders; bind in the same order below. + let mut where_clauses = vec!["project_id = ?".to_string()]; + if query.metric_name.is_some() { + where_clauses.push("metric_name = ?".to_string()); + } + if query.metric_type.is_some() { + where_clauses.push("metric_type = ?".to_string()); + } + if query.service_name.is_some() { + where_clauses.push("service_name = ?".to_string()); + } + if query.environment.is_some() { + where_clauses.push("deployment_environment = ?".to_string()); + } + for _ in &query.label_filters { + where_clauses.push("attributes[?] = ?".to_string()); + } + if query.start_time.is_some() { + where_clauses.push("timestamp >= fromUnixTimestamp64Milli(?)".to_string()); + } + if query.end_time.is_some() { + where_clauses.push("timestamp <= fromUnixTimestamp64Milli(?)".to_string()); + } + // Only aggregate rows that carry a scalar value. + where_clauses.push("value IS NOT NULL".to_string()); + let where_sql = where_clauses.join(" AND "); + + let sql = format!( + "SELECT \ + toInt64(toUnixTimestamp(toStartOfInterval(timestamp, {interval_sql}))) * 1000 AS bucket_ms, \ + avg(assumeNotNull(value)) AS avg_value, \ + min(assumeNotNull(value)) AS min_value, \ + max(assumeNotNull(value)) AS max_value, \ + count() AS count, \ + {agg_expr} AS agg_value, \ + {group_select} \ + FROM metrics \ + WHERE {where_sql} \ + GROUP BY bucket_ms{group_by_extra} \ + ORDER BY bucket_ms ASC \ + LIMIT ?" + ); + + // Bind order: SELECT group keys first (the `attributes[?]` in the + // projection), then WHERE params in clause order, then LIMIT. + let mut q = self.ch.query(&sql); + for key in &query.group_by { + q = q.bind(key.clone()); + } + q = q.bind(query.project_id); + if let Some(ref name) = query.metric_name { + q = q.bind(name.clone()); + } + if let Some(mt) = query.metric_type { + q = q.bind(mt.to_string()); + } + if let Some(ref svc) = query.service_name { + q = q.bind(svc.clone()); + } + if let Some(ref env) = query.environment { + q = q.bind(env.clone()); + } + for (k, v) in &query.label_filters { + q = q.bind(k.clone()); + q = q.bind(v.clone()); + } + if let Some(start) = query.start_time { + q = q.bind(start.timestamp_millis()); + } + if let Some(end) = query.end_time { + q = q.bind(end.timestamp_millis()); + } + q = q.bind(limit); + + let rows = q + .fetch_all::() + .await + .map_err(|e| ch_query_err("query_metrics", e))?; + + let group_keys = query.group_by.clone(); + Ok(rows + .into_iter() + .map(|r| { + let series_key = if group_keys.is_empty() { + None + } else { + Some( + group_keys + .iter() + .cloned() + .zip(r.series_values) + .collect::>(), + ) + }; + MetricBucket { + bucket: chrono::Utc + .timestamp_millis_opt(r.bucket_ms) + .single() + .unwrap_or_default(), + avg_value: r.avg_value, + min_value: r.min_value, + max_value: r.max_value, + count: r.count as i64, + value: r.agg_value, + quantiles: match query.aggregation.quantile() { + Some(qq) => vec![(qq, r.agg_value)], + None => Vec::new(), + }, + histogram_summary: None, + series_key, + } + }) + .collect()) } + /// List distinct metric names for a project from the CH `metrics` table. async fn list_metric_names(&self, project_id: i32) -> StorageResult> { - self.inner.list_metric_names(project_id).await + let rows = self + .ch + .query( + "SELECT DISTINCT metric_name FROM metrics \ + WHERE project_id = ? ORDER BY metric_name", + ) + .bind(project_id) + .fetch_all::() + .await + .map_err(|e| ch_query_err("list_metric_names", e))?; + + Ok(rows.into_iter().map(|r| r.metric_name).collect()) + } + + // ── Non-span methods — delegate to TimescaleDB unconditionally ─────────── + // (ADR-016 Phases 2–4 will replace these with CH implementations) + + async fn store_logs(&self, records: Vec) -> StorageResult { + self.inner.store_logs(records).await + } + + async fn archive_logs(&self, records: Vec) -> StorageResult { + self.inner.archive_logs(records).await } async fn query_logs(&self, query: LogQuery) -> StorageResult> { @@ -1645,8 +2255,20 @@ impl OtelStorage for ClickHouseOtelStorage { self.inner.check_quota(project_id).await } - // ── Anomaly-detection helpers — delegate to TimescaleDB ────────────────── + // ── Anomaly-detection helpers (ClickHouse — native) ────────────────────── + // + // store_metrics now writes only to CH, so these MUST read from CH or the + // anomaly detector would see an empty otel_metrics on Timescale. Implemented + // natively here to keep anomaly detection alive (ADR-016 Phase B, option a). + /// Hour-of-day / day-of-week baseline stats for a metric over a lookback + /// window. Mirrors the TimescaleDB query: average + population stddev grouped + /// by hour and weekday. ClickHouse `toHour`/`toDayOfWeek` operate in UTC for a + /// `DateTime64(_, 'UTC')` column. + /// + /// `toDayOfWeek` returns 1=Monday..7=Sunday; the Postgres `EXTRACT(DOW)` + /// returns 0=Sunday..6=Saturday. We remap to the Postgres convention so the + /// anomaly detector sees identical day indices regardless of backend. async fn get_metric_baseline( &self, project_id: i32, @@ -1655,17 +2277,66 @@ impl OtelStorage for ClickHouseOtelStorage { environment: Option<&str>, lookback_days: i32, ) -> StorageResult> { - self.inner - .get_metric_baseline( - project_id, - service_name, - metric_name, - environment, - lookback_days, - ) + // Clamp the lookback to a sane range and bind it as the interval count. + let lookback = lookback_days.clamp(1, 3650); + + let mut where_clauses = vec![ + "project_id = ?".to_string(), + "service_name = ?".to_string(), + "metric_name = ?".to_string(), + "value IS NOT NULL".to_string(), + "timestamp >= now() - toIntervalDay(?)".to_string(), + ]; + if environment.is_some() { + where_clauses.push("deployment_environment = ?".to_string()); + } + let where_sql = where_clauses.join(" AND "); + + // toDayOfWeek(...) % 7 maps Mon..Sun (1..7) -> 1..6,0 (Mon..Sat, Sun=0), + // matching Postgres EXTRACT(DOW) where Sunday = 0. + let sql = format!( + "SELECT \ + toInt32(toHour(timestamp)) AS hour_of_day, \ + toInt32(toDayOfWeek(timestamp) % 7) AS day_of_week, \ + avg(value) AS avg_value, \ + ifNull(stddevPop(value), 0) AS stddev_value, \ + count() AS sample_count \ + FROM metrics \ + WHERE {where_sql} \ + GROUP BY hour_of_day, day_of_week \ + ORDER BY day_of_week, hour_of_day" + ); + + let mut q = self + .ch + .query(&sql) + .bind(project_id) + .bind(service_name) + .bind(metric_name) + .bind(lookback as u32); + if let Some(env) = environment { + q = q.bind(env); + } + + let rows = q + .fetch_all::() .await + .map_err(|e| ch_query_err("get_metric_baseline", e))?; + + Ok(rows + .into_iter() + .map(|r| BaselinePoint { + hour_of_day: r.hour_of_day, + day_of_week: r.day_of_week, + avg_value: r.avg_value, + stddev_value: r.stddev_value, + sample_count: r.sample_count as i64, + }) + .collect()) } + /// Recent 1-minute aggregates for anomaly scoring. Buckets on + /// `toStartOfMinute` and averages the scalar `value`. async fn get_recent_minute_aggregates( &self, project_id: i32, @@ -1674,15 +2345,60 @@ impl OtelStorage for ClickHouseOtelStorage { environment: Option<&str>, minutes: i32, ) -> StorageResult> { - self.inner - .get_recent_minute_aggregates( - project_id, - service_name, - metric_name, - environment, - minutes, - ) + use chrono::TimeZone; + + let window = minutes.clamp(1, 100_000); + + let mut where_clauses = vec![ + "project_id = ?".to_string(), + "service_name = ?".to_string(), + "metric_name = ?".to_string(), + "value IS NOT NULL".to_string(), + "timestamp >= now() - toIntervalMinute(?)".to_string(), + ]; + if environment.is_some() { + where_clauses.push("deployment_environment = ?".to_string()); + } + let where_sql = where_clauses.join(" AND "); + + let sql = format!( + "SELECT \ + toInt64(toUnixTimestamp(toStartOfMinute(timestamp))) * 1000 AS bucket_ms, \ + avg(value) AS avg_value, \ + count() AS count \ + FROM metrics \ + WHERE {where_sql} \ + GROUP BY bucket_ms \ + ORDER BY bucket_ms ASC" + ); + + let mut q = self + .ch + .query(&sql) + .bind(project_id) + .bind(service_name) + .bind(metric_name) + .bind(window as u32); + if let Some(env) = environment { + q = q.bind(env); + } + + let rows = q + .fetch_all::() .await + .map_err(|e| ch_query_err("get_recent_minute_aggregates", e))?; + + Ok(rows + .into_iter() + .map(|r| MinuteAggregate { + bucket: chrono::Utc + .timestamp_millis_opt(r.bucket_ms) + .single() + .unwrap_or_default(), + avg_value: r.avg_value, + count: r.count as i64, + }) + .collect()) } async fn get_recent_deploys( @@ -1969,4 +2685,587 @@ mod tests { let otel_err = ch_query_err("query_trace_summaries", err); assert!(otel_err.to_string().contains("query_trace_summaries")); } + + // ── Metric row tests ──────────────────────────────────────────────────── + + use crate::types::{AggregationTemporality, Exemplar, MetricPoint, MetricQuery, MetricType}; + + /// A Gauge point with attributes — the simplest scalar case. + fn make_gauge() -> MetricPoint { + let mut p = MetricPoint::skeleton( + 42, + Some(7), + ResourceInfo { + service_name: "my-service".into(), + service_version: Some("1.2.3".into()), + deployment_environment: Some("production".into()), + attributes: BTreeMap::new(), + }, + "http.server.active_requests".into(), + MetricType::Gauge, + "1".into(), + Utc::now(), + { + let mut m = BTreeMap::new(); + m.insert("http.method".into(), "GET".into()); + m + }, + ); + p.value = Some(3.5); + p + } + + /// A cumulative Histogram point exercising the full array/aggregate fields. + fn make_histogram() -> MetricPoint { + let mut p = MetricPoint::skeleton( + 42, + None, + ResourceInfo::default(), + "http.server.duration".into(), + MetricType::Histogram, + "ms".into(), + Utc::now(), + BTreeMap::new(), + ); + p.temporality = Some(AggregationTemporality::Cumulative); + p.histogram_count = Some(10); + p.histogram_sum = Some(1234.5); + p.histogram_min = Some(1.0); + p.histogram_max = Some(500.0); + p.histogram_bounds = Some(vec![0.0, 5.0, 10.0]); + p.histogram_bucket_counts = Some(vec![1, 4, 3, 2]); + p.value = Some(1234.5 / 10.0); // synthetic mean from decode + p + } + + #[test] + fn ch_metric_row_field_count_matches_ddl() { + // Field-order landmine guard: ChMetricRow serialises positionally and + // MUST mirror 0003_metrics.sql column order/count exactly. Counting via + // a fully-specified struct literal forces a compile error if a field is + // added/removed without updating CH_METRIC_ROW_FIELD_COUNT. + let row = ChMetricRow::from(&make_gauge()); + let ChMetricRow { + project_id: _, + deployment_id: _, + service_name: _, + service_version: _, + deployment_environment: _, + metric_name: _, + metric_type: _, + temporality: _, + is_monotonic: _, + unit: _, + description: _, + timestamp: _, + start_time: _, + flags: _, + value: _, + histogram_count: _, + histogram_sum: _, + histogram_min: _, + histogram_max: _, + histogram_bounds: _, + histogram_bucket_counts: _, + exp_scale: _, + exp_zero_count: _, + exp_zero_threshold: _, + exp_positive_offset: _, + exp_positive_counts: _, + exp_negative_offset: _, + exp_negative_counts: _, + summary_quantiles: _, + exemplars: _, + attributes: _, + _version: _, + } = row; + // 31 domain columns + _version sentinel = 32 serialised fields; the DDL + // declares 31 named columns plus _version, matching this destructure. + assert_eq!(CH_METRIC_ROW_FIELD_COUNT, 31); + } + + #[test] + fn gauge_point_to_ch_row_field_mapping() { + let row = ChMetricRow::from(&make_gauge()); + + assert_eq!(row.project_id, 42); + assert_eq!(row.deployment_id, Some(7)); + assert_eq!(row.service_name, "my-service"); + assert_eq!(row.service_version, "1.2.3"); + assert_eq!(row.deployment_environment, "production"); + assert_eq!(row.metric_name, "http.server.active_requests"); + assert_eq!(row.metric_type, "gauge"); + // Gauge has no temporality -> sentinel. + assert_eq!(row.temporality, "unspecified"); + assert_eq!(row.is_monotonic, None); + assert_eq!(row.unit, "1"); + assert_eq!(row.value, Some(3.5)); + // attributes Map preserved as key/value pairs. + assert_eq!( + row.attributes, + vec![("http.method".to_string(), "GET".to_string())] + ); + // No histogram arrays -> empty sentinels (never Nullable). + assert!(row.histogram_bounds.is_empty()); + assert!(row.histogram_bucket_counts.is_empty()); + assert!(row.summary_quantiles.is_empty()); + assert!(row.exemplars.is_empty()); + assert!(row._version > 0); + } + + #[test] + fn histogram_point_to_ch_row_field_mapping() { + let row = ChMetricRow::from(&make_histogram()); + + assert_eq!(row.metric_type, "histogram"); + assert_eq!(row.temporality, "cumulative"); + assert_eq!(row.histogram_count, Some(10)); + assert_eq!(row.histogram_sum, Some(1234.5)); + assert_eq!(row.histogram_min, Some(1.0)); + assert_eq!(row.histogram_max, Some(500.0)); + assert_eq!(row.histogram_bounds, vec![0.0, 5.0, 10.0]); + assert_eq!(row.histogram_bucket_counts, vec![1, 4, 3, 2]); + } + + #[test] + fn monotonic_sum_maps_is_monotonic_to_u8() { + let mut p = make_gauge(); + p.metric_type = MetricType::Sum; + p.is_monotonic = Some(true); + p.temporality = Some(AggregationTemporality::Delta); + let row = ChMetricRow::from(&p); + assert_eq!(row.metric_type, "sum"); + assert_eq!(row.is_monotonic, Some(1)); + assert_eq!(row.temporality, "delta"); + + p.is_monotonic = Some(false); + let row = ChMetricRow::from(&p); + assert_eq!(row.is_monotonic, Some(0)); + } + + #[test] + fn exemplars_map_to_tuple_with_hex_ids() { + let mut p = make_gauge(); + p.exemplars = vec![Exemplar { + timestamp: Utc::now(), + value: 9.0, + trace_id: Some("deadbeef".into()), + span_id: Some("cafe".into()), + attributes: BTreeMap::new(), + }]; + let row = ChMetricRow::from(&p); + assert_eq!(row.exemplars.len(), 1); + let (trace, span, value, ts) = &row.exemplars[0]; + assert_eq!(trace, "deadbeef"); + assert_eq!(span, "cafe"); + assert_eq!(*value, 9.0); + assert!(*ts > 0); + } + + #[test] + fn chunks_split_at_metric_batch_size() { + // 20_001 metrics -> ceil(20_001 / 10_000) = 3 chunks. + let points: Vec = (0..20_001).map(|_| make_gauge()).collect(); + let chunks: Vec<_> = points.chunks(10_000).collect(); + assert_eq!(chunks.len(), 3); + assert_eq!(chunks[0].len(), 10_000); + assert_eq!(chunks[2].len(), 1); + } + + /// A monotonic cumulative Sum (counter) — exercises is_monotonic + temporality + /// + start_time + flags on the scalar path. + fn make_monotonic_sum() -> MetricPoint { + let mut p = MetricPoint::skeleton( + 7, + Some(3), + ResourceInfo { + service_name: "api".into(), + service_version: Some("9.9.9".into()), + deployment_environment: Some("staging".into()), + attributes: BTreeMap::new(), + }, + "http.requests.total".into(), + MetricType::Sum, + "1".into(), + Utc::now(), + { + let mut m = BTreeMap::new(); + m.insert("route".into(), "/api/v1".into()); + m + }, + ); + p.temporality = Some(AggregationTemporality::Cumulative); + p.is_monotonic = Some(true); + p.start_time = Some(Utc::now()); + p.flags = 1; + p.description = Some("Total HTTP requests".into()); + p.value = Some(4242.0); + p + } + + /// An exponential-histogram point with positive/negative bucket arrays and + /// the exp_* scalar fields populated. + fn make_exp_histogram() -> MetricPoint { + let mut p = MetricPoint::skeleton( + 7, + None, + ResourceInfo::default(), + "rpc.duration".into(), + MetricType::ExponentialHistogram, + "ms".into(), + Utc::now(), + BTreeMap::new(), + ); + p.temporality = Some(AggregationTemporality::Delta); + p.histogram_count = Some(8); + p.histogram_sum = Some(40.0); + p.histogram_min = Some(0.1); + p.histogram_max = Some(10.0); + p.exp_scale = Some(3); + p.exp_zero_count = Some(1); + p.exp_zero_threshold = Some(1e-6); + p.exp_positive_offset = Some(2); + p.exp_positive_counts = Some(vec![1, 2, 3]); + p.exp_negative_offset = Some(-1); + p.exp_negative_counts = Some(vec![4, 5]); + p.value = Some(5.0); // synthetic mean + p + } + + /// A summary point carrying quantile/value pairs. + fn make_summary() -> MetricPoint { + let mut p = MetricPoint::skeleton( + 7, + None, + ResourceInfo::default(), + "rpc.server.duration".into(), + MetricType::Summary, + "ms".into(), + Utc::now(), + BTreeMap::new(), + ); + p.histogram_count = Some(4); + p.histogram_sum = Some(20.0); + p.summary_quantiles = Some(vec![(0.5, 4.0), (0.99, 9.0)]); + p.value = Some(5.0); // synthetic mean + p + } + + #[test] + fn monotonic_sum_point_to_ch_row_field_mapping() { + let row = ChMetricRow::from(&make_monotonic_sum()); + + assert_eq!(row.metric_type, "sum"); + assert_eq!(row.temporality, "cumulative"); + assert_eq!(row.is_monotonic, Some(1)); + assert_eq!(row.unit, "1"); + assert_eq!(row.description, "Total HTTP requests"); + assert_eq!(row.flags, 1); + // start_time is set -> Some(positive ms). + assert!(row.start_time.is_some()); + assert!(row.start_time.unwrap_or(0) > 0); + assert_eq!(row.value, Some(4242.0)); + assert_eq!( + row.attributes, + vec![("route".to_string(), "/api/v1".to_string())] + ); + // Sums carry no histogram arrays. + assert!(row.histogram_bounds.is_empty()); + assert!(row.exp_positive_counts.is_empty()); + assert!(row.summary_quantiles.is_empty()); + } + + #[test] + fn exp_histogram_point_to_ch_row_field_mapping() { + let row = ChMetricRow::from(&make_exp_histogram()); + + assert_eq!(row.metric_type, "exponential_histogram"); + assert_eq!(row.temporality, "delta"); + assert_eq!(row.histogram_count, Some(8)); + assert_eq!(row.histogram_sum, Some(40.0)); + assert_eq!(row.histogram_min, Some(0.1)); + assert_eq!(row.histogram_max, Some(10.0)); + assert_eq!(row.exp_scale, Some(3)); + assert_eq!(row.exp_zero_count, Some(1)); + assert_eq!(row.exp_zero_threshold, Some(1e-6)); + assert_eq!(row.exp_positive_offset, Some(2)); + assert_eq!(row.exp_positive_counts, vec![1, 2, 3]); + assert_eq!(row.exp_negative_offset, Some(-1)); + assert_eq!(row.exp_negative_counts, vec![4, 5]); + // Explicit-histogram bound arrays remain empty sentinels here. + assert!(row.histogram_bounds.is_empty()); + assert!(row.histogram_bucket_counts.is_empty()); + // is_monotonic is non-Sum -> None. + assert_eq!(row.is_monotonic, None); + } + + #[test] + fn summary_point_to_ch_row_field_mapping() { + let row = ChMetricRow::from(&make_summary()); + + assert_eq!(row.metric_type, "summary"); + // Summaries do not report temporality -> sentinel. + assert_eq!(row.temporality, "unspecified"); + assert_eq!(row.histogram_count, Some(4)); + assert_eq!(row.histogram_sum, Some(20.0)); + assert_eq!(row.summary_quantiles, vec![(0.5, 4.0), (0.99, 9.0)]); + // Summaries carry no explicit/exponential histogram arrays. + assert!(row.histogram_bounds.is_empty()); + assert!(row.exp_positive_counts.is_empty()); + assert!(row.exemplars.is_empty()); + } + + #[test] + fn point_with_exemplars_and_labels_full_mapping() { + // A gauge carrying both labels AND multiple exemplars, asserting that the + // exemplar tuples and the attribute Map both survive in full. + let ts = Utc::now(); + let mut p = MetricPoint::skeleton( + 7, + Some(3), + ResourceInfo::default(), + "db.pool.in_use".into(), + MetricType::Gauge, + "1".into(), + ts, + { + let mut m = BTreeMap::new(); + m.insert("pool".into(), "primary".into()); + m.insert("db.system".into(), "postgresql".into()); + m + }, + ); + p.exemplars = vec![ + Exemplar { + timestamp: ts, + value: 12.0, + trace_id: Some("aabbccdd".into()), + span_id: Some("1122".into()), + attributes: BTreeMap::new(), + }, + Exemplar { + timestamp: ts, + value: 13.0, + // An exemplar with no trace/span link -> empty-string sentinels. + trace_id: None, + span_id: None, + attributes: BTreeMap::new(), + }, + ]; + p.value = Some(11.0); + + let row = ChMetricRow::from(&p); + + // Attributes Map preserved in sorted (BTreeMap) key order. + assert_eq!( + row.attributes, + vec![ + ("db.system".to_string(), "postgresql".to_string()), + ("pool".to_string(), "primary".to_string()), + ] + ); + // Both exemplars survive; the second one's missing IDs become "". + assert_eq!(row.exemplars.len(), 2); + assert_eq!(row.exemplars[0].0, "aabbccdd"); + assert_eq!(row.exemplars[0].1, "1122"); + assert_eq!(row.exemplars[0].2, 12.0); + assert!(row.exemplars[0].3 > 0); + assert_eq!(row.exemplars[1].0, ""); + assert_eq!(row.exemplars[1].1, ""); + assert_eq!(row.exemplars[1].2, 13.0); + assert_eq!(row.value, Some(11.0)); + } + + #[test] + fn ch_metric_row_field_order_matches_ddl_columns() { + // SERIALIZATION LANDMINE GUARD. + // + // ChMetricRow is serialised positionally over RowBinary; a mismatch + // between the struct field order and the `metrics` DDL column order + // silently corrupts every insert. This test parses the column list out + // of 0003_metrics.sql and asserts it equals the ChMetricRow field order + // (derived from clickhouse::Row), so a future reorder of either side + // fails loudly here instead of corrupting data at runtime. + let ddl = include_str!("../../../migrations/clickhouse/0003_metrics.sql"); + + // Extract the column names from the `CREATE TABLE metrics ( ... )` body. + let create_start = ddl + .find("CREATE TABLE IF NOT EXISTS metrics") + .expect("DDL must declare the metrics table"); + let body_start = ddl[create_start..] + .find('(') + .map(|i| create_start + i + 1) + .expect("CREATE TABLE must have an opening paren"); + // The column body ends at the matching `)` that precedes `ENGINE`. + let engine_pos = ddl[body_start..] + .find("ENGINE") + .map(|i| body_start + i) + .expect("DDL must declare an ENGINE"); + let body = &ddl[body_start..engine_pos]; + + let mut ddl_columns: Vec = Vec::new(); + for raw_line in body.lines() { + let line = raw_line.trim(); + // Skip blanks and whole-line comments. + if line.is_empty() || line.starts_with("--") { + continue; + } + // MATERIALIZED / ALIAS columns are computed by ClickHouse and are NOT + // part of the positional RowBinary insert, so they are not ChMetricRow + // fields — skip them (e.g. attributes_hash, the series fingerprint). + if line.contains(" MATERIALIZED ") || line.contains(" ALIAS ") { + continue; + } + // The first whitespace-delimited token on a column line is the name. + // Lines inside the body are always `column_name Type ...,`. + let token = line.split_whitespace().next().unwrap_or(""); + // Guard against any stray non-identifier tokens. + if token.is_empty() || !token.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + continue; + } + ddl_columns.push(token.to_string()); + } + + // The ChMetricRow field order, declared once here and kept in lockstep + // with the struct definition above. Any change to the struct field list + // (or the DDL) must update this and will be caught by the two asserts. + let row_fields = [ + "project_id", + "deployment_id", + "service_name", + "service_version", + "deployment_environment", + "metric_name", + "metric_type", + "temporality", + "is_monotonic", + "unit", + "description", + "timestamp", + "start_time", + "flags", + "value", + "histogram_count", + "histogram_sum", + "histogram_min", + "histogram_max", + "histogram_bounds", + "histogram_bucket_counts", + "exp_scale", + "exp_zero_count", + "exp_zero_threshold", + "exp_positive_offset", + "exp_positive_counts", + "exp_negative_offset", + "exp_negative_counts", + "summary_quantiles", + "exemplars", + "attributes", + "_version", + ]; + + // 31 domain columns + _version sentinel. + assert_eq!( + row_fields.len(), + CH_METRIC_ROW_FIELD_COUNT + 1, + "row_fields must list every ChMetricRow field incl _version" + ); + assert_eq!( + ddl_columns.len(), + row_fields.len(), + "DDL column count ({}) must equal ChMetricRow field count ({}). \ + DDL columns parsed: {:?}", + ddl_columns.len(), + row_fields.len(), + ddl_columns + ); + for (i, (ddl_col, row_field)) in ddl_columns.iter().zip(row_fields.iter()).enumerate() { + assert_eq!( + ddl_col, row_field, + "Column #{i} mismatch: DDL has `{ddl_col}` but ChMetricRow has `{row_field}`. \ + Positional RowBinary serialisation requires EXACT order — fix the struct or DDL." + ); + } + } + + // ── translate_bucket_interval tests ───────────────────────────────────── + + #[test] + fn translate_bucket_interval_known_units() { + assert_eq!(translate_bucket_interval("1 hour"), "INTERVAL 1 HOUR"); + assert_eq!(translate_bucket_interval("5 minutes"), "INTERVAL 5 MINUTE"); + assert_eq!( + translate_bucket_interval("30 seconds"), + "INTERVAL 30 SECOND" + ); + assert_eq!(translate_bucket_interval("1 day"), "INTERVAL 1 DAY"); + assert_eq!(translate_bucket_interval("2 weeks"), "INTERVAL 2 WEEK"); + // Singular + abbreviations. + assert_eq!(translate_bucket_interval("15 min"), "INTERVAL 15 MINUTE"); + } + + #[test] + fn translate_bucket_interval_rejects_injection_and_garbage() { + // Injection attempts and malformed inputs fall back to the safe default. + assert_eq!( + translate_bucket_interval("1 hour; DROP TABLE metrics"), + "INTERVAL 1 HOUR" + ); + assert_eq!(translate_bucket_interval("abc def"), "INTERVAL 1 HOUR"); + assert_eq!(translate_bucket_interval(""), "INTERVAL 1 HOUR"); + assert_eq!(translate_bucket_interval("0 hours"), "INTERVAL 1 HOUR"); + assert_eq!(translate_bucket_interval("-5 hours"), "INTERVAL 1 HOUR"); + assert_eq!( + translate_bucket_interval("999999999 hours"), + "INTERVAL 1 HOUR" + ); + assert_eq!(translate_bucket_interval("1 fortnight"), "INTERVAL 1 HOUR"); + } + + #[test] + fn store_metrics_query_shapes_use_unbound_metrics_table() { + // Lightweight guard that the native metric SQL targets the new `metrics` + // table and bucketing helper (not the Timescale otel_metrics path). + let interval = translate_bucket_interval( + MetricQuery::default() + .bucket_interval + .as_deref() + .unwrap_or("1 hour"), + ); + assert_eq!(interval, "INTERVAL 1 HOUR"); + } + + // ── interval_seconds (RatePerSec divisor) ─────────────────────────────── + + #[test] + fn interval_seconds_parses_canonical_fragments() { + assert_eq!(interval_seconds("INTERVAL 1 SECOND"), 1); + assert_eq!(interval_seconds("INTERVAL 30 SECOND"), 30); + assert_eq!(interval_seconds("INTERVAL 5 MINUTE"), 300); + assert_eq!(interval_seconds("INTERVAL 1 HOUR"), 3600); + assert_eq!(interval_seconds("INTERVAL 2 DAY"), 172_800); + assert_eq!(interval_seconds("INTERVAL 1 WEEK"), 604_800); + } + + #[test] + fn interval_seconds_falls_back_to_one_hour_on_garbage() { + assert_eq!(interval_seconds(""), 3600); + assert_eq!(interval_seconds("5 MINUTE"), 3600); // missing INTERVAL keyword + assert_eq!(interval_seconds("INTERVAL x HOUR"), 3600); + assert_eq!(interval_seconds("INTERVAL 5 FORTNIGHT"), 3600); + } + + #[test] + fn interval_seconds_roundtrips_with_translate() { + // The two helpers must agree: translate produces the canonical fragment + // that interval_seconds parses back. + assert_eq!( + interval_seconds(&translate_bucket_interval("5 minutes")), + 300 + ); + assert_eq!( + interval_seconds(&translate_bucket_interval("1 day")), + 86_400 + ); + } } diff --git a/crates/temps-otel/src/storage/timescaledb.rs b/crates/temps-otel/src/storage/timescaledb.rs index 3399d9ecb..620cb759c 100644 --- a/crates/temps-otel/src/storage/timescaledb.rs +++ b/crates/temps-otel/src/storage/timescaledb.rs @@ -693,13 +693,7 @@ impl OtelStorage for TimescaleDbStorage { Ok(rows .into_iter() - .map(|r| MetricBucket { - bucket: r.bucket, - avg_value: r.avg_value, - min_value: r.min_value, - max_value: r.max_value, - count: r.count, - }) + .map(|r| MetricBucket::scalar(r.bucket, r.avg_value, r.min_value, r.max_value, r.count)) .collect()) } diff --git a/crates/temps-otel/src/types.rs b/crates/temps-otel/src/types.rs index e38b214ba..dc75a794e 100644 --- a/crates/temps-otel/src/types.rs +++ b/crates/temps-otel/src/types.rs @@ -66,6 +66,8 @@ pub enum MetricType { Gauge, Sum, Histogram, + ExponentialHistogram, + Summary, } impl std::fmt::Display for MetricType { @@ -74,10 +76,164 @@ impl std::fmt::Display for MetricType { MetricType::Gauge => write!(f, "gauge"), MetricType::Sum => write!(f, "sum"), MetricType::Histogram => write!(f, "histogram"), + MetricType::ExponentialHistogram => write!(f, "exponential_histogram"), + MetricType::Summary => write!(f, "summary"), } } } +/// The aggregation applied when reducing raw metric points into a time bucket. +/// +/// Store-neutral: every storage backend (ClickHouse today, TimescaleDB later) +/// must be able to satisfy this contract. `Quantile(q)` carries the requested +/// quantile in `[0.0, 1.0]` (e.g. `0.95` for p95). +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum MetricAggregation { + /// Arithmetic mean of the scalar value in each bucket. The default. + #[default] + Avg, + /// Sum of the scalar value in each bucket. + Sum, + /// Minimum scalar value in each bucket. + Min, + /// Maximum scalar value in each bucket. + Max, + /// Number of points in each bucket. + Count, + /// Per-second rate of change of a cumulative monotonic counter, computed as + /// `(max - min) / window_seconds` within each bucket. Non-monotonic series + /// fall back to a simple delta. + RatePerSec, + /// A quantile of the scalar value in each bucket. The carried `f64` is the + /// requested quantile in `[0.0, 1.0]`. + #[serde(untagged)] + Quantile(f64), +} + +impl std::fmt::Display for MetricAggregation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MetricAggregation::Avg => write!(f, "avg"), + MetricAggregation::Sum => write!(f, "sum"), + MetricAggregation::Min => write!(f, "min"), + MetricAggregation::Max => write!(f, "max"), + MetricAggregation::Count => write!(f, "count"), + MetricAggregation::RatePerSec => write!(f, "rate_per_sec"), + MetricAggregation::Quantile(q) => write!(f, "quantile({q})"), + } + } +} + +impl MetricAggregation { + /// Parse a query-string aggregation token into a typed aggregation. + /// + /// Accepts the keyword forms (`avg`, `sum`, `min`, `max`, `count`, `rate`) + /// and quantile forms `p50`/`p95`/`p99` and `quantile:0.95` / `q0.95`. + /// Unknown or out-of-range inputs fall back to [`MetricAggregation::Avg`]. + pub fn parse(s: &str) -> Self { + let lower = s.trim().to_ascii_lowercase(); + match lower.as_str() { + "avg" | "average" | "mean" => return MetricAggregation::Avg, + "sum" | "total" => return MetricAggregation::Sum, + "min" | "minimum" => return MetricAggregation::Min, + "max" | "maximum" => return MetricAggregation::Max, + "count" => return MetricAggregation::Count, + "rate" | "rate_per_sec" | "ratepersec" => return MetricAggregation::RatePerSec, + _ => {} + } + // pNN shorthand (p50 -> 0.50, p95 -> 0.95, p999 -> 0.999, p99.9 -> 0.999) + if let Some(rest) = lower.strip_prefix('p') { + if let Ok(on) = rest.parse::() { + let q = if rest.contains('.') { + on / 100.0 + } else { + on / 10f64.powi(rest.len() as i32) + }; + if (0.0..=1.0).contains(&q) { + return MetricAggregation::Quantile(q); + } + } + } + // quantile:0.95 / q0.95 / quantile(0.95) + for prefix in ["quantile:", "quantile(", "q"] { + if let Some(rest) = lower.strip_prefix(prefix) { + let cleaned = rest.trim_end_matches(')'); + if let Ok(q) = cleaned.parse::() { + if (0.0..=1.0).contains(&q) { + return MetricAggregation::Quantile(q); + } + } + } + } + MetricAggregation::Avg + } + + /// The clamped quantile for a `Quantile` aggregation, else `None`. + pub fn quantile(&self) -> Option { + match self { + MetricAggregation::Quantile(q) => Some(q.clamp(0.0, 1.0)), + _ => None, + } + } +} + +/// The aggregation temporality of a Sum/Histogram/ExponentialHistogram metric. +/// +/// Mirrors OTel's `AggregationTemporality` proto enum: whether reported values +/// are cumulative since the start of the series (Cumulative) or only the delta +/// since the previous report (Delta). +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum AggregationTemporality { + /// Temporality not reported by the producer. + Unspecified, + /// Each value covers only the interval since the previous report. + Delta, + /// Each value is cumulative since the start of the series. + Cumulative, +} + +impl std::fmt::Display for AggregationTemporality { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AggregationTemporality::Unspecified => write!(f, "unspecified"), + AggregationTemporality::Delta => write!(f, "delta"), + AggregationTemporality::Cumulative => write!(f, "cumulative"), + } + } +} + +impl AggregationTemporality { + /// Map the raw protobuf `aggregation_temporality` i32 to our enum. + /// + /// Unknown values fall back to `Unspecified` so unexpected producers never + /// cause a decode failure. + pub fn from_proto(value: i32) -> Self { + match value { + 1 => AggregationTemporality::Delta, + 2 => AggregationTemporality::Cumulative, + _ => AggregationTemporality::Unspecified, + } + } +} + +/// A single exemplar — a sampled measurement linking a metric point back to a +/// trace/span that contributed to it. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct Exemplar { + #[schema(value_type = String, format = DateTime)] + pub timestamp: DateTime, + /// The raw measured value of this exemplar. + pub value: f64, + /// The trace this exemplar links to, hex-encoded (absent when empty). + pub trace_id: Option, + /// The span this exemplar links to, hex-encoded (absent when empty). + pub span_id: Option, + /// Filtered attributes attached to this exemplar. + pub attributes: BTreeMap, +} + /// A single metric data point ready for storage. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct MetricPoint { @@ -87,13 +243,30 @@ pub struct MetricPoint { pub metric_name: String, pub metric_type: MetricType, pub unit: String, + /// Human-readable description from the OTel `Metric.description` field. + #[serde(default)] + pub description: Option, #[schema(value_type = String, format = DateTime)] pub timestamp: DateTime, + /// Start of the interval this point covers (from `start_time_unix_nano`). + /// Used for delta/cumulative reasoning; absent when the producer omits it. + #[serde(default)] + #[schema(value_type = Option, format = DateTime)] + pub start_time: Option>, + /// Aggregation temporality (Sum/Histogram/ExponentialHistogram only). + #[serde(default)] + pub temporality: Option, + /// Whether a Sum is monotonic (counter vs up-down counter). `None` for non-Sum. + #[serde(default)] + pub is_monotonic: Option, + /// Data-point flags bitmask (e.g. NO_RECORDED_VALUE) from the OTLP point. + #[serde(default)] + pub flags: u32, /// For Gauge/Sum: the scalar value. pub value: Option, - /// For Histogram: count of observations. + /// For Histogram/Summary/ExponentialHistogram: count of observations. pub histogram_count: Option, - /// For Histogram: sum of observations. + /// For Histogram/Summary/ExponentialHistogram: sum of observations. pub histogram_sum: Option, /// For Histogram: min value. pub histogram_min: Option, @@ -103,10 +276,89 @@ pub struct MetricPoint { pub histogram_bounds: Option>, /// For Histogram: count per bucket. pub histogram_bucket_counts: Option>, + // ── ExponentialHistogram fields ────────────────────────────────── + /// Resolution scale of the exponential buckets. + #[serde(default)] + pub exp_scale: Option, + /// Count of values that are exactly zero (or within the zero threshold). + #[serde(default)] + pub exp_zero_count: Option, + /// The threshold below which values are counted in `exp_zero_count`. + #[serde(default)] + pub exp_zero_threshold: Option, + /// Bucket offset for the positive range. + #[serde(default)] + pub exp_positive_offset: Option, + /// Per-bucket counts for the positive range. + #[serde(default)] + pub exp_positive_counts: Option>, + /// Bucket offset for the negative range. + #[serde(default)] + pub exp_negative_offset: Option, + /// Per-bucket counts for the negative range. + #[serde(default)] + pub exp_negative_counts: Option>, + // ── Summary fields ─────────────────────────────────────────────── + /// Summary quantile/value pairs `(quantile, value)`. + #[serde(default)] + pub summary_quantiles: Option>, + /// Exemplars sampled for this data point, linking back to traces. + #[serde(default)] + pub exemplars: Vec, /// Attribute labels on this data point. pub attributes: BTreeMap, } +impl MetricPoint { + /// Construct a minimal Gauge-shaped `MetricPoint` with all optional + /// full-fidelity fields defaulted. Helpers (decode, tests) fill in only the + /// fields they care about; this keeps every call site compiling as the type + /// grows. + #[allow(clippy::too_many_arguments)] + pub fn skeleton( + project_id: i32, + deployment_id: Option, + resource: ResourceInfo, + metric_name: String, + metric_type: MetricType, + unit: String, + timestamp: DateTime, + attributes: BTreeMap, + ) -> Self { + Self { + project_id, + deployment_id, + resource, + metric_name, + metric_type, + unit, + description: None, + timestamp, + start_time: None, + temporality: None, + is_monotonic: None, + flags: 0, + value: None, + histogram_count: None, + histogram_sum: None, + histogram_min: None, + histogram_max: None, + histogram_bounds: None, + histogram_bucket_counts: None, + exp_scale: None, + exp_zero_count: None, + exp_zero_threshold: None, + exp_positive_offset: None, + exp_positive_counts: None, + exp_negative_offset: None, + exp_negative_counts: None, + summary_quantiles: None, + exemplars: Vec::new(), + attributes, + } + } +} + // ── Traces ─────────────────────────────────────────────────────────── /// Span status code. @@ -781,6 +1033,13 @@ impl GenAiSpanDetail { } /// Filter for querying metrics. +/// +/// This is the **store-neutral query contract** every backend must satisfy. +/// The basic shape (`metric_name`/`service_name`/`environment`/time window / +/// `bucket_interval`/`limit`) is honoured by both ClickHouse and TimescaleDB. +/// The richer options (`metric_type`, `label_filters`, `group_by`, +/// `aggregation`) are populated by the query API and are forward-compatible: +/// each is `#[serde(default)]` so older callers and JSON payloads still parse. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct MetricQuery { pub project_id: i32, @@ -791,6 +1050,22 @@ pub struct MetricQuery { pub end_time: Option>, pub bucket_interval: Option, pub limit: Option, + /// Restrict to a single metric type (gauge/sum/histogram/…). `None` = any. + #[serde(default)] + pub metric_type: Option, + /// Exact-match data-point label filters as `(key, value)` pairs. ANDead + /// together. Keys MUST pass the same allowlist as ingest before reaching a + /// store; values are always bound, never concatenated. + #[serde(default)] + pub label_filters: Vec<(String, String)>, + /// Label keys to group the series by, producing one bucket stream per + /// distinct label-set. Keys MUST pass the allowlist. Empty = no grouping + /// (one aggregate stream). + #[serde(default)] + pub group_by: Vec, + /// The aggregation reducing raw points into each bucket. Defaults to `Avg`. + #[serde(default)] + pub aggregation: MetricAggregation, } /// Filter for querying log records. @@ -807,7 +1082,31 @@ pub struct LogQuery { pub offset: Option, } +/// A compact summary of an explicit-bucket histogram aggregated over a bucket. +/// +/// Carries only the cheap reduced fields (count/sum/min/max). Full quantile +/// reconstruction from the bucket arrays is deferred; the raw bucket arrays +/// remain available in the store for a later read-side reconstruction. +#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)] +pub struct HistogramSummary { + /// Total observation count summed across the bucket window. + pub count: u64, + /// Sum of observed values across the bucket window. + pub sum: f64, + /// Minimum observed value, when reported by the producer. + pub min: Option, + /// Maximum observed value, when reported by the producer. + pub max: Option, +} + /// A time-bucketed metric aggregate for chart display. +/// +/// Store-neutral response contract. The legacy scalar fields +/// (`avg_value`/`min_value`/`max_value`/`count`) are always populated for chart +/// back-compat. The richer fields describe the explicitly-requested +/// [`MetricAggregation`] (`value`), optional `quantiles`, an optional +/// `histogram_summary`, and a `series_key` identifying the label-set when the +/// query used `group_by`. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct MetricBucket { #[schema(value_type = String, format = DateTime)] @@ -816,6 +1115,48 @@ pub struct MetricBucket { pub min_value: f64, pub max_value: f64, pub count: i64, + /// The value of the requested [`MetricAggregation`] for this bucket. For the + /// default `Avg` aggregation this equals `avg_value`. `#[serde(default)]` so + /// pre-existing payloads (which only carried avg/min/max/count) still parse. + #[serde(default)] + pub value: f64, + /// Computed quantile/value pairs `(quantile, value)` when the query asked for + /// quantile aggregation; otherwise empty. + #[serde(default)] + pub quantiles: Vec<(f64, f64)>, + /// A reduced histogram summary when the bucketed metric is a histogram. + #[serde(default)] + pub histogram_summary: Option, + /// The label-set this bucket belongs to, as ordered `(key, value)` pairs, + /// when the query grouped by labels. Empty/`None` = the single ungrouped + /// aggregate stream. + #[serde(default)] + pub series_key: Option>, +} + +impl MetricBucket { + /// Construct a scalar bucket from the four legacy aggregate columns, + /// defaulting the richer fields. `value` is set to `avg_value` so callers + /// that don't request a specific aggregation get a sensible default. + pub fn scalar( + bucket: DateTime, + avg_value: f64, + min_value: f64, + max_value: f64, + count: i64, + ) -> Self { + Self { + bucket, + avg_value, + min_value, + max_value, + count, + value: avg_value, + quantiles: Vec::new(), + histogram_summary: None, + series_key: None, + } + } } /// Quota usage information for a project. @@ -897,6 +1238,173 @@ mod tests { assert_eq!(MetricType::Gauge.to_string(), "gauge"); assert_eq!(MetricType::Sum.to_string(), "sum"); assert_eq!(MetricType::Histogram.to_string(), "histogram"); + assert_eq!( + MetricType::ExponentialHistogram.to_string(), + "exponential_histogram" + ); + assert_eq!(MetricType::Summary.to_string(), "summary"); + } + + #[test] + fn test_aggregation_temporality_display() { + assert_eq!( + AggregationTemporality::Unspecified.to_string(), + "unspecified" + ); + assert_eq!(AggregationTemporality::Delta.to_string(), "delta"); + assert_eq!(AggregationTemporality::Cumulative.to_string(), "cumulative"); + } + + #[test] + fn test_aggregation_temporality_from_proto() { + assert_eq!( + AggregationTemporality::from_proto(0), + AggregationTemporality::Unspecified + ); + assert_eq!( + AggregationTemporality::from_proto(1), + AggregationTemporality::Delta + ); + assert_eq!( + AggregationTemporality::from_proto(2), + AggregationTemporality::Cumulative + ); + // Unknown values fall back to Unspecified. + assert_eq!( + AggregationTemporality::from_proto(99), + AggregationTemporality::Unspecified + ); + } + + #[test] + fn test_aggregation_temporality_serde_roundtrip() { + let json = serde_json::to_string(&AggregationTemporality::Cumulative).unwrap(); + assert_eq!(json, "\"cumulative\""); + let parsed: AggregationTemporality = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed, AggregationTemporality::Cumulative); + } + + // ── MetricAggregation ─────────────────────────────────────────── + + #[test] + fn test_metric_aggregation_default_is_avg() { + assert_eq!(MetricAggregation::default(), MetricAggregation::Avg); + } + + #[test] + fn test_metric_aggregation_parse_keywords() { + assert_eq!(MetricAggregation::parse("avg"), MetricAggregation::Avg); + assert_eq!(MetricAggregation::parse("MEAN"), MetricAggregation::Avg); + assert_eq!(MetricAggregation::parse("sum"), MetricAggregation::Sum); + assert_eq!(MetricAggregation::parse("min"), MetricAggregation::Min); + assert_eq!(MetricAggregation::parse("max"), MetricAggregation::Max); + assert_eq!(MetricAggregation::parse("count"), MetricAggregation::Count); + assert_eq!( + MetricAggregation::parse("rate"), + MetricAggregation::RatePerSec + ); + // Unknown falls back to Avg. + assert_eq!(MetricAggregation::parse("bogus"), MetricAggregation::Avg); + } + + #[test] + fn test_metric_aggregation_parse_quantiles() { + assert_eq!( + MetricAggregation::parse("p50"), + MetricAggregation::Quantile(0.5) + ); + assert_eq!( + MetricAggregation::parse("p95"), + MetricAggregation::Quantile(0.95) + ); + assert_eq!( + MetricAggregation::parse("p99"), + MetricAggregation::Quantile(0.99) + ); + assert_eq!( + MetricAggregation::parse("p999"), + MetricAggregation::Quantile(0.999) + ); + assert_eq!( + MetricAggregation::parse("quantile:0.95"), + MetricAggregation::Quantile(0.95) + ); + assert_eq!( + MetricAggregation::parse("q0.9"), + MetricAggregation::Quantile(0.9) + ); + // Explicit out-of-range quantile falls back to Avg. + assert_eq!( + MetricAggregation::parse("quantile:2.0"), + MetricAggregation::Avg + ); + assert_eq!(MetricAggregation::parse("q1.5"), MetricAggregation::Avg); + } + + #[test] + fn test_metric_aggregation_quantile_accessor_clamps() { + assert_eq!(MetricAggregation::Avg.quantile(), None); + assert_eq!(MetricAggregation::Quantile(0.95).quantile(), Some(0.95)); + // Out-of-range stored value is clamped on read. + assert_eq!(MetricAggregation::Quantile(1.5).quantile(), Some(1.0)); + assert_eq!(MetricAggregation::Quantile(-0.5).quantile(), Some(0.0)); + } + + #[test] + fn test_metric_query_default_contract_fields() { + let q = MetricQuery::default(); + assert!(q.metric_type.is_none()); + assert!(q.label_filters.is_empty()); + assert!(q.group_by.is_empty()); + assert_eq!(q.aggregation, MetricAggregation::Avg); + } + + #[test] + fn test_metric_bucket_scalar_defaults_rich_fields() { + let now = chrono::Utc::now(); + let b = MetricBucket::scalar(now, 10.0, 5.0, 20.0, 3); + assert_eq!(b.avg_value, 10.0); + assert_eq!(b.min_value, 5.0); + assert_eq!(b.max_value, 20.0); + assert_eq!(b.count, 3); + // `value` defaults to avg; richer fields are empty/None. + assert_eq!(b.value, 10.0); + assert!(b.quantiles.is_empty()); + assert!(b.histogram_summary.is_none()); + assert!(b.series_key.is_none()); + } + + #[test] + fn test_metric_bucket_backcompat_deserialize_without_rich_fields() { + // A payload from before the richer fields existed must still parse, + // defaulting value/quantiles/histogram_summary/series_key. + let json = r#"{"bucket":"2026-01-01T00:00:00Z","avg_value":1.0,"min_value":0.0,"max_value":2.0,"count":5}"#; + let b: MetricBucket = serde_json::from_str(json).unwrap(); + assert_eq!(b.avg_value, 1.0); + assert_eq!(b.value, 0.0); // default, no value key present + assert!(b.quantiles.is_empty()); + assert!(b.series_key.is_none()); + } + + #[test] + fn test_metric_point_skeleton_defaults() { + let p = MetricPoint::skeleton( + 7, + None, + ResourceInfo::default(), + "m".into(), + MetricType::Gauge, + "1".into(), + chrono::Utc::now(), + BTreeMap::new(), + ); + assert_eq!(p.project_id, 7); + assert!(p.temporality.is_none()); + assert!(p.is_monotonic.is_none()); + assert_eq!(p.flags, 0); + assert!(p.exemplars.is_empty()); + assert!(p.summary_quantiles.is_none()); + assert!(p.exp_scale.is_none()); } #[test] diff --git a/crates/temps-otel/tests/clickhouse_storage_test.rs b/crates/temps-otel/tests/clickhouse_storage_test.rs new file mode 100644 index 000000000..dca46fcfd --- /dev/null +++ b/crates/temps-otel/tests/clickhouse_storage_test.rs @@ -0,0 +1,389 @@ +//! Real-ClickHouse integration test for the OTel metrics storage path. +//! +//! Spins up a `clickhouse/clickhouse-server` testcontainer, runs the OTel CH +//! migrations (which create the `metrics` table via `0003_metrics.sql`), then +//! round-trips a Gauge and a Histogram `MetricPoint` through the native +//! `ClickHouseOtelStorage` metric methods: +//! +//! store_metrics -> query_metrics + list_metric_names +//! +//! If Docker is not reachable the test skips gracefully (per CLAUDE.md: Docker +//! tests must NEVER be `#[ignore]`d — they detect unavailability at runtime and +//! return). +//! +//! The inner `TimescaleDbStorage` is wired to a sea-orm `MockDatabase` because +//! the metric methods under test (`store_metrics`, `query_metrics`, +//! `list_metric_names`) read/write only ClickHouse and never touch the inner +//! Postgres storage. No Postgres container is required. + +use std::collections::BTreeMap; +use std::sync::Arc; + +use chrono::Utc; +use sea_orm::{DatabaseBackend, MockDatabase}; + +use temps_otel::storage::clickhouse::{ClickHouseOtelConfig, ClickHouseOtelStorage}; +use temps_otel::storage::timescaledb::TimescaleDbStorage; +use temps_otel::storage::OtelStorage; +use temps_otel::types::{MetricAggregation, MetricPoint, MetricQuery, MetricType, ResourceInfo}; + +/// Container handle + a connected `ClickHouseOtelStorage`. Returns `None` when +/// Docker is unavailable so the test can skip without failing CI. +async fn setup() -> Option<(ClickHouseOtelStorage, Box)> { + use testcontainers::{ + core::{wait::HttpWaitStrategy, ContainerPort, WaitFor}, + runners::AsyncRunner, + GenericImage, ImageExt, + }; + + let image = GenericImage::new("clickhouse/clickhouse-server", "24.8") + .with_exposed_port(ContainerPort::Tcp(8123)) + // The clickhouse-server image writes "Ready for connections" only to its + // in-container log file — never to stdout/stderr — so a log-message wait + // always times out and the test silently skips. Wait on the HTTP /ping + // endpoint (returns 200 "Ok." once the server accepts queries) instead. + .with_wait_for(WaitFor::http( + HttpWaitStrategy::new("/ping") + .with_port(ContainerPort::Tcp(8123)) + .with_expected_status_code(200u16), + )) + .with_env_var("CLICKHOUSE_DB", "temps_otel_test") + // Do NOT set CLICKHOUSE_USER=default (the image's user-init then rejects + // the pre-existing default user) and do NOT use an empty password (an + // empty CLICKHOUSE_PASSWORD leaves `default` unauthenticatable in 24.8). + .with_env_var("CLICKHOUSE_PASSWORD", "test"); + + let container = match image.start().await { + Ok(c) => c, + Err(e) => { + eprintln!("Skipping ClickHouse OTel metrics test: cannot start container ({e})"); + return None; + } + }; + + let host_port = match container.get_host_port_ipv4(8123).await { + Ok(p) => p, + Err(e) => { + eprintln!("Skipping ClickHouse OTel metrics test: cannot get host port ({e})"); + return None; + } + }; + + let url = format!("http://127.0.0.1:{host_port}"); + let config = ClickHouseOtelConfig::new(&url, "temps_otel_test", "default", "test"); + + // A bare client for the migration runner + readiness probe. + let probe = ::clickhouse::Client::default() + .with_url(&url) + .with_database("temps_otel_test") + .with_user("default") + .with_password("test"); + + // Wait until the HTTP listener actually accepts queries. + let mut last_err = String::new(); + for _ in 0..30 { + match probe.query("SELECT 1").execute().await { + Ok(_) => { + last_err.clear(); + break; + } + Err(e) => { + last_err = format!("{e}"); + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + } + } + if !last_err.is_empty() { + eprintln!("Skipping ClickHouse OTel metrics test: server never became ready ({last_err})"); + return None; + } + + // Apply OTel CH migrations (spans + metrics). This MUST succeed — it is the + // entire reason for the test. Assert loudly on failure. + temps_otel::storage::clickhouse::migrations::apply_migrations(&probe, "temps_otel_test") + .await + .expect("apply_migrations failed against testcontainer ClickHouse"); + + // The inner Timescale storage is never exercised by the metric methods under + // test; a MockDatabase satisfies the constructor without a Postgres server. + let mock_db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); + let inner = Arc::new(TimescaleDbStorage::new(Arc::new(mock_db), None)); + + let storage = ClickHouseOtelStorage::new(config, inner); + Some((storage, Box::new(container))) +} + +fn gauge_point() -> MetricPoint { + let mut p = MetricPoint::skeleton( + 101, + Some(9), + ResourceInfo { + service_name: "checkout".into(), + service_version: Some("2.0.0".into()), + deployment_environment: Some("production".into()), + attributes: BTreeMap::new(), + }, + "http.server.active_requests".into(), + MetricType::Gauge, + "1".into(), + Utc::now(), + { + let mut m = BTreeMap::new(); + m.insert("http.method".into(), "POST".into()); + m + }, + ); + p.value = Some(7.0); + p +} + +fn histogram_point() -> MetricPoint { + let mut p = MetricPoint::skeleton( + 101, + Some(9), + ResourceInfo { + service_name: "checkout".into(), + service_version: Some("2.0.0".into()), + deployment_environment: Some("production".into()), + attributes: BTreeMap::new(), + }, + "http.server.duration".into(), + MetricType::Histogram, + "ms".into(), + Utc::now(), + BTreeMap::new(), + ); + p.histogram_count = Some(4); + p.histogram_sum = Some(400.0); + p.histogram_min = Some(10.0); + p.histogram_max = Some(200.0); + p.histogram_bounds = Some(vec![0.0, 50.0, 100.0]); + p.histogram_bucket_counts = Some(vec![1, 1, 1, 1]); + // Synthetic scalar value (mean) so query_metrics has a number to aggregate. + p.value = Some(100.0); + p +} + +#[tokio::test] +async fn metrics_roundtrip_store_query_list() { + let Some((storage, _container)) = setup().await else { + return; // Docker unavailable — skip gracefully. + }; + + // 1. Store a Gauge + Histogram point. + let stored = storage + .store_metrics(vec![gauge_point(), histogram_point()]) + .await + .expect("store_metrics should succeed"); + assert_eq!(stored, 2, "both points should be written"); + + // 2. list_metric_names returns both distinct names, sorted. + let names = storage + .list_metric_names(101) + .await + .expect("list_metric_names should succeed"); + assert_eq!( + names, + vec![ + "http.server.active_requests".to_string(), + "http.server.duration".to_string(), + ], + "distinct metric names should round-trip, sorted" + ); + + // A different project sees nothing. + let other = storage + .list_metric_names(999) + .await + .expect("list_metric_names for empty project should succeed"); + assert!(other.is_empty(), "other project must see no metrics"); + + // 3. query_metrics on the gauge returns a bucket with the stored value. + let buckets = storage + .query_metrics(MetricQuery { + project_id: 101, + metric_name: Some("http.server.active_requests".into()), + bucket_interval: Some("1 hour".into()), + ..Default::default() + }) + .await + .expect("query_metrics should succeed"); + assert_eq!(buckets.len(), 1, "one gauge point -> one bucket"); + let b = &buckets[0]; + assert_eq!(b.count, 1); + assert!((b.avg_value - 7.0).abs() < f64::EPSILON); + assert!((b.min_value - 7.0).abs() < f64::EPSILON); + assert!((b.max_value - 7.0).abs() < f64::EPSILON); +} + +/// A gauge for `request.latency` carrying a single `http.method` label and a +/// scalar value, used to exercise group_by / label_filters / aggregation. +fn labelled_gauge(project_id: i32, method: &str, value: f64) -> MetricPoint { + let mut p = MetricPoint::skeleton( + project_id, + Some(9), + ResourceInfo { + service_name: "api".into(), + service_version: Some("1.0.0".into()), + deployment_environment: Some("production".into()), + attributes: BTreeMap::new(), + }, + "request.latency".into(), + MetricType::Gauge, + "ms".into(), + Utc::now(), + { + let mut m = BTreeMap::new(); + m.insert("http.method".into(), method.to_string()); + m + }, + ); + p.value = Some(value); + p +} + +#[tokio::test] +async fn query_metrics_aggregation_group_by_and_label_filter() { + let Some((storage, _container)) = setup().await else { + return; // Docker unavailable — skip gracefully. + }; + + // GET: values 10, 30 (avg 20, max 30); POST: value 100. + // Give each point a distinct millisecond timestamp: the two GET points share a + // series (project+metric+service+labels), so without distinct timestamps the + // ReplacingMergeTree series key (… , timestamp, attributes_hash) would treat + // them as one and keep only the latest. All three stay within the same hour + // bucket, so the grouped query still yields exactly two series. + let base = Utc::now(); + let mut points = vec![ + labelled_gauge(2002, "GET", 10.0), + labelled_gauge(2002, "GET", 30.0), + labelled_gauge(2002, "POST", 100.0), + ]; + points[0].timestamp = base; + points[1].timestamp = base + chrono::Duration::milliseconds(1); + points[2].timestamp = base + chrono::Duration::milliseconds(2); + let stored = storage + .store_metrics(points) + .await + .expect("store_metrics should succeed"); + assert_eq!(stored, 3); + + // 1. max aggregation, no grouping → single bucket, max across all = 100. + let buckets = storage + .query_metrics(MetricQuery { + project_id: 2002, + metric_name: Some("request.latency".into()), + bucket_interval: Some("1 hour".into()), + aggregation: MetricAggregation::Max, + ..Default::default() + }) + .await + .expect("max query should succeed"); + assert_eq!(buckets.len(), 1, "one time bucket"); + assert!( + (buckets[0].value - 100.0).abs() < f64::EPSILON, + "max value should be 100, got {}", + buckets[0].value + ); + + // 2. avg aggregation grouped by http.method → two series. + let grouped = storage + .query_metrics(MetricQuery { + project_id: 2002, + metric_name: Some("request.latency".into()), + bucket_interval: Some("1 hour".into()), + aggregation: MetricAggregation::Avg, + group_by: vec!["http.method".into()], + ..Default::default() + }) + .await + .expect("grouped query should succeed"); + assert_eq!(grouped.len(), 2, "one series per distinct method"); + for b in &grouped { + let key = b + .series_key + .as_ref() + .expect("grouped bucket must carry a series_key"); + assert_eq!(key.len(), 1); + assert_eq!(key[0].0, "http.method"); + match key[0].1.as_str() { + "GET" => assert!((b.value - 20.0).abs() < 1e-9, "GET avg should be 20"), + "POST" => assert!((b.value - 100.0).abs() < 1e-9, "POST avg should be 100"), + other => panic!("unexpected method label: {other}"), + } + } + + // 3. label_filters narrows to GET only → avg 20 over a single ungrouped series. + let filtered = storage + .query_metrics(MetricQuery { + project_id: 2002, + metric_name: Some("request.latency".into()), + bucket_interval: Some("1 hour".into()), + aggregation: MetricAggregation::Avg, + label_filters: vec![("http.method".into(), "GET".into())], + ..Default::default() + }) + .await + .expect("filtered query should succeed"); + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].count, 2, "only the two GET points"); + assert!((filtered[0].value - 20.0).abs() < 1e-9); + + // 4. p95 quantile aggregation populates the quantiles vec. + let quant = storage + .query_metrics(MetricQuery { + project_id: 2002, + metric_name: Some("request.latency".into()), + bucket_interval: Some("1 hour".into()), + aggregation: MetricAggregation::Quantile(0.95), + ..Default::default() + }) + .await + .expect("quantile query should succeed"); + assert_eq!(quant.len(), 1); + assert_eq!(quant[0].quantiles.len(), 1, "one (q, value) pair"); + assert!((quant[0].quantiles[0].0 - 0.95).abs() < f64::EPSILON); +} + +#[tokio::test] +async fn query_metrics_rejects_disallowed_label_key() { + let Some((storage, _container)) = setup().await else { + return; // Docker unavailable — skip gracefully. + }; + + // A group_by key with forbidden characters must be rejected before SQL. + let err = storage + .query_metrics(MetricQuery { + project_id: 2002, + group_by: vec!["evil key; DROP".into()], + ..Default::default() + }) + .await; + assert!(err.is_err(), "disallowed label key must be rejected"); +} + +#[tokio::test] +async fn store_metrics_drops_disallowed_name() { + let Some((storage, _container)) = setup().await else { + return; // Docker unavailable — skip gracefully. + }; + + // A metric whose name contains a SQL-injection payload must be dropped at + // the trust boundary and never written. + let mut bad = gauge_point(); + bad.metric_name = "evil'; DROP TABLE metrics; --".into(); + bad.project_id = 555; + + let stored = storage + .store_metrics(vec![bad]) + .await + .expect("store_metrics should not error on a dropped point"); + assert_eq!(stored, 0, "the disallowed-name point must be dropped"); + + let names = storage + .list_metric_names(555) + .await + .expect("list_metric_names should succeed"); + assert!(names.is_empty(), "nothing should have been written"); +} diff --git a/crates/temps-otel/tests/decode_to_store_test.rs b/crates/temps-otel/tests/decode_to_store_test.rs new file mode 100644 index 000000000..cc799023a --- /dev/null +++ b/crates/temps-otel/tests/decode_to_store_test.rs @@ -0,0 +1,377 @@ +//! End-to-end OTLP decode -> native ClickHouse store fidelity test. +//! +//! This proves the full claim "metrics are being stored": it feeds a *real* +//! `ExportMetricsServiceRequest` protobuf (the exact wire shape an OTel exporter +//! emits) through [`decode_metrics_request`] and then [`OtelStorage::store_metrics`] +//! into a live ClickHouse `metrics` table, and finally reads the rows back — +//! BOTH via the public `query_metrics`/`list_metric_names` API AND by selecting +//! the raw stored columns — asserting that full OTLP fidelity survives the whole +//! path: aggregation temporality, the `is_monotonic` counter flag, explicit +//! histogram bucket bounds/counts, and the data-point attribute labels. +//! +//! If Docker / ClickHouse is unreachable the test prints a skip message and +//! returns (per CLAUDE.md Docker tests MUST NOT be `#[ignore]`d — they detect +//! unavailability at runtime and skip gracefully). When skipped, the storage +//! roundtrip is NOT executed. +//! +//! The inner `TimescaleDbStorage` is a sea-orm `MockDatabase`: the metric +//! methods under test read/write only ClickHouse and never touch Postgres. + +use std::sync::Arc; + +use prost::Message; +use sea_orm::{DatabaseBackend, MockDatabase}; + +use temps_otel::ingest::decode::decode_metrics_request; +use temps_otel::proto; +use temps_otel::storage::clickhouse::{ClickHouseOtelConfig, ClickHouseOtelStorage}; +use temps_otel::storage::timescaledb::TimescaleDbStorage; +use temps_otel::storage::OtelStorage; +use temps_otel::types::{MetricAggregation, MetricQuery}; + +/// A raw row read straight back out of the `metrics` table, used to prove the +/// structured columns (temporality / is_monotonic / histogram arrays / +/// attributes) round-trip — not just the aggregated scalar value. +#[derive(::clickhouse::Row, serde::Deserialize, Debug)] +struct RawMetricRow { + metric_name: String, + metric_type: String, + temporality: String, + is_monotonic: Option, + unit: String, + value: Option, + histogram_count: Option, + histogram_sum: Option, + histogram_bounds: Vec, + histogram_bucket_counts: Vec, + attributes: Vec<(String, String)>, +} + +/// Build a realistic multi-metric OTLP export: a Gauge, a monotonic cumulative +/// Sum (counter) and a cumulative explicit Histogram, all under one resource and +/// carrying data-point labels. Returns the protobuf-encoded bytes exactly as an +/// exporter would POST them. +fn sample_export_request() -> Vec { + let kv = |k: &str, v: &str| proto::common::v1::KeyValue { + key: k.into(), + value: Some(proto::common::v1::AnyValue { + value: Some(proto::common::v1::any_value::Value::StringValue(v.into())), + }), + }; + + let resource = proto::resource::v1::Resource { + attributes: vec![ + kv("service.name", "checkout"), + kv("service.version", "2.0.0"), + kv("deployment.environment", "production"), + ], + dropped_attributes_count: 0, + }; + + let gauge = proto::metrics::v1::Metric { + name: "http.server.active_requests".into(), + description: "In-flight requests".into(), + unit: "1".into(), + data: Some(proto::metrics::v1::metric::Data::Gauge( + proto::metrics::v1::Gauge { + data_points: vec![proto::metrics::v1::NumberDataPoint { + time_unix_nano: 1_700_000_000_000_000_000, + value: Some(proto::metrics::v1::number_data_point::Value::AsDouble(7.0)), + attributes: vec![kv("http.method", "POST")], + ..Default::default() + }], + }, + )), + }; + + let counter = proto::metrics::v1::Metric { + name: "http.requests.total".into(), + description: "Total requests".into(), + unit: "1".into(), + data: Some(proto::metrics::v1::metric::Data::Sum( + proto::metrics::v1::Sum { + data_points: vec![proto::metrics::v1::NumberDataPoint { + start_time_unix_nano: 1_699_000_000_000_000_000, + time_unix_nano: 1_700_000_000_000_000_000, + value: Some(proto::metrics::v1::number_data_point::Value::AsInt(4242)), + attributes: vec![kv("route", "/checkout")], + ..Default::default() + }], + aggregation_temporality: 2, // CUMULATIVE + is_monotonic: true, + }, + )), + }; + + let histogram = proto::metrics::v1::Metric { + name: "http.server.duration".into(), + description: String::new(), + unit: "ms".into(), + data: Some(proto::metrics::v1::metric::Data::Histogram( + proto::metrics::v1::Histogram { + data_points: vec![proto::metrics::v1::HistogramDataPoint { + time_unix_nano: 1_700_000_000_000_000_000, + count: 10, + sum: Some(900.0), + min: Some(5.0), + max: Some(450.0), + explicit_bounds: vec![10.0, 100.0, 250.0], + bucket_counts: vec![2, 3, 4, 1], + attributes: vec![kv("http.method", "GET")], + ..Default::default() + }], + aggregation_temporality: 2, // CUMULATIVE + }, + )), + }; + + let request = proto::collector::metrics::v1::ExportMetricsServiceRequest { + resource_metrics: vec![proto::metrics::v1::ResourceMetrics { + resource: Some(resource), + scope_metrics: vec![proto::metrics::v1::ScopeMetrics { + scope: None, + metrics: vec![gauge, counter, histogram], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + + request.encode_to_vec() +} + +/// Spin a ClickHouse testcontainer, apply migrations, and return a connected +/// storage plus a bare read-back client. Returns `None` when Docker is +/// unavailable so the test skips gracefully. +async fn setup() -> Option<( + ClickHouseOtelStorage, + ::clickhouse::Client, + Box, +)> { + use testcontainers::{ + core::{wait::HttpWaitStrategy, ContainerPort, WaitFor}, + runners::AsyncRunner, + GenericImage, ImageExt, + }; + + let image = GenericImage::new("clickhouse/clickhouse-server", "24.8") + .with_exposed_port(ContainerPort::Tcp(8123)) + // The clickhouse-server image writes "Ready for connections" only to its + // in-container log file — never to stdout/stderr — so a log-message wait + // always times out and the test silently skips. Wait on the HTTP /ping + // endpoint (returns 200 "Ok." once the server accepts queries) instead. + .with_wait_for(WaitFor::http( + HttpWaitStrategy::new("/ping") + .with_port(ContainerPort::Tcp(8123)) + .with_expected_status_code(200u16), + )) + .with_env_var("CLICKHOUSE_DB", "temps_otel_e2e") + // Do NOT set CLICKHOUSE_USER=default (the image's user-init then rejects + // the pre-existing default user) and do NOT use an empty password (an + // empty CLICKHOUSE_PASSWORD leaves `default` unauthenticatable in 24.8). + .with_env_var("CLICKHOUSE_PASSWORD", "test"); + + let container = match image.start().await { + Ok(c) => c, + Err(e) => { + eprintln!("Skipping OTLP decode->store test: cannot start ClickHouse container ({e})"); + return None; + } + }; + + let host_port = match container.get_host_port_ipv4(8123).await { + Ok(p) => p, + Err(e) => { + eprintln!("Skipping OTLP decode->store test: cannot get host port ({e})"); + return None; + } + }; + + let url = format!("http://127.0.0.1:{host_port}"); + let config = ClickHouseOtelConfig::new(&url, "temps_otel_e2e", "default", "test"); + + let probe = ::clickhouse::Client::default() + .with_url(&url) + .with_database("temps_otel_e2e") + .with_user("default") + .with_password("test"); + + // Wait until the HTTP listener accepts queries. + let mut last_err = String::new(); + for _ in 0..30 { + match probe.query("SELECT 1").execute().await { + Ok(_) => { + last_err.clear(); + break; + } + Err(e) => { + last_err = format!("{e}"); + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + } + } + if !last_err.is_empty() { + eprintln!("Skipping OTLP decode->store test: server never became ready ({last_err})"); + return None; + } + + // Migrations MUST succeed — this is the whole point of the test. + temps_otel::storage::clickhouse::migrations::apply_migrations(&probe, "temps_otel_e2e") + .await + .expect("apply_migrations failed against testcontainer ClickHouse"); + + let mock_db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); + let inner = Arc::new(TimescaleDbStorage::new(Arc::new(mock_db), None)); + let storage = ClickHouseOtelStorage::new(config, inner); + + Some((storage, probe, Box::new(container))) +} + +#[tokio::test] +async fn otlp_decode_to_store_preserves_full_fidelity() { + let Some((storage, read_client, _container)) = setup().await else { + return; // Docker unavailable — skip gracefully. Roundtrip NOT executed. + }; + + const PROJECT_ID: i32 = 777; + + // 1. Decode a real OTLP protobuf export, exactly as the ingest handler does. + let encoded = sample_export_request(); + let mut points = decode_metrics_request(&encoded, PROJECT_ID, Some(42)) + .expect("decode_metrics_request must succeed on a valid export"); + assert_eq!(points.len(), 3, "gauge + counter + histogram = 3 points"); + + // Sanity-check the decode produced the right internal types BEFORE storing, + // so a failure localises to decode vs store. + let by_name = |name: &str| { + points + .iter() + .find(|p| p.metric_name == name) + .unwrap_or_else(|| panic!("decoded point {name} missing")) + }; + assert_eq!(by_name("http.requests.total").is_monotonic, Some(true)); + assert_eq!( + by_name("http.server.duration").histogram_bucket_counts, + Some(vec![2, 3, 4, 1]) + ); + + // The sample protobuf carries fixed 2023 timestamps, but the metrics table has + // a 90-day TTL — stored rows would be immediately TTL-expired and excluded from + // every read. Retime each decoded point to "now" (distinct millisecond per + // point) while preserving all other decoded fields; the read-back assertions + // below check no timestamp, only the structured fidelity. + let retime_base = chrono::Utc::now(); + for (i, p) in points.iter_mut().enumerate() { + p.timestamp = retime_base + chrono::Duration::milliseconds(i as i64); + } + + // 2. Store the decoded points natively into ClickHouse. + let stored = storage + .store_metrics(points) + .await + .expect("store_metrics must succeed"); + assert_eq!(stored, 3, "all three decoded points should be written"); + + // 3a. Public API: list_metric_names returns all three, sorted. + let names = storage + .list_metric_names(PROJECT_ID) + .await + .expect("list_metric_names must succeed"); + assert_eq!( + names, + vec![ + "http.requests.total".to_string(), + "http.server.active_requests".to_string(), + "http.server.duration".to_string(), + ], + "all stored metric names should round-trip via the public API" + ); + + // 3b. Public API: query_metrics on the gauge returns the stored scalar. + let gauge_buckets = storage + .query_metrics(MetricQuery { + project_id: PROJECT_ID, + metric_name: Some("http.server.active_requests".into()), + bucket_interval: Some("1 hour".into()), + aggregation: MetricAggregation::Max, + ..Default::default() + }) + .await + .expect("query_metrics must succeed"); + assert_eq!(gauge_buckets.len(), 1, "one gauge point -> one bucket"); + assert!( + (gauge_buckets[0].value - 7.0).abs() < f64::EPSILON, + "gauge value 7.0 must survive to query_metrics, got {}", + gauge_buckets[0].value + ); + + // 4. Raw column read-back: prove the STRUCTURED fidelity survived, not just + // the aggregated scalar. Read every stored column for this project and + // assert temporality / is_monotonic / histogram arrays / attributes. + // FINAL collapses any ReplacingMergeTree duplicates. + let rows: Vec = read_client + .query( + "SELECT metric_name, metric_type, temporality, is_monotonic, unit, value, \ + histogram_count, histogram_sum, histogram_bounds, histogram_bucket_counts, attributes \ + FROM metrics FINAL WHERE project_id = ? ORDER BY metric_name", + ) + .bind(PROJECT_ID) + .fetch_all::() + .await + .expect("raw read-back of stored metric rows must succeed"); + assert_eq!(rows.len(), 3, "three raw rows stored"); + + // rows are ordered by metric_name: + // [0] http.requests.total (counter) + // [1] http.server.active_requests (gauge) + // [2] http.server.duration (histogram) + let counter = &rows[0]; + assert_eq!(counter.metric_name, "http.requests.total"); + assert_eq!(counter.metric_type, "sum"); + assert_eq!( + counter.temporality, "cumulative", + "aggregation temporality must survive decode->store" + ); + assert_eq!( + counter.is_monotonic, + Some(1), + "is_monotonic counter flag must survive decode->store" + ); + assert_eq!(counter.unit, "1"); + assert_eq!(counter.value, Some(4242.0)); + + let gauge = &rows[1]; + assert_eq!(gauge.metric_name, "http.server.active_requests"); + assert_eq!(gauge.metric_type, "gauge"); + // Gauge carries no temporality -> sentinel; no is_monotonic. + assert_eq!(gauge.temporality, "unspecified"); + assert_eq!(gauge.is_monotonic, None); + assert_eq!(gauge.value, Some(7.0)); + assert_eq!( + gauge.attributes, + vec![("http.method".to_string(), "POST".to_string())], + "data-point labels must survive decode->store" + ); + + let hist = &rows[2]; + assert_eq!(hist.metric_name, "http.server.duration"); + assert_eq!(hist.metric_type, "histogram"); + assert_eq!(hist.temporality, "cumulative"); + assert_eq!(hist.histogram_count, Some(10)); + assert_eq!(hist.histogram_sum, Some(900.0)); + assert_eq!( + hist.histogram_bounds, + vec![10.0, 100.0, 250.0], + "explicit histogram bucket bounds must survive decode->store" + ); + assert_eq!( + hist.histogram_bucket_counts, + vec![2, 3, 4, 1], + "histogram bucket counts must survive decode->store" + ); + assert_eq!( + hist.attributes, + vec![("http.method".to_string(), "GET".to_string())], + "histogram data-point labels must survive decode->store" + ); +} diff --git a/crates/temps-otel/tests/timescaledb_storage_test.rs b/crates/temps-otel/tests/timescaledb_storage_test.rs index 10c890902..e571c65e6 100644 --- a/crates/temps-otel/tests/timescaledb_storage_test.rs +++ b/crates/temps-otel/tests/timescaledb_storage_test.rs @@ -73,23 +73,18 @@ fn sample_span( /// Build a sample MetricPoint. fn sample_metric(project_id: i32, name: &str, value: f64) -> MetricPoint { - MetricPoint { + let mut p = MetricPoint::skeleton( project_id, - deployment_id: None, - resource: test_resource(), - metric_name: name.into(), - metric_type: MetricType::Gauge, - unit: "ms".into(), - timestamp: Utc::now(), - value: Some(value), - histogram_count: None, - histogram_sum: None, - histogram_min: None, - histogram_max: None, - histogram_bounds: None, - histogram_bucket_counts: None, - attributes: BTreeMap::new(), - } + None, + test_resource(), + name.into(), + MetricType::Gauge, + "ms".into(), + Utc::now(), + BTreeMap::new(), + ); + p.value = Some(value); + p } /// Build a sample LogRecord. diff --git a/web/src/components/dashboard/Sidebar.tsx b/web/src/components/dashboard/Sidebar.tsx index e2556d678..449bc468f 100644 --- a/web/src/components/dashboard/Sidebar.tsx +++ b/web/src/components/dashboard/Sidebar.tsx @@ -43,6 +43,7 @@ import { Key, KeyRound, Layers, + LineChart, LogOut, Mail, Monitor, @@ -825,6 +826,7 @@ const projectBaseNav: ProjectNavItem[] = [ { title: 'All events', url: 'observe', icon: Eye }, { title: 'Uptime', url: 'monitors', icon: Activity }, { title: 'Metrics', url: 'monitoring', icon: Gauge }, + { title: 'OTel Metrics', url: 'metrics', icon: LineChart }, { title: 'Traces', url: 'traces', icon: Network }, { title: 'AI Traces', url: 'ai-gateway?tab=activity', icon: Bot }, { title: 'Request Logs', url: 'request-logs', icon: Rss }, diff --git a/web/src/components/observe/types.ts b/web/src/components/observe/types.ts index 6fa68b86b..3fb52edf4 100644 --- a/web/src/components/observe/types.ts +++ b/web/src/components/observe/types.ts @@ -25,3 +25,34 @@ export const ALL_KINDS = [ ] as const satisfies ReadonlyArray< import('@/api/client').EventKind > + +/** + * Metric kind for the unified Observe stream — SCAFFOLDED, OFF BY DEFAULT. + * + * The dedicated OTel metrics explorer lives at `metrics/*` (see + * `pages/MetricsExplorer.tsx`). Surfacing individual metric datapoints inside + * the merged Observe timeline additionally requires the backend events + * endpoint to emit a `metric` variant: a new `EventKind::Metric` plus a + * `MetricRow` arm on `ObservabilityEvent` in the merged events handler. That + * is backend work outside Phases A–C, so the generated SDK's `EventKind` + * union (`'request' | 'span' | 'error' | 'revenue'`) does NOT yet include + * `'metric'`. + * + * Intent is recorded here so the wiring is a single, well-marked step once the + * SDK is regenerated: + * 1. Backend: add `EventKind::Metric` + `MetricRow` to the events response + * and the merged query. + * 2. cd web && bun run openapi-ts # regenerates `EventKind`/`ObservabilityEvent`. + * 3. Append `'metric'` to `ALL_KINDS` above (the `satisfies` constraint will + * then accept it), add a `KIND_META` entry + `consoleSummary` arm in + * `pages/Observe.tsx`, and — because metrics are high-volume — leave it + * OUT of `DEFAULT_KINDS` so it stays off by default like Traces. + * + * `METRIC_KIND` is the literal to append; kept as a const so the future change + * is grep-able and the default-off policy is documented next to it. + */ +export const METRIC_KIND = 'metric' as const + +/** Whether the metric kind is loaded on first Observe visit. Off — metric + * datapoints are high-cardinality/high-volume, same rationale as Traces. */ +export const METRIC_KIND_DEFAULT_ON = false diff --git a/web/src/pages/Metrics.tsx b/web/src/pages/Metrics.tsx new file mode 100644 index 000000000..02954f3f4 --- /dev/null +++ b/web/src/pages/Metrics.tsx @@ -0,0 +1,20 @@ +import { Routes, Route } from 'react-router-dom' +import { ProjectResponse } from '@/api/client' +import MetricsExplorer from './MetricsExplorer' + +interface MetricsProps { + project: ProjectResponse +} + +/** + * Metrics section router. Mirrors `Traces.tsx`: an index explorer and room to + * grow a `:metricName` detail drill-down later. Mounted at `metrics/*` under + * the project (see ProjectDetail routes). + */ +export default function Metrics({ project }: MetricsProps) { + return ( + + } /> + + ) +} diff --git a/web/src/pages/MetricsExplorer.tsx b/web/src/pages/MetricsExplorer.tsx new file mode 100644 index 000000000..b65eba943 --- /dev/null +++ b/web/src/pages/MetricsExplorer.tsx @@ -0,0 +1,662 @@ +import { EnvironmentResponse, ProjectResponse } from '@/api/client' +import { + getEnvironmentsOptions, + listMetricNamesOptions, + queryMetricsOptions, +} from '@/api/client/@tanstack/react-query.gen' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Skeleton } from '@/components/ui/skeleton' +import { EmptyState } from '@/components/ui/empty-state' +import { ThresholdLineChart } from '@/components/charts/threshold-line-chart' +import { usePageTitle } from '@/hooks/usePageTitle' +import { useQuery } from '@tanstack/react-query' +import { format } from 'date-fns' +import { + Gauge, + LineChart as LineChartIcon, + Plus, + RefreshCw, + Search, + Tag, + X, +} from 'lucide-react' +import { useMemo, useState } from 'react' +import { useSearchParams } from 'react-router-dom' + +interface MetricsExplorerProps { + project: ProjectResponse +} + +// ── Time ranges ──────────────────────────────────────────────────────────── + +type TimeRange = '1h' | '6h' | '24h' | '7d' | '30d' + +const TIME_RANGES: { value: TimeRange; label: string }[] = [ + { value: '1h', label: 'Last hour' }, + { value: '6h', label: 'Last 6 hours' }, + { value: '24h', label: 'Last 24 hours' }, + { value: '7d', label: 'Last 7 days' }, + { value: '30d', label: 'Last 30 days' }, +] + +/** + * Map a time range to a sensible default ClickHouse-side bucket interval. + * + * The backend (`query_metrics`) parses these through a strict allowlist + * (`translate_bucket_interval`), so only the canonical `"N unit"` strings are + * accepted — keep this map in lockstep with that allowlist. + */ +const RANGE_BUCKET: Record = { + '1h': '1 minute', + '6h': '5 minutes', + '24h': '15 minutes', + '7d': '1 hour', + '30d': '6 hours', +} + +function timeRangeToFrom(range: TimeRange): Date { + const now = Date.now() + const map: Record = { + '1h': 60 * 60 * 1000, + '6h': 6 * 60 * 60 * 1000, + '24h': 24 * 60 * 60 * 1000, + '7d': 7 * 24 * 60 * 60 * 1000, + '30d': 30 * 24 * 60 * 60 * 1000, + } + return new Date(now - map[range]) +} + +// ── Aggregation selector ───────────────────────────────────────────────────── +// +// These mirror the FROZEN store-neutral contract from Phase C +// (`MetricAggregation` in temps-otel/src/types.rs). The backend accepts the +// string form via the `aggregation` query param. NOTE: the `aggregation` +// param is NOT yet present in the generated `QueryMetricsData` type — it only +// lands after the SDK is regenerated against the Phase C backend (see the +// REGEN TODO near the query below). Until then the selector is presentational +// and the chart falls back to the average series the current SDK returns. + +type AggKind = + | 'avg' + | 'sum' + | 'min' + | 'max' + | 'count' + | 'rate' + | 'p50' + | 'p90' + | 'p95' + | 'p99' + +const AGGREGATIONS: { value: AggKind; label: string }[] = [ + { value: 'avg', label: 'Average' }, + { value: 'sum', label: 'Sum' }, + { value: 'min', label: 'Min' }, + { value: 'max', label: 'Max' }, + { value: 'count', label: 'Count' }, + { value: 'rate', label: 'Rate / sec' }, + { value: 'p50', label: 'p50' }, + { value: 'p90', label: 'p90' }, + { value: 'p95', label: 'p95' }, + { value: 'p99', label: 'p99' }, +] + +// ── Label filters ──────────────────────────────────────────────────────────── + +interface LabelFilter { + key: string + value: string +} + +/** Serialize label filters to a compact, regen-ready URL string: `k=v,k2=v2`. */ +function serializeLabelFilters(filters: LabelFilter[]): string { + return filters + .filter((f) => f.key.trim().length > 0) + .map((f) => `${f.key.trim()}=${f.value.trim()}`) + .join(',') +} + +function parseLabelFilters(raw: string | null): LabelFilter[] { + if (!raw) return [] + return raw + .split(',') + .map((pair) => pair.trim()) + .filter(Boolean) + .map((pair) => { + const idx = pair.indexOf('=') + if (idx === -1) return { key: pair, value: '' } + return { key: pair.slice(0, idx), value: pair.slice(idx + 1) } + }) +} + +// ── Page ───────────────────────────────────────────────────────────────────── + +/** + * OTel metrics explorer — mirrors the TracesList pattern (metric-name + * selector + filter builder + time range) but renders a time-bucketed line + * chart instead of a row list. + * + * Backend wiring: bound to the generated SDK (`listMetricNames`, + * `queryMetrics`). The richer Phase C query surface (per-series `group_by`, + * typed `aggregation`, `metric_type`, `label_filters`, and the richer + * `OtelMetricBucket` response with `value`/`quantiles`/`series_key`) is + * SCAFFOLDED here but commented as a REGEN TODO because the generated SDK in + * this worktree predates the Phase C handler changes. Project rule: always use + * the generated SDK — never hand-roll a fetch — so the new params stay + * commented until the SDK is regenerated against a running server: + * + * 1. Start the server (see start-temps skill). + * 2. Mint a local admin API key: bunx @temps-sdk/cli api-key create ... + * 3. cd web && bun run openapi-ts # regenerates src/api/client + * 4. Revoke the temporary key. + * + * After regen the renamed types (OtelMetricsResponse / OtelMetricBucket / + * OtelMetricNamesResponse) and the new query params become available and the + * TODO blocks below can be un-commented. + */ +export default function MetricsExplorer({ project }: MetricsExplorerProps) { + usePageTitle(`Metrics · ${project.name}`) + const [searchParams, setSearchParams] = useSearchParams() + + // ── URL-backed filter state ── + const metricName = searchParams.get('metric') ?? '' + const serviceName = searchParams.get('service') ?? '' + const timeRange = ((): TimeRange => { + const r = searchParams.get('range') as TimeRange | null + return r && TIME_RANGES.some((t) => t.value === r) ? r : '24h' + })() + const environmentId = searchParams.get('environment_id') + ? Number(searchParams.get('environment_id')) + : null + const aggregation = ((): AggKind => { + const a = searchParams.get('agg') as AggKind | null + return a && AGGREGATIONS.some((x) => x.value === a) ? a : 'avg' + })() + const labelFilters = useMemo( + () => parseLabelFilters(searchParams.get('labels')), + [searchParams], + ) + + const [nameSearch, setNameSearch] = useState('') + + const patchParams = (mutate: (p: URLSearchParams) => void) => { + const params = new URLSearchParams(searchParams) + mutate(params) + setSearchParams(params, { replace: true }) + } + + const setMetricName = (v: string) => + patchParams((p) => (v ? p.set('metric', v) : p.delete('metric'))) + const setServiceName = (v: string) => + patchParams((p) => (v ? p.set('service', v) : p.delete('service'))) + const setTimeRange = (v: TimeRange) => + patchParams((p) => (v === '24h' ? p.delete('range') : p.set('range', v))) + const setEnvironmentId = (v: number | null) => + patchParams((p) => + v == null ? p.delete('environment_id') : p.set('environment_id', String(v)), + ) + const setAggregation = (v: AggKind) => + patchParams((p) => (v === 'avg' ? p.delete('agg') : p.set('agg', v))) + const setLabelFilters = (next: LabelFilter[]) => + patchParams((p) => { + const s = serializeLabelFilters(next) + s ? p.set('labels', s) : p.delete('labels') + }) + + // ── Environments (for the env selector, mirrors TracesList) ── + const { data: environments } = useQuery({ + ...getEnvironmentsOptions({ path: { project_id: project.id } }), + enabled: !!project.id, + }) + + // ── Metric-name catalogue ── + const namesQuery = useQuery({ + ...listMetricNamesOptions({ path: { project_id: project.id } }), + enabled: !!project.id, + }) + + const allNames = namesQuery.data?.names ?? [] + const filteredNames = useMemo(() => { + const q = nameSearch.trim().toLowerCase() + if (!q) return allNames + return allNames.filter((n) => n.toLowerCase().includes(q)) + }, [allNames, nameSearch]) + + // ── Time-bucketed series ── + const fromDate = useMemo(() => timeRangeToFrom(timeRange), [timeRange]) + const selectedEnv = environments?.find((e) => e.id === environmentId) ?? null + + const metricsQuery = useQuery({ + ...queryMetricsOptions({ + query: { + project_id: project.id, + metric_name: metricName || undefined, + service_name: serviceName || undefined, + environment: selectedEnv?.name || undefined, + start_time: fromDate.toISOString(), + end_time: new Date().toISOString(), + bucket_interval: RANGE_BUCKET[timeRange], + limit: 1000, + // ── REGEN TODO (Phase C query surface) ── + // The following params exist on the backend handler + // (`MetricQueryParams` in query_handler.rs) but are NOT yet on the + // generated `QueryMetricsData` type in this worktree. Un-comment + // after `bun run openapi-ts` regenerates the SDK against a running + // Phase C server: + // + // metric_type: undefined, // 'gauge' | 'sum' | 'histogram' | ... + // aggregation, // AggKind string ('avg' | 'p95' | 'rate' | ...) + // label_filters: serializeLabelFilters(labelFilters) || undefined, + // group_by: undefined, // comma-separated label keys → per-series buckets + // + // and the response narrows from `MetricsResponse`/`MetricBucket` to + // the richer `OtelMetricsResponse`/`OtelMetricBucket` + // (value / quantiles / histogram_summary / series_key). + }, + }), + // Only fetch once a concrete metric is chosen — querying every metric at + // once would be expensive and meaningless to chart. + enabled: !!project.id && metricName.length > 0, + }) + + const buckets = metricsQuery.data?.data ?? [] + + // Map the generated `MetricBucket` rows into the recharts point shape. + // + // REGEN NOTE: with the current (pre-Phase-C) SDK the only series available is + // `avg_value`. After regen, `OtelMetricBucket.value` carries the requested + // aggregation (avg/sum/p95/rate/…) and `series_key` enables multi-series + // rendering. Until then we always plot the average and the aggregation + // selector is advisory. + const chartData = useMemo( + () => + buckets.map((b) => ({ + bucket: b.bucket, + // `value` is the regen-ready aggregation field; the current SDK only + // ships avg_value, so fall back to it. `(b as { value?: number })` + // keeps TS happy before regen widens the type. + value: (b as { value?: number }).value ?? b.avg_value, + avg_value: b.avg_value, + min_value: b.min_value, + max_value: b.max_value, + count: b.count, + })), + [buckets], + ) + + const aggLabel = + AGGREGATIONS.find((a) => a.value === aggregation)?.label ?? 'Average' + + return ( +
+ {/* Header */} +
+
+
+ +

Metrics

+
+

+ Explore OpenTelemetry metrics for {project.name}. +

+
+ +
+ + {/* Filter bar */} +
+
+ {/* Metric-name selector with inline search */} +
+ + setNameSearch(e.target.value)} + onKeyDown={(e) => e.stopPropagation()} + placeholder="Filter metrics…" + className="h-8 pl-7 font-mono text-xs" + /> +
+
+ {namesQuery.isPending ? ( +
+ +
+ ) : filteredNames.length === 0 ? ( +
+ {allNames.length === 0 + ? 'No metrics ingested yet.' + : 'No metrics match your search.'} +
+ ) : ( + filteredNames.map((n) => ( + + {n} + + )) + )} + + +
+ + {/* Aggregation */} +
+ + +
+ + {/* Environment */} +
+ + +
+ + {/* Time range */} +
+ + +
+
+ + {/* Service-name filter + label-filter builder */} +
+
+ + setServiceName(e.target.value)} + placeholder="Filter by service name…" + className="pl-8 font-mono text-xs" + /> +
+
+ + {/* Label filters (regen-ready, sent once SDK exposes label_filters) */} + + + + {/* Chart / states */} +
+ 0} + isError={metricsQuery.isError} + errorMessage={ + metricsQuery.error instanceof Error + ? metricsQuery.error.message + : 'Failed to load metric series.' + } + chartData={chartData} + /> +
+ + ) +} + +// ── Label filter builder ───────────────────────────────────────────────────── + +function LabelFilterBuilder({ + value, + onChange, +}: { + value: LabelFilter[] + onChange: (next: LabelFilter[]) => void +}) { + const add = () => onChange([...value, { key: '', value: '' }]) + const update = (i: number, patch: Partial) => + onChange(value.map((f, idx) => (idx === i ? { ...f, ...patch } : f))) + const remove = (i: number) => onChange(value.filter((_, idx) => idx !== i)) + + return ( +
+
+
+ + Label filters +
+ +
+ {value.length === 0 ? ( +

+ No label filters. Add key/value pairs to narrow the series by + attribute (e.g. http.method = GET). +

+ ) : ( +
+ {value.map((f, i) => ( +
+ update(i, { key: e.target.value })} + placeholder="label.key" + className="h-8 font-mono text-xs sm:w-[200px]" + /> + = + update(i, { value: e.target.value })} + placeholder="value" + className="h-8 flex-1 font-mono text-xs" + /> + +
+ ))} +

+ Label filters are applied server-side once the SDK is regenerated + against the Phase C backend (the label_filters{' '} + query param). They are URL-persisted now so the view stays shareable. +

+
+ )} +
+ ) +} + +// ── Chart + states ─────────────────────────────────────────────────────────── + +interface ChartPoint { + bucket: string + value: number + avg_value: number + min_value: number + max_value: number + count: number +} + +function MetricChart({ + metricName, + aggLabel, + isPending, + isError, + errorMessage, + chartData, +}: { + metricName: string + aggLabel: string + isPending: boolean + isError: boolean + errorMessage: string + chartData: ChartPoint[] +}) { + if (!metricName) { + return ( + + ) + } + + if (isPending) { + return + } + + if (isError) { + return ( +
+

+ Failed to load metric series +

+

{errorMessage}

+
+ ) + } + + if (chartData.length === 0) { + return ( + + ) + } + + // Format bucket timestamps for the X axis. Buckets arrive as ISO strings + // from the backend; render a compact HH:mm (or date for wide ranges). + const xData = chartData.map((p) => ({ + ...p, + label: formatBucketLabel(p.bucket), + })) + + return ( +
+
+ + {metricName} + + {aggLabel} +
+ formatMetricValue(v)} + yTickFormatter={(v) => formatMetricValue(v)} + /> +
+ ) +} + +function formatBucketLabel(bucket: string): string { + const d = new Date(bucket) + if (Number.isNaN(d.getTime())) return bucket + return format(d, 'MMM d HH:mm') +} + +function formatMetricValue(v: number): string { + if (!Number.isFinite(v)) return '—' + const abs = Math.abs(v) + if (abs >= 1_000_000) return `${(v / 1_000_000).toFixed(2)}M` + if (abs >= 1_000) return `${(v / 1_000).toFixed(2)}k` + if (abs >= 1) return v.toFixed(2) + return v.toPrecision(3) +} diff --git a/web/src/pages/ProjectDetail.tsx b/web/src/pages/ProjectDetail.tsx index 9cbcb8530..2c0a284af 100644 --- a/web/src/pages/ProjectDetail.tsx +++ b/web/src/pages/ProjectDetail.tsx @@ -43,6 +43,7 @@ import Observe from './Observe' import RequestLogs from './RequestLogs' import ProjectAiCrawlers from './ProjectAiCrawlers' import Traces from './Traces' +import Metrics from './Metrics' import { ProjectAgentActivity } from './AiGateway' import { AutofixerPage } from '@/components/autofixer/AutofixerPage' import { AutofixRedirect } from '@/components/autofixer/AutofixRedirect' @@ -417,6 +418,10 @@ export function ProjectDetail() { path="traces/*" element={} /> + } + /> } From e70e80dfdd17ac2d12c2b1a451820258ff76f130 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 04:03:21 +0200 Subject: [PATCH 02/56] feat(otel): temporality-aware rate() and histogram-summary aggregation - rate() now honours aggregation temporality: DELTA series sum their per-interval increments while CUMULATIVE counters use the within-bucket (max - min). Previously every series used max-min, which undercounts the rate of delta-temporality counters. - query_metrics now populates histogram_summary with the explicit bucket layout (bounds + element-wise-summed bucket_counts) alongside count/sum/min/max, so histogram metrics return their real distribution instead of only a misleading synthetic mean. This enables correct quantile reconstruction from the buckets. Both behaviours are covered by new live-ClickHouse integration tests (query_metrics_rate_respects_temporality, histogram_summary_aggregates_buckets). --- .../temps-otel/src/storage/clickhouse/mod.rs | 50 ++++- crates/temps-otel/src/types.rs | 15 +- .../tests/clickhouse_storage_test.rs | 177 +++++++++++++++++- 3 files changed, 230 insertions(+), 12 deletions(-) diff --git a/crates/temps-otel/src/storage/clickhouse/mod.rs b/crates/temps-otel/src/storage/clickhouse/mod.rs index 882906163..4eb5f5b32 100644 --- a/crates/temps-otel/src/storage/clickhouse/mod.rs +++ b/crates/temps-otel/src/storage/clickhouse/mod.rs @@ -52,9 +52,9 @@ use crate::error::OtelError; use crate::storage::timescaledb::TimescaleDbStorage; use crate::storage::{BaselinePoint, DeployEvent, MinuteAggregate, OtelStorage, StorageResult}; use crate::types::{ - GenAiEvent, GenAiSpanDetail, GenAiTraceSummary, HealthSummary, Insight, InsightStatus, - LogQuery, LogRecord, MetricAggregation, MetricBucket, MetricPoint, MetricQuery, SpanEvent, - SpanKind, SpanRecord, SpanStatusCode, StorageQuota, TraceQuery, TraceSummary, + GenAiEvent, GenAiSpanDetail, GenAiTraceSummary, HealthSummary, HistogramSummary, Insight, + InsightStatus, LogQuery, LogRecord, MetricAggregation, MetricBucket, MetricPoint, MetricQuery, + SpanEvent, SpanKind, SpanRecord, SpanStatusCode, StorageQuota, TraceQuery, TraceSummary, }; // ── Client configuration ──────────────────────────────────────────────────── @@ -587,6 +587,14 @@ struct ChMetricBucketRow { count: u64, /// The value of the requested aggregation for this bucket. agg_value: f64, + /// Aggregated explicit-histogram fields (zero/empty for non-histogram + /// metrics). `hist_bucket_counts` is summed element-wise across the window. + hist_count: u64, + hist_sum: f64, + hist_min: Option, + hist_max: Option, + hist_bounds: Vec, + hist_bucket_counts: Vec, /// Grouped label values, in `group_by` order (empty when ungrouped). series_values: Vec, } @@ -2035,9 +2043,17 @@ impl OtelStorage for ClickHouseOtelStorage { MetricAggregation::Count => "toFloat64(count())".to_string(), MetricAggregation::RatePerSec => { let secs = interval_seconds(&interval_sql).max(1); - // Cumulative counters are stored raw (not re-deltaed); within a - // bucket the increase is (max - min). Guard against zero width. - format!("(max(assumeNotNull(value)) - min(assumeNotNull(value))) / {secs}.0") + // Temporality-aware per-second rate. DELTA series already carry + // per-interval increments, so the bucket rate is sum/window. + // CUMULATIVE counters are stored raw (monotonic running total), + // so the within-bucket increase is (max - min). `any(temporality)` + // reads the metric's (uniform) temporality; the window divisor is + // guarded against zero by secs.max(1) above. + format!( + "if(any(temporality) = 'delta', \ + sum(assumeNotNull(value)), \ + max(assumeNotNull(value)) - min(assumeNotNull(value))) / {secs}.0" + ) } MetricAggregation::Quantile(q) => { let qc = q.clamp(0.0, 1.0); @@ -2100,6 +2116,12 @@ impl OtelStorage for ClickHouseOtelStorage { max(assumeNotNull(value)) AS max_value, \ count() AS count, \ {agg_expr} AS agg_value, \ + sum(ifNull(histogram_count, 0)) AS hist_count, \ + sum(ifNull(histogram_sum, 0)) AS hist_sum, \ + min(histogram_min) AS hist_min, \ + max(histogram_max) AS hist_max, \ + anyIf(histogram_bounds, notEmpty(histogram_bounds)) AS hist_bounds, \ + sumForEachIf(histogram_bucket_counts, notEmpty(histogram_bucket_counts)) AS hist_bucket_counts, \ {group_select} \ FROM metrics \ WHERE {where_sql} \ @@ -2173,7 +2195,21 @@ impl OtelStorage for ClickHouseOtelStorage { Some(qq) => vec![(qq, r.agg_value)], None => Vec::new(), }, - histogram_summary: None, + // Populated only when this bucket actually contains explicit + // histogram data (bounds present); gauge/sum metrics leave the + // histogram columns empty and so report None. + histogram_summary: if r.hist_bounds.is_empty() { + None + } else { + Some(HistogramSummary { + count: r.hist_count, + sum: r.hist_sum, + min: r.hist_min, + max: r.hist_max, + bounds: r.hist_bounds, + bucket_counts: r.hist_bucket_counts, + }) + }, series_key, } }) diff --git a/crates/temps-otel/src/types.rs b/crates/temps-otel/src/types.rs index dc75a794e..01ae0b816 100644 --- a/crates/temps-otel/src/types.rs +++ b/crates/temps-otel/src/types.rs @@ -1082,11 +1082,13 @@ pub struct LogQuery { pub offset: Option, } -/// A compact summary of an explicit-bucket histogram aggregated over a bucket. +/// An explicit-bucket histogram aggregated over a time bucket. /// -/// Carries only the cheap reduced fields (count/sum/min/max). Full quantile -/// reconstruction from the bucket arrays is deferred; the raw bucket arrays -/// remain available in the store for a later read-side reconstruction. +/// Carries the reduced scalars (count/sum/min/max) plus the explicit bucket +/// layout — `bounds` (the upper bounds) and `bucket_counts` (observation counts, +/// summed element-wise across the window; length is `bounds.len() + 1`, the last +/// entry being the +Inf overflow bucket). With these, a caller can reconstruct +/// any quantile (e.g. p95) via cumulative-count interpolation. #[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)] pub struct HistogramSummary { /// Total observation count summed across the bucket window. @@ -1097,6 +1099,11 @@ pub struct HistogramSummary { pub min: Option, /// Maximum observed value, when reported by the producer. pub max: Option, + /// Explicit bucket upper bounds (OTLP `explicit_bounds`), ascending. + pub bounds: Vec, + /// Per-bucket observation counts summed element-wise across the window. + /// Length is `bounds.len() + 1` (the trailing element is the +Inf bucket). + pub bucket_counts: Vec, } /// A time-bucketed metric aggregate for chart display. diff --git a/crates/temps-otel/tests/clickhouse_storage_test.rs b/crates/temps-otel/tests/clickhouse_storage_test.rs index dca46fcfd..8563c5736 100644 --- a/crates/temps-otel/tests/clickhouse_storage_test.rs +++ b/crates/temps-otel/tests/clickhouse_storage_test.rs @@ -25,7 +25,9 @@ use sea_orm::{DatabaseBackend, MockDatabase}; use temps_otel::storage::clickhouse::{ClickHouseOtelConfig, ClickHouseOtelStorage}; use temps_otel::storage::timescaledb::TimescaleDbStorage; use temps_otel::storage::OtelStorage; -use temps_otel::types::{MetricAggregation, MetricPoint, MetricQuery, MetricType, ResourceInfo}; +use temps_otel::types::{ + AggregationTemporality, MetricAggregation, MetricPoint, MetricQuery, MetricType, ResourceInfo, +}; /// Container handle + a connected `ClickHouseOtelStorage`. Returns `None` when /// Docker is unavailable so the test can skip without failing CI. @@ -387,3 +389,176 @@ async fn store_metrics_drops_disallowed_name() { .expect("list_metric_names should succeed"); assert!(names.is_empty(), "nothing should have been written"); } + +/// A Sum (counter) point with an explicit temporality + scalar value. +fn counter_point( + project_id: i32, + metric_name: &str, + temporality: AggregationTemporality, + value: f64, + ts: chrono::DateTime, +) -> MetricPoint { + let mut p = MetricPoint::skeleton( + project_id, + Some(9), + ResourceInfo { + service_name: "api".into(), + service_version: Some("1.0.0".into()), + deployment_environment: Some("production".into()), + attributes: BTreeMap::new(), + }, + metric_name.into(), + MetricType::Sum, + "1".into(), + ts, + BTreeMap::new(), + ); + p.value = Some(value); + p.temporality = Some(temporality); + p.is_monotonic = Some(true); + p +} + +#[tokio::test] +async fn query_metrics_rate_respects_temporality() { + let Some((storage, _container)) = setup().await else { + return; // Docker unavailable — skip gracefully. + }; + + let base = Utc::now(); + // Cumulative counter: raw running total 100 -> 130 within one hour; the + // per-second rate is the within-bucket increase (130 - 100) / 3600. + // Delta counter: per-interval increments 10 and 20; the rate is the SUM + // (10 + 20) / 3600 — NOT max-min (which would be 10/3600). This is the + // discriminating case that proves temporality is honoured. + let points = vec![ + counter_point( + 3003, + "cumulative.req", + AggregationTemporality::Cumulative, + 100.0, + base, + ), + counter_point( + 3003, + "cumulative.req", + AggregationTemporality::Cumulative, + 130.0, + base + chrono::Duration::milliseconds(1), + ), + counter_point(3003, "delta.req", AggregationTemporality::Delta, 10.0, base), + counter_point( + 3003, + "delta.req", + AggregationTemporality::Delta, + 20.0, + base + chrono::Duration::milliseconds(1), + ), + ]; + assert_eq!(storage.store_metrics(points).await.expect("store"), 4); + + let secs = 3600.0; + let rate_query = |name: &str| MetricQuery { + project_id: 3003, + metric_name: Some(name.into()), + bucket_interval: Some("1 hour".into()), + aggregation: MetricAggregation::RatePerSec, + ..Default::default() + }; + + let cumulative = storage + .query_metrics(rate_query("cumulative.req")) + .await + .expect("cumulative rate query"); + assert_eq!(cumulative.len(), 1); + assert!( + (cumulative[0].value - 30.0 / secs).abs() < 1e-9, + "cumulative rate should be (130-100)/3600, got {}", + cumulative[0].value + ); + + let delta = storage + .query_metrics(rate_query("delta.req")) + .await + .expect("delta rate query"); + assert_eq!(delta.len(), 1); + assert!( + (delta[0].value - 30.0 / secs).abs() < 1e-9, + "delta rate should be (10+20)/3600 (sum, not max-min), got {}", + delta[0].value + ); +} + +#[tokio::test] +async fn query_metrics_histogram_summary_aggregates_buckets() { + let Some((storage, _container)) = setup().await else { + return; // Docker unavailable — skip gracefully. + }; + + let base = Utc::now(); + let mk = |count: u64, sum: f64, buckets: Vec, ts: chrono::DateTime| { + let mut p = MetricPoint::skeleton( + 4004, + Some(9), + ResourceInfo { + service_name: "api".into(), + service_version: Some("1.0.0".into()), + deployment_environment: Some("production".into()), + attributes: BTreeMap::new(), + }, + "http.server.duration".into(), + MetricType::Histogram, + "ms".into(), + ts, + BTreeMap::new(), + ); + p.histogram_count = Some(count); + p.histogram_sum = Some(sum); + p.histogram_min = Some(1.0); + p.histogram_max = Some(240.0); + p.histogram_bounds = Some(vec![10.0, 100.0, 250.0]); + p.histogram_bucket_counts = Some(buckets); + p.value = Some(sum / count as f64); // synthetic mean + p + }; + // Two histogram points for the same series within one hour, same bounds. + let stored = storage + .store_metrics(vec![ + mk(4, 100.0, vec![1, 1, 1, 1], base), + mk( + 6, + 200.0, + vec![1, 2, 3, 0], + base + chrono::Duration::milliseconds(1), + ), + ]) + .await + .expect("store"); + assert_eq!(stored, 2); + + let buckets = storage + .query_metrics(MetricQuery { + project_id: 4004, + metric_name: Some("http.server.duration".into()), + bucket_interval: Some("1 hour".into()), + aggregation: MetricAggregation::Avg, + ..Default::default() + }) + .await + .expect("histogram query"); + assert_eq!(buckets.len(), 1); + let hs = buckets[0] + .histogram_summary + .as_ref() + .expect("histogram_summary must be populated for a histogram metric"); + assert_eq!(hs.count, 10, "observation counts summed across the window"); + assert!((hs.sum - 300.0).abs() < f64::EPSILON); + assert_eq!(hs.bounds, vec![10.0, 100.0, 250.0]); + assert_eq!( + hs.bucket_counts, + vec![2, 3, 4, 1], + "bucket counts summed element-wise" + ); + assert_eq!(hs.min, Some(1.0)); + assert_eq!(hs.max, Some(240.0)); +} From 04fd8666e1ca0d1551ac42e6f431063e13582820 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 04:06:43 +0200 Subject: [PATCH 03/56] test(otel): live-ClickHouse round-trip for exp-histogram/summary/exemplar columns Adds a decode->store->raw-read test that inserts a metric carrying the nested Array(Tuple(...)) columns (exponential-histogram bucket counts, summary quantiles, and exemplars with trace/span ids) into a live ClickHouse container and asserts they survive. These RowBinary nested-tuple codepaths were previously only unit-tested at the row-mapping tier, never against a real ClickHouse server. --- .../temps-otel/tests/decode_to_store_test.rs | 103 +++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/crates/temps-otel/tests/decode_to_store_test.rs b/crates/temps-otel/tests/decode_to_store_test.rs index cc799023a..3562ab711 100644 --- a/crates/temps-otel/tests/decode_to_store_test.rs +++ b/crates/temps-otel/tests/decode_to_store_test.rs @@ -17,8 +17,10 @@ //! The inner `TimescaleDbStorage` is a sea-orm `MockDatabase`: the metric //! methods under test read/write only ClickHouse and never touch Postgres. +use std::collections::BTreeMap; use std::sync::Arc; +use chrono::Utc; use prost::Message; use sea_orm::{DatabaseBackend, MockDatabase}; @@ -27,7 +29,9 @@ use temps_otel::proto; use temps_otel::storage::clickhouse::{ClickHouseOtelConfig, ClickHouseOtelStorage}; use temps_otel::storage::timescaledb::TimescaleDbStorage; use temps_otel::storage::OtelStorage; -use temps_otel::types::{MetricAggregation, MetricQuery}; +use temps_otel::types::{ + Exemplar, MetricAggregation, MetricPoint, MetricQuery, MetricType, ResourceInfo, +}; /// A raw row read straight back out of the `metrics` table, used to prove the /// structured columns (temporality / is_monotonic / histogram arrays / @@ -375,3 +379,100 @@ async fn otlp_decode_to_store_preserves_full_fidelity() { "histogram data-point labels must survive decode->store" ); } + +/// Raw read-back of the nested `Array(Tuple(...))` columns. These exercise the +/// trickiest RowBinary codepaths — a single field-order or type-width mismatch in +/// a nested tuple silently corrupts data — and were previously only unit-tested, +/// never inserted into a live ClickHouse. +#[derive(::clickhouse::Row, serde::Deserialize, Debug)] +struct RawStructuredRow { + metric_name: String, + exp_scale: Option, + exp_zero_count: Option, + exp_positive_offset: Option, + exp_positive_counts: Vec, + exp_negative_counts: Vec, + summary_quantiles: Vec<(f64, f64)>, + // Tuple(trace_id, span_id, value, DateTime64 -> raw i64 ms). + exemplars: Vec<(String, String, f64, i64)>, +} + +#[tokio::test] +async fn store_and_readback_exp_histogram_summary_exemplars() { + let Some((storage, read_client, _container)) = setup().await else { + return; // Docker unavailable — skip gracefully. Roundtrip NOT executed. + }; + + const PROJECT_ID: i32 = 888; + + // One point carrying every nested-structure column at once (storage does not + // enforce cross-field metric-type consistency; this maximises wire coverage). + let mut p = MetricPoint::skeleton( + PROJECT_ID, + Some(7), + ResourceInfo { + service_name: "svc".into(), + service_version: None, + deployment_environment: None, + attributes: BTreeMap::new(), + }, + "request.duration.exp".into(), + MetricType::ExponentialHistogram, + "ms".into(), + Utc::now(), + BTreeMap::new(), + ); + p.exp_scale = Some(3); + p.exp_zero_count = Some(5); + p.exp_zero_threshold = Some(0.001); + p.exp_positive_offset = Some(-2); + p.exp_positive_counts = Some(vec![1, 4, 9, 2]); + p.exp_negative_offset = Some(0); + p.exp_negative_counts = Some(vec![0, 1]); + p.summary_quantiles = Some(vec![(0.5, 12.0), (0.9, 48.0), (0.99, 99.0)]); + p.exemplars = vec![Exemplar { + timestamp: Utc::now(), + value: 42.0, + trace_id: Some("abc123".into()), + span_id: Some("def456".into()), + attributes: BTreeMap::new(), + }]; + p.value = Some(7.0); + + assert_eq!( + storage.store_metrics(vec![p]).await.expect("store_metrics"), + 1 + ); + + let rows: Vec = read_client + .query( + "SELECT metric_name, exp_scale, exp_zero_count, exp_positive_offset, \ + exp_positive_counts, exp_negative_counts, summary_quantiles, exemplars \ + FROM metrics FINAL WHERE project_id = ? ORDER BY metric_name", + ) + .bind(PROJECT_ID) + .fetch_all::() + .await + .expect("raw read-back of nested structured columns must succeed"); + + assert_eq!(rows.len(), 1); + let r = &rows[0]; + assert_eq!(r.metric_name, "request.duration.exp"); + assert_eq!(r.exp_scale, Some(3)); + assert_eq!(r.exp_zero_count, Some(5)); + assert_eq!(r.exp_positive_offset, Some(-2)); + assert_eq!(r.exp_positive_counts, vec![1, 4, 9, 2]); + assert_eq!(r.exp_negative_counts, vec![0, 1]); + assert_eq!( + r.summary_quantiles, + vec![(0.5, 12.0), (0.9, 48.0), (0.99, 99.0)], + "Array(Tuple(Float64,Float64)) summary quantiles must survive" + ); + assert_eq!(r.exemplars.len(), 1, "exemplar must survive to the store"); + assert_eq!(r.exemplars[0].0, "abc123", "exemplar trace_id"); + assert_eq!(r.exemplars[0].1, "def456", "exemplar span_id"); + assert!( + (r.exemplars[0].2 - 42.0).abs() < f64::EPSILON, + "exemplar value" + ); +} From 956996ca2085991c0804f976006385a6896d06d1 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 08:30:41 +0200 Subject: [PATCH 04/56] fix(otel): temporality-correct histogram_summary (no cumulative double-count) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit histogram_summary previously summed raw histogram_count / bucket_counts across all rows in a window. For CUMULATIVE histograms re-exported multiple times (the OTLP default), each export is a running total, so summing them multiplied the counts by the number of exports — a live demo showed count=300 for 50 observations exported 6 times. Compute histogram_summary from a per-series sub-aggregation: each series (attributes_hash) is collapsed first (CUMULATIVE -> latest snapshot via argMax/max; DELTA or unspecified -> sum across the window), then summed across series up to the requested grouping granularity, matched back to the scalar rows by (bucket_ms, series_values). Scalar/quantile aggregations are unchanged. Covered by a new live-ClickHouse test: cumulative re-exports collapse to per-series latest, then sum across series. --- .../temps-otel/src/storage/clickhouse/mod.rs | 144 ++++++++++++++---- .../tests/clickhouse_storage_test.rs | 102 +++++++++++++ 2 files changed, 217 insertions(+), 29 deletions(-) diff --git a/crates/temps-otel/src/storage/clickhouse/mod.rs b/crates/temps-otel/src/storage/clickhouse/mod.rs index 4eb5f5b32..4eefa3811 100644 --- a/crates/temps-otel/src/storage/clickhouse/mod.rs +++ b/crates/temps-otel/src/storage/clickhouse/mod.rs @@ -587,18 +587,25 @@ struct ChMetricBucketRow { count: u64, /// The value of the requested aggregation for this bucket. agg_value: f64, - /// Aggregated explicit-histogram fields (zero/empty for non-histogram - /// metrics). `hist_bucket_counts` is summed element-wise across the window. - hist_count: u64, - hist_sum: f64, - hist_min: Option, - hist_max: Option, - hist_bounds: Vec, - hist_bucket_counts: Vec, /// Grouped label values, in `group_by` order (empty when ungrouped). series_values: Vec, } +/// Row of the temporality-aware histogram sub-aggregation (one per +/// bucket × grouped-series). Matched back to [`ChMetricBucketRow`] by +/// `(bucket_ms, series_values)`. +#[derive(::clickhouse::Row, Deserialize, Debug)] +struct ChHistogramRow { + bucket_ms: i64, + series_values: Vec, + hcount: u64, + hsum: f64, + hmin: Option, + hmax: Option, + hbounds: Vec, + hbuckets: Vec, +} + /// Row returned by `list_metric_names` — a single distinct name. #[derive(::clickhouse::Row, Deserialize, Debug)] struct ChMetricNameRow { @@ -2080,6 +2087,18 @@ impl OtelStorage for ClickHouseOtelStorage { } else { ", series_values".to_string() }; + // The grouped-label array WITHOUT the `AS series_values` alias, for reuse + // inside the histogram sub-aggregation's projection. + let group_array = if query.group_by.is_empty() { + "[]".to_string() + } else { + let parts: Vec = query + .group_by + .iter() + .map(|_| "attributes[?]".to_string()) + .collect(); + format!("[{}]", parts.join(", ")) + }; // Build WHERE with `?` placeholders; bind in the same order below. let mut where_clauses = vec!["project_id = ?".to_string()]; @@ -2116,12 +2135,6 @@ impl OtelStorage for ClickHouseOtelStorage { max(assumeNotNull(value)) AS max_value, \ count() AS count, \ {agg_expr} AS agg_value, \ - sum(ifNull(histogram_count, 0)) AS hist_count, \ - sum(ifNull(histogram_sum, 0)) AS hist_sum, \ - min(histogram_min) AS hist_min, \ - max(histogram_max) AS hist_max, \ - anyIf(histogram_bounds, notEmpty(histogram_bounds)) AS hist_bounds, \ - sumForEachIf(histogram_bucket_counts, notEmpty(histogram_bucket_counts)) AS hist_bucket_counts, \ {group_select} \ FROM metrics \ WHERE {where_sql} \ @@ -2166,10 +2179,97 @@ impl OtelStorage for ClickHouseOtelStorage { .await .map_err(|e| ch_query_err("query_metrics", e))?; + // Histogram summary, computed separately so cumulative re-exports are NOT + // double-counted. The inner query collapses each series (cumulative -> + // latest snapshot via argMax/max; delta or unspecified -> sum across the + // window); the outer sums those per-series results up to the requested + // grouping granularity. Matched back to the scalar rows by + // (bucket_ms, series_values). + let hist_sql = format!( + "SELECT bucket_ms, series_values, \ + sum(s_count) AS hcount, sum(s_sum) AS hsum, \ + min(s_min) AS hmin, max(s_max) AS hmax, \ + anyIf(s_bounds, notEmpty(s_bounds)) AS hbounds, \ + sumForEach(s_buckets) AS hbuckets \ + FROM ( \ + SELECT \ + toInt64(toUnixTimestamp(toStartOfInterval(timestamp, {interval_sql}))) * 1000 AS bucket_ms, \ + any({group_array}) AS series_values, \ + attributes_hash AS ah, \ + if(any(temporality) = 'cumulative', max(ifNull(histogram_count, 0)), sum(ifNull(histogram_count, 0))) AS s_count, \ + if(any(temporality) = 'cumulative', max(ifNull(histogram_sum, 0)), sum(ifNull(histogram_sum, 0))) AS s_sum, \ + min(histogram_min) AS s_min, \ + max(histogram_max) AS s_max, \ + any(histogram_bounds) AS s_bounds, \ + if(any(temporality) = 'cumulative', argMax(histogram_bucket_counts, timestamp), sumForEach(histogram_bucket_counts)) AS s_buckets \ + FROM metrics \ + WHERE {where_sql} AND notEmpty(histogram_bucket_counts) \ + GROUP BY bucket_ms, ah \ + ) \ + GROUP BY bucket_ms, series_values \ + ORDER BY bucket_ms ASC \ + LIMIT ?" + ); + let mut hq = self.ch.query(&hist_sql); + for key in &query.group_by { + hq = hq.bind(key.clone()); + } + hq = hq.bind(query.project_id); + if let Some(ref name) = query.metric_name { + hq = hq.bind(name.clone()); + } + if let Some(mt) = query.metric_type { + hq = hq.bind(mt.to_string()); + } + if let Some(ref svc) = query.service_name { + hq = hq.bind(svc.clone()); + } + if let Some(ref env) = query.environment { + hq = hq.bind(env.clone()); + } + for (k, v) in &query.label_filters { + hq = hq.bind(k.clone()); + hq = hq.bind(v.clone()); + } + if let Some(start) = query.start_time { + hq = hq.bind(start.timestamp_millis()); + } + if let Some(end) = query.end_time { + hq = hq.bind(end.timestamp_millis()); + } + hq = hq.bind(limit); + let hist_rows = hq + .fetch_all::() + .await + .map_err(|e| ch_query_err("query_metrics histogram", e))?; + let mut hist_map: std::collections::HashMap<(i64, Vec), HistogramSummary> = + std::collections::HashMap::new(); + for h in hist_rows { + if h.hbounds.is_empty() { + continue; + } + hist_map.insert( + (h.bucket_ms, h.series_values.clone()), + HistogramSummary { + count: h.hcount, + sum: h.hsum, + min: h.hmin, + max: h.hmax, + bounds: h.hbounds, + bucket_counts: h.hbuckets, + }, + ); + } + let group_keys = query.group_by.clone(); Ok(rows .into_iter() .map(|r| { + // Look up the temporality-correct histogram summary (None for + // gauge/sum metrics, which produce no histogram rows). + let histogram_summary = hist_map + .get(&(r.bucket_ms, r.series_values.clone())) + .cloned(); let series_key = if group_keys.is_empty() { None } else { @@ -2195,21 +2295,7 @@ impl OtelStorage for ClickHouseOtelStorage { Some(qq) => vec![(qq, r.agg_value)], None => Vec::new(), }, - // Populated only when this bucket actually contains explicit - // histogram data (bounds present); gauge/sum metrics leave the - // histogram columns empty and so report None. - histogram_summary: if r.hist_bounds.is_empty() { - None - } else { - Some(HistogramSummary { - count: r.hist_count, - sum: r.hist_sum, - min: r.hist_min, - max: r.hist_max, - bounds: r.hist_bounds, - bucket_counts: r.hist_bucket_counts, - }) - }, + histogram_summary, series_key, } }) diff --git a/crates/temps-otel/tests/clickhouse_storage_test.rs b/crates/temps-otel/tests/clickhouse_storage_test.rs index 8563c5736..ad7c3cf67 100644 --- a/crates/temps-otel/tests/clickhouse_storage_test.rs +++ b/crates/temps-otel/tests/clickhouse_storage_test.rs @@ -562,3 +562,105 @@ async fn query_metrics_histogram_summary_aggregates_buckets() { assert_eq!(hs.min, Some(1.0)); assert_eq!(hs.max, Some(240.0)); } + +#[tokio::test] +async fn query_metrics_cumulative_histogram_uses_latest_not_sum() { + let Some((storage, _container)) = setup().await else { + return; // Docker unavailable — skip gracefully. + }; + + let base = Utc::now(); + let mk = |method: &str, count: u64, buckets: Vec, ts: chrono::DateTime| { + let mut p = MetricPoint::skeleton( + 5005, + Some(9), + ResourceInfo { + service_name: "api".into(), + service_version: Some("1.0.0".into()), + deployment_environment: Some("production".into()), + attributes: BTreeMap::new(), + }, + "http.server.duration".into(), + MetricType::Histogram, + "ms".into(), + ts, + { + let mut m = BTreeMap::new(); + m.insert("http.method".into(), method.to_string()); + m + }, + ); + p.temporality = Some(AggregationTemporality::Cumulative); + p.histogram_count = Some(count); + p.histogram_sum = Some(count as f64 * 10.0); + p.histogram_min = Some(1.0); + p.histogram_max = Some(99.0); + p.histogram_bounds = Some(vec![10.0, 100.0]); // 3 buckets incl +Inf + p.histogram_bucket_counts = Some(buckets); + p.value = Some(10.0); + p + }; + + // CUMULATIVE histograms re-exported within one window (counts are running + // totals). GET grows 10 -> 20 -> 30 across three exports; POST grows 25 -> 50 + // across two. A correct read must take each series' LATEST snapshot — never + // sum the re-exports — then sum across the two series. + let stored = storage + .store_metrics(vec![ + mk("GET", 10, vec![4, 4, 2], base), + mk( + "GET", + 20, + vec![8, 8, 4], + base + chrono::Duration::milliseconds(1), + ), + mk( + "GET", + 30, + vec![12, 12, 6], + base + chrono::Duration::milliseconds(2), + ), + mk( + "POST", + 25, + vec![10, 10, 5], + base + chrono::Duration::milliseconds(1), + ), + mk( + "POST", + 50, + vec![20, 20, 10], + base + chrono::Duration::milliseconds(3), + ), + ]) + .await + .expect("store"); + assert_eq!(stored, 5); + + let buckets = storage + .query_metrics(MetricQuery { + project_id: 5005, + metric_name: Some("http.server.duration".into()), + bucket_interval: Some("1 hour".into()), + aggregation: MetricAggregation::Avg, + ..Default::default() + }) + .await + .expect("histogram query"); + assert_eq!(buckets.len(), 1); + let hs = buckets[0] + .histogram_summary + .as_ref() + .expect("histogram_summary populated"); + // GET latest count 30 + POST latest count 50 = 80 (NOT 10+20+30+25+50=135). + assert_eq!( + hs.count, 80, + "cumulative re-exports must collapse to per-series latest, then sum series" + ); + // GET latest [12,12,6] + POST latest [20,20,10] = [32,32,16]. + assert_eq!( + hs.bucket_counts, + vec![32, 32, 16], + "per-series latest buckets summed across series" + ); +} From 18c1f12ed5123d431b0ea9cf40a7775600605daf Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 09:25:35 +0200 Subject: [PATCH 05/56] feat(web): wire metrics explorer to Phase C API + all-metrics overview Regenerate the SDK against the Phase C backend (typed aggregation / metric_type / label_filters params; histogram_summary, quantiles, series_key on MetricBucket). MetricsExplorer: - Default view now shows ALL metrics as an overview grid (one mini chart per metric with its latest value); click a card to drill into the detailed view. - Send the real aggregation + label_filters params. For histogram metrics, percentiles are computed client-side from the histogram_summary buckets (the backend scalar quantile runs over the synthetic mean). - Add a histogram Distribution panel (count/mean/p50-p99 + per-bucket bars). - Fix an infinite refetch loop: end_time used new Date() every render, changing the query key each render; the time bounds are now memoized. --- .../api/client/@tanstack/react-query.gen.ts | 29 +- web/src/api/client/sdk.gen.ts | 30 +- web/src/api/client/types.gen.ts | 324 ++++++------- web/src/pages/MetricsExplorer.tsx | 432 +++++++++++++++--- 4 files changed, 530 insertions(+), 285 deletions(-) diff --git a/web/src/api/client/@tanstack/react-query.gen.ts b/web/src/api/client/@tanstack/react-query.gen.ts index 744c69805..e634d8145 100644 --- a/web/src/api/client/@tanstack/react-query.gen.ts +++ b/web/src/api/client/@tanstack/react-query.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, getAdminGate, patchAdminGate, listOidcProviders, createOidcProvider, deleteOidcProvider, updateOidcProvider, listOidcRoleMappings, createOidcRoleMapping, testOidcProvider, listOidcProviderUsers, deleteOidcRoleMapping, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, oidcCallback, startOidcLoginBySlug, listPublicProviders, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listServiceSchedules, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, listScheduleServices, attachScheduleServices, detachScheduleService, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, deploymentMetricsGetRange, deploymentMetricsToggle, deploymentMetricsGetLatest, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, getOnDemandCertStatus, listOnDemandCerts, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, updateEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, externalServiceMetricsGetRange, externalServiceMetricsGetAlertRules, externalServiceMetricsCreateAlertRule, externalServiceMetricsDeleteAlertRule, externalServiceMetricsUpdateAlertRule, externalServiceMetricsByDatabase, externalServiceMetricsToggle, externalServiceMetricsGetLatest, externalServiceMetricsStatus, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, nodeMetricsGetRange, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider2, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, listDeploymentContainerLogs, getDeploymentContainerLogContent, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, listKnownAiAgents, getProxyLogByRequestId, getAiAgentPages, getAiAgentBreakdown, getAiAgentTimeline, getAiPageBreakdown, getAiStatusBreakdown, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, getDiskStatus, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, updateOtelIngest, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; +import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, getAdminGate, patchAdminGate, listOidcProviders, createOidcProvider, deleteOidcProvider, updateOidcProvider, listOidcRoleMappings, createOidcRoleMapping, testOidcProvider, listOidcProviderUsers, deleteOidcRoleMapping, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, oidcCallback, startOidcLoginBySlug, listPublicProviders, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listServiceSchedules, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, listScheduleServices, attachScheduleServices, detachScheduleService, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, deploymentMetricsGetRange, deploymentMetricsToggle, deploymentMetricsGetLatest, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, getOnDemandCertStatus, listOnDemandCerts, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, updateEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, externalServiceMetricsGetRange, externalServiceMetricsGetAlertRules, externalServiceMetricsCreateAlertRule, externalServiceMetricsDeleteAlertRule, externalServiceMetricsUpdateAlertRule, externalServiceMetricsByDatabase, externalServiceMetricsToggle, externalServiceMetricsGetLatest, externalServiceMetricsStatus, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, nodeMetricsGetRange, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider2, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, listDeploymentContainerLogs, getDeploymentContainerLogContent, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, listKnownAiAgents, getProxyLogByRequestId, getAiAgentPages, getAiAgentBreakdown, getAiAgentTimeline, getAiPageBreakdown, getAiStatusBreakdown, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, getDiskStatus, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; import { queryOptions, type UseMutationOptions, type DefaultError, infiniteQueryOptions, type InfiniteData } from '@tanstack/react-query'; -import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, GetAdminGateData, PatchAdminGateData, PatchAdminGateResponse, ListOidcProvidersData, CreateOidcProviderData, CreateOidcProviderResponse, DeleteOidcProviderData, DeleteOidcProviderResponse, UpdateOidcProviderData, UpdateOidcProviderResponse, ListOidcRoleMappingsData, CreateOidcRoleMappingData, CreateOidcRoleMappingResponse, TestOidcProviderData, TestOidcProviderResponse, ListOidcProviderUsersData, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageRecentError, GetUsageRecentResponse, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, OidcCallbackData, StartOidcLoginBySlugData, ListPublicProvidersData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListServiceSchedulesData, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, ListScheduleServicesData, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesResponse2 as AttachScheduleServicesResponse, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, DeploymentMetricsGetRangeData, DeploymentMetricsToggleData, DeploymentMetricsGetLatestData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, GetOnDemandCertStatusData, ListOnDemandCertsData, ListOnDemandCertsResponse2 as ListOnDemandCertsResponse, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, UpdateEmailProviderData, UpdateEmailProviderResponse, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponse, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponse, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponse, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsToggleData, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsStatusData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, NodeMetricsGetRangeData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProvider2Data, UpdateEmailProvider2Response, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, ListDeploymentContainerLogsData, GetDeploymentContainerLogContentData, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, ListKnownAiAgentsData, GetProxyLogByRequestIdData, GetAiAgentPagesData, GetAiAgentBreakdownData, GetAiAgentTimelineData, GetAiPageBreakdownData, GetAiStatusBreakdownData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, GetDiskStatusData, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, UpdateOtelIngestData, UpdateOtelIngestResponse2 as UpdateOtelIngestResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; +import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, GetAdminGateData, PatchAdminGateData, PatchAdminGateResponse, ListOidcProvidersData, CreateOidcProviderData, CreateOidcProviderResponse, DeleteOidcProviderData, DeleteOidcProviderResponse, UpdateOidcProviderData, UpdateOidcProviderResponse, ListOidcRoleMappingsData, CreateOidcRoleMappingData, CreateOidcRoleMappingResponse, TestOidcProviderData, TestOidcProviderResponse, ListOidcProviderUsersData, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageRecentError, GetUsageRecentResponse, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, OidcCallbackData, StartOidcLoginBySlugData, ListPublicProvidersData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListServiceSchedulesData, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, ListScheduleServicesData, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesResponse2 as AttachScheduleServicesResponse, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, DeploymentMetricsGetRangeData, DeploymentMetricsToggleData, DeploymentMetricsGetLatestData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, GetOnDemandCertStatusData, ListOnDemandCertsData, ListOnDemandCertsResponse2 as ListOnDemandCertsResponse, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, UpdateEmailProviderData, UpdateEmailProviderResponse, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponse, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponse, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponse, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsToggleData, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsStatusData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, NodeMetricsGetRangeData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProvider2Data, UpdateEmailProvider2Response, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, ListDeploymentContainerLogsData, GetDeploymentContainerLogContentData, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, ListKnownAiAgentsData, GetProxyLogByRequestIdData, GetAiAgentPagesData, GetAiAgentBreakdownData, GetAiAgentTimelineData, GetAiPageBreakdownData, GetAiStatusBreakdownData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, GetDiskStatusData, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; import { client } from '../client.gen'; export type QueryKey = [ @@ -13146,31 +13146,6 @@ export const updateGlobalMcpMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await updateOtelIngest({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - /** * Manually refresh the proxy route table * Reloads all routes from the database into the in-memory proxy cache. diff --git a/web/src/api/client/sdk.gen.ts b/web/src/api/client/sdk.gen.ts index d840df57f..a2c2e8c4f 100644 --- a/web/src/api/client/sdk.gen.ts +++ b/web/src/api/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { type Options as ClientOptions, type Client, type TDataShape, formDataBodySerializer } from './client'; -import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, GetAdminGateData, GetAdminGateResponses, GetAdminGateErrors, PatchAdminGateData, PatchAdminGateResponses, PatchAdminGateErrors, ListOidcProvidersData, ListOidcProvidersResponses, CreateOidcProviderData, CreateOidcProviderResponses, CreateOidcProviderErrors, DeleteOidcProviderData, DeleteOidcProviderResponses, UpdateOidcProviderData, UpdateOidcProviderResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingResponses, TestOidcProviderData, TestOidcProviderResponses, ListOidcProviderUsersData, ListOidcProviderUsersResponses, ListOidcProviderUsersErrors, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponses, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, OidcCallbackData, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, ListPublicProvidersData, ListPublicProvidersResponses, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListServiceSchedulesData, ListServiceSchedulesResponses, ListServiceSchedulesErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, ListScheduleServicesData, ListScheduleServicesResponses, ListScheduleServicesErrors, AttachScheduleServicesData, AttachScheduleServicesResponses, AttachScheduleServicesErrors, DetachScheduleServiceData, DetachScheduleServiceResponses, DetachScheduleServiceErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeResponses, DeploymentMetricsGetRangeErrors, DeploymentMetricsToggleData, DeploymentMetricsToggleResponses, DeploymentMetricsToggleErrors, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetLatestErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, GetOnDemandCertStatusData, GetOnDemandCertStatusResponses, GetOnDemandCertStatusErrors, ListOnDemandCertsData, ListOnDemandCertsResponses, ListOnDemandCertsErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsStatusErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, NodeMetricsGetRangeData, NodeMetricsGetRangeResponses, NodeMetricsGetRangeErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProvider2Data, UpdateEmailProvider2Responses, UpdateEmailProvider2Errors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, ListDeploymentContainerLogsData, ListDeploymentContainerLogsResponses, ListDeploymentContainerLogsErrors, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentResponses, GetDeploymentContainerLogContentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, ListKnownAiAgentsData, ListKnownAiAgentsResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetAiAgentPagesData, GetAiAgentPagesResponses, GetAiAgentPagesErrors, GetAiAgentBreakdownData, GetAiAgentBreakdownResponses, GetAiAgentBreakdownErrors, GetAiAgentTimelineData, GetAiAgentTimelineResponses, GetAiAgentTimelineErrors, GetAiPageBreakdownData, GetAiPageBreakdownResponses, GetAiPageBreakdownErrors, GetAiStatusBreakdownData, GetAiStatusBreakdownResponses, GetAiStatusBreakdownErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, GetDiskStatusData, GetDiskStatusResponses, GetDiskStatusErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, UpdateOtelIngestData, UpdateOtelIngestResponses, UpdateOtelIngestErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; +import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, GetAdminGateData, GetAdminGateResponses, GetAdminGateErrors, PatchAdminGateData, PatchAdminGateResponses, PatchAdminGateErrors, ListOidcProvidersData, ListOidcProvidersResponses, CreateOidcProviderData, CreateOidcProviderResponses, CreateOidcProviderErrors, DeleteOidcProviderData, DeleteOidcProviderResponses, UpdateOidcProviderData, UpdateOidcProviderResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingResponses, TestOidcProviderData, TestOidcProviderResponses, ListOidcProviderUsersData, ListOidcProviderUsersResponses, ListOidcProviderUsersErrors, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponses, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, OidcCallbackData, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, ListPublicProvidersData, ListPublicProvidersResponses, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListServiceSchedulesData, ListServiceSchedulesResponses, ListServiceSchedulesErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, ListScheduleServicesData, ListScheduleServicesResponses, ListScheduleServicesErrors, AttachScheduleServicesData, AttachScheduleServicesResponses, AttachScheduleServicesErrors, DetachScheduleServiceData, DetachScheduleServiceResponses, DetachScheduleServiceErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeResponses, DeploymentMetricsGetRangeErrors, DeploymentMetricsToggleData, DeploymentMetricsToggleResponses, DeploymentMetricsToggleErrors, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetLatestErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, GetOnDemandCertStatusData, GetOnDemandCertStatusResponses, GetOnDemandCertStatusErrors, ListOnDemandCertsData, ListOnDemandCertsResponses, ListOnDemandCertsErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsStatusErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, NodeMetricsGetRangeData, NodeMetricsGetRangeResponses, NodeMetricsGetRangeErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProvider2Data, UpdateEmailProvider2Responses, UpdateEmailProvider2Errors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, ListDeploymentContainerLogsData, ListDeploymentContainerLogsResponses, ListDeploymentContainerLogsErrors, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentResponses, GetDeploymentContainerLogContentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, ListKnownAiAgentsData, ListKnownAiAgentsResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetAiAgentPagesData, GetAiAgentPagesResponses, GetAiAgentPagesErrors, GetAiAgentBreakdownData, GetAiAgentBreakdownResponses, GetAiAgentBreakdownErrors, GetAiAgentTimelineData, GetAiAgentTimelineResponses, GetAiAgentTimelineErrors, GetAiPageBreakdownData, GetAiPageBreakdownResponses, GetAiPageBreakdownErrors, GetAiStatusBreakdownData, GetAiStatusBreakdownResponses, GetAiStatusBreakdownErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, GetDiskStatusData, GetDiskStatusResponses, GetDiskStatusErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; import { client } from './client.gen'; export type Options = ClientOptions & { @@ -9972,34 +9972,6 @@ export const updateGlobalMcp = (options: O }); }; -/** - * Enable or disable OpenTelemetry (OTLP/HTTP) ingestion. - * Dedicated, narrowly-scoped kill-switch guarded by the `OtelWrite` - * permission (distinct from the broad `SettingsWrite` that gates the full - * settings blob), so "manage OpenTelemetry" can be delegated without granting - * full platform-settings access. Read-modify-writes only `otel_ingest.enabled` - * on the singleton settings row; all other fields are untouched. The write - * goes through `update_settings`, which refreshes the in-memory cache and - * publishes `settings_change` NOTIFY so the ingest handlers (even in the - * split proxy process, ADR-017) pick up the change promptly. - */ -export const updateOtelIngest = (options: Options) => { - return (options.client ?? client).patch({ - security: [ - { - scheme: 'bearer', - type: 'http' - } - ], - url: '/settings/otel-ingest', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); -}; - /** * Manually refresh the proxy route table * Reloads all routes from the database into the in-memory proxy cache. diff --git a/web/src/api/client/types.gen.ts b/web/src/api/client/types.gen.ts index ef8d94d42..3f6035b77 100644 --- a/web/src/api/client/types.gen.ts +++ b/web/src/api/client/types.gen.ts @@ -533,6 +533,15 @@ export type AggregatedBucketsResponse = { export type AggregationLevel = 'events' | 'sessions' | 'visitors'; +/** + * The aggregation temporality of a Sum/Histogram/ExponentialHistogram metric. + * + * Mirrors OTel's `AggregationTemporality` proto enum: whether reported values + * are cumulative since the start of the series (Cumulative) or only the delta + * since the previous report (Delta). + */ +export type AggregationTemporality = 'unspecified' | 'delta' | 'cumulative'; + /** * Response wrapping the AI agent breakdown rows. */ @@ -772,13 +781,6 @@ export type AppSettings = { * hardware that already has its own per-host headroom). */ build_limits?: BuildLimitsSettings; - /** - * Nightly Docker/disk cleanup behaviour — retention windows for unused - * deployment images and BuildKit cache, plus the schedule. Operator-tunable - * at runtime; the cleanup scheduler re-reads this on every run, so changes - * take effect without a restart. - */ - cleanup?: CleanupSettings; /** * Binary version tag (e.g. "v0.1.0") of the *console* process * (`temps serve`, role=all or role=console) that last started. Written @@ -822,13 +824,6 @@ export type AppSettings = { monitoring?: MonitoringSettings; multi_node?: MultiNodeSettings; on_demand_tls?: OnDemandTlsSettings; - /** - * OpenTelemetry ingest (OTLP/HTTP) settings. Instance-global kill-switch - * for the inbound metrics/traces/logs ingest endpoints under - * `/api/otel/v1*`. Distinct from `monitoring`, which governs the - * *outbound* Prometheus scraper (a different data source). - */ - otel_ingest?: OtelIngestSettings; preview_domain?: string; preview_gateway?: PreviewGatewaySettings; rate_limiting?: RateLimitSettings; @@ -849,7 +844,6 @@ export type AppSettings = { export type AppSettingsResponse = { agent_sandbox: AgentSandboxSettingsMasked; ai_config: AiConfigSettings; - cleanup: CleanupSettings; container_logs: ContainerLogSettings; disk_space_alert: DiskSpaceAlertSettings; dns_provider: DnsProviderSettingsMasked; @@ -870,14 +864,6 @@ export type AppSettingsResponse = { letsencrypt: LetsEncryptSettings; monitoring: MonitoringSettingsMasked; multi_node: MultiNodeSettingsMasked; - /** - * Whether OTLP/HTTP ingestion is currently accepted. Mirrors - * `otel_ingest.enabled`. Flipped via the dedicated - * `PATCH /settings/otel-ingest` endpoint (guarded by `otel:write`), not - * the generic settings PUT — but surfaced here so the UI can render the - * current state without a second request. - */ - otel_ingest_enabled: boolean; preview_domain: string; preview_gateway: PreviewGatewaySettingsMasked; rate_limiting: RateLimitSettings; @@ -1614,60 +1600,6 @@ export type ChildBackupListResponse = { children: Array; }; -/** - * Nightly Docker/disk cleanup settings. - * - * Governs the background `DockerCleanupService` that reclaims disk on each - * node: dangling images, superseded *tagged* deployment images, and BuildKit - * build cache. Defaults are conservative — safe for a multi-tenant control - * plane — and every value is operator-tunable at runtime (the scheduler - * re-reads settings on every run, so no restart is needed). - * - * Image removal is always DB-driven: an image is only ever removed if its tag - * belongs to a known deployment that is neither live nor inside the - * per-environment rollback window. Base images and in-flight builds are never - * touched, regardless of these values. - */ -export type CleanupSettings = { - /** - * Maximum *unused* age (days) for BuildKit build cache. Cache not touched - * by a build within this window is reclaimed on the next run. Lower = - * reclaims disk sooner but causes more cold (slow) builds. Default 7. - * Typical: 1 (daily-deploy/disk-tight), 3 (weekday-active), 7 (default). - */ - build_cache_max_age_days?: number; - /** - * Hard cap on total BuildKit build-cache size, in megabytes. Enforced - * independently of the age window, so a burst of builds cannot fill the - * disk before the next nightly run. `0` (default) means "no size cap — - * age window only". When set, the least-recently-used cache beyond this - * size is reclaimed regardless of age. - */ - build_cache_max_size_mb?: number; - /** - * Master switch for the nightly cleanup. When `false`, no images or build - * cache are pruned (the static-asset/chunk cleanup still runs, as it is - * unrelated to Docker). Defaults to `true`. - */ - enabled?: boolean; - /** - * Minimum age (days) an unreferenced deployment image must reach before it - * is eligible for removal. Acts as a safety floor against racing a deploy - * whose DB row hasn't reached a live state yet. Default 7. - */ - image_max_age_days?: number; - /** - * How many of the most-recent deployments *per environment* to keep images - * for, so a rollback is always possible even when those deployments are no - * longer live. Default 3. Set 0 to keep only currently-live images. - */ - keep_deployments_per_env?: number; - /** - * Hour of day (UTC, 0–23) the nightly cleanup runs. Default 2 (02:00 UTC). - */ - run_hour_utc?: number; -}; - export type CliDeviceApproveRequest = { user_code: string; }; @@ -2855,10 +2787,23 @@ export type CreatePrResponse = { /** * Request to create a project from a template + * + * Supports two deploy modes: + * * **Fork mode** — when `git_provider_connection_id` is set, the template + * repo is cloned into a new repository under the user's Git account and the + * project tracks that fork (git-push deploys, automatic deploy on push). + * * **One-click public-repo mode** — when `git_provider_connection_id` is + * omitted, the project deploys directly from the template's public source + * repository (no fork, no Git account required). This is the activation + * path: a brand-new user with no Git provider connected can still deploy a + * demo in one click. `repository_name` / `repository_owner` are ignored in + * this mode, and automatic-deploy-on-push is unavailable (there is no fork + * to receive webhooks). */ export type CreateProjectFromTemplateRequest = { /** - * Enable automatic deployment on push (defaults to true) + * Enable automatic deployment on push (defaults to true). Only honoured in + * fork mode; public-repo deploys cannot receive push webhooks. */ automatic_deploy?: boolean; /** @@ -6752,6 +6697,43 @@ export type HierarchyLevel = { name: string; }; +/** + * An explicit-bucket histogram aggregated over a time bucket. + * + * Carries the reduced scalars (count/sum/min/max) plus the explicit bucket + * layout — `bounds` (the upper bounds) and `bucket_counts` (observation counts, + * summed element-wise across the window; length is `bounds.len() + 1`, the last + * entry being the +Inf overflow bucket). With these, a caller can reconstruct + * any quantile (e.g. p95) via cumulative-count interpolation. + */ +export type HistogramSummary = { + /** + * Explicit bucket upper bounds (OTLP `explicit_bounds`), ascending. + */ + bounds: Array; + /** + * Per-bucket observation counts summed element-wise across the window. + * Length is `bounds.len() + 1` (the trailing element is the +Inf bucket). + */ + bucket_counts: Array; + /** + * Total observation count summed across the bucket window. + */ + count: number; + /** + * Maximum observed value, when reported by the producer. + */ + max?: number | null; + /** + * Minimum observed value, when reported by the producer. + */ + min?: number | null; + /** + * Sum of observed values across the bucket window. + */ + sum: number; +}; + export type HourlyPageSessions = { avg_duration_seconds: number; event_count: number; @@ -7913,15 +7895,61 @@ export type MessageContent = string | Array; */ export type MeteredMode = 'derive_from_invoices' | 'use_subscription' | 'ignore'; +/** + * The aggregation applied when reducing raw metric points into a time bucket. + * + * Store-neutral: every storage backend (ClickHouse today, TimescaleDB later) + * must be able to satisfy this contract. `Quantile(q)` carries the requested + * quantile in `[0.0, 1.0]` (e.g. `0.95` for p95). + */ +export type MetricAggregation = 'avg' | 'sum' | 'min' | 'max' | 'count' | 'rate_per_sec' | { + /** + * A quantile of the scalar value in each bucket. The carried `f64` is the + * requested quantile in `[0.0, 1.0]`. + */ + quantile: number; +}; + /** * A time-bucketed metric aggregate for chart display. + * + * Store-neutral response contract. The legacy scalar fields + * (`avg_value`/`min_value`/`max_value`/`count`) are always populated for chart + * back-compat. The richer fields describe the explicitly-requested + * [`MetricAggregation`] (`value`), optional `quantiles`, an optional + * `histogram_summary`, and a `series_key` identifying the label-set when the + * query used `group_by`. */ export type MetricBucket = { avg_value: number; bucket: string; count: number; + histogram_summary?: null | HistogramSummary; max_value: number; min_value: number; + /** + * Computed quantile/value pairs `(quantile, value)` when the query asked for + * quantile aggregation; otherwise empty. + */ + quantiles?: Array<[ + number, + number + ]>; + /** + * The label-set this bucket belongs to, as ordered `(key, value)` pairs, + * when the query grouped by labels. Empty/`None` = the single ungrouped + * aggregate stream. + */ + series_key?: Array<[ + string, + string + ]> | null; + /** + * The value of the requested [`MetricAggregation`] for this bucket. For the + * default `Avg` aggregation this equals `avg_value`. `#[serde(default)]` so + * pre-existing payloads (which only carried avg/min/max/count) still parse. + */ + value?: number; }; /** @@ -7938,14 +7966,10 @@ export type MetricDataPoint = { value: number; }; -export type MetricNamesResponse = { - names: Array; -}; - /** * The type of an OTel metric. */ -export type MetricType = 'gauge' | 'sum' | 'histogram'; +export type MetricType = 'gauge' | 'sum' | 'histogram' | 'exponential_histogram' | 'summary'; export type MetricsOverTimeResponse = { cls: Array; @@ -8008,11 +8032,6 @@ export type MetricsRangeQuery = { range?: string; }; -export type MetricsResponse = { - count: number; - data: Array; -}; - /** * Freshness status: when metrics were last received for this service. */ @@ -8781,31 +8800,13 @@ export type OperationResultsResponse = { operations: Array; }; -/** - * OpenTelemetry ingest (OTLP/HTTP) settings. - * - * Controls the inbound OTLP receiver endpoints (`/api/otel/v1/metrics`, - * `/traces`, `/logs`, and their path-scoped variants). This is the - * data path where deployed applications and infrastructure services - * *push* telemetry into Temps — it is NOT temps self-instrumentation - * (temps does not emit its own OTLP) and NOT the Prometheus scraper - * (see [`MonitoringSettings`], a separate pull-based source). - */ -export type OtelIngestSettings = { - /** - * Master switch for OTLP ingest. When `false`, the ingest handlers - * short-circuit *before* auth/decompress/decode/store and return an - * empty OTLP success envelope (HTTP 200) so SDK exporters treat the - * batch as delivered and do not retry-storm. The background OTel - * analysis loops (anomaly detection, health compute) also pause while - * this is `false`. - * - * Defaults to `true`: OTLP ingest is a pre-existing always-on receiver, - * so a `settings` row written before this field existed deserializes to - * `enabled = true` (preserving current behaviour on upgrade — no silent - * telemetry loss). - */ - enabled?: boolean; +export type OtelMetricNamesResponse = { + names: Array; +}; + +export type OtelMetricsResponse = { + count: number; + data: Array; }; /** @@ -13612,6 +13613,10 @@ export type TemplateResponse = { * Environment variables template */ env_vars: Array; + /** + * Container port the prebuilt image listens on (image deploys only). + */ + exposed_port?: number | null; /** * Feature highlights */ @@ -13621,26 +13626,22 @@ export type TemplateResponse = { */ git: GitRefResponse; /** - * URL to template image/icon - */ - image_url?: string | null; - /** - * Whether the template is featured/promoted + * HTTP health-check path probed after the container starts (image deploys). */ - is_featured: boolean; + health_check_path?: string | null; /** * Prebuilt Docker image reference. When set, the one-click deploy pulls and * runs this image directly (no build); when absent it builds from `git`. */ image?: string | null; /** - * Container port the prebuilt image listens on (image deploys only). + * URL to template image/icon */ - exposed_port?: number | null; + image_url?: string | null; /** - * HTTP health-check path probed after the container starts (image deploys). + * Whether the template is featured/promoted */ - health_check_path?: string | null; + is_featured: boolean; /** * Display name */ @@ -14550,28 +14551,6 @@ export type UpdateOidcProviderRequest = { trust_idp_email?: boolean | null; }; -/** - * Request body for the OTLP ingest kill-switch. - */ -export type UpdateOtelIngestRequest = { - /** - * Whether OTLP/HTTP ingestion (`/api/otel/v1*`) is accepted. When - * `false`, the ingest endpoints accept-and-discard (HTTP 200) and the - * OTel analysis loops pause. - */ - enabled: boolean; -}; - -/** - * Response for the OTLP ingest kill-switch update. - */ -export type UpdateOtelIngestResponse = { - /** - * The effective state after the update. - */ - enabled: boolean; -}; - export type UpdatePreferencesRequest = { preferences: NotificationPreferencesResponse; }; @@ -29078,7 +29057,7 @@ export type ListMetricNamesResponses = { /** * List of metric names */ - 200: MetricNamesResponse; + 200: OtelMetricNamesResponse; }; export type ListMetricNamesResponse = ListMetricNamesResponses[keyof ListMetricNamesResponses]; @@ -29119,11 +29098,31 @@ export type QueryMetricsData = { * Max buckets to return (default: 1000) */ limit?: number; + /** + * Filter by metric type (gauge, sum, histogram, exponential_histogram, summary) + */ + metric_type?: string; + /** + * Per-bucket aggregation: avg (default), sum, min, max, count, rate, p50/p95/p99, quantile:0.95 + */ + aggregation?: string; + /** + * Comma-separated key=value data-point label filters (keys must match [a-zA-Z0-9_.:-]) + */ + label_filters?: string; + /** + * Comma-separated label keys to group series by + */ + group_by?: string; }; url: '/otel/metrics'; }; export type QueryMetricsErrors = { + /** + * Invalid label key + */ + 400: ProblemDetails; /** * Unauthorized */ @@ -29144,7 +29143,7 @@ export type QueryMetricsResponses = { /** * Metrics data */ - 200: MetricsResponse; + 200: OtelMetricsResponse; }; export type QueryMetricsResponse = QueryMetricsResponses[keyof QueryMetricsResponses]; @@ -39793,37 +39792,6 @@ export type UpdateGlobalMcpResponses = { export type UpdateGlobalMcpResponse = UpdateGlobalMcpResponses[keyof UpdateGlobalMcpResponses]; -export type UpdateOtelIngestData = { - body: UpdateOtelIngestRequest; - path?: never; - query?: never; - url: '/settings/otel-ingest'; -}; - -export type UpdateOtelIngestErrors = { - /** - * Unauthorized - */ - 401: unknown; - /** - * Insufficient permissions (requires otel:write) - */ - 403: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type UpdateOtelIngestResponses = { - /** - * OTLP ingest toggle updated - */ - 200: UpdateOtelIngestResponse; -}; - -export type UpdateOtelIngestResponse2 = UpdateOtelIngestResponses[keyof UpdateOtelIngestResponses]; - export type RefreshRouteTableData = { body?: never; path?: never; diff --git a/web/src/pages/MetricsExplorer.tsx b/web/src/pages/MetricsExplorer.tsx index b65eba943..9f3b4dd97 100644 --- a/web/src/pages/MetricsExplorer.tsx +++ b/web/src/pages/MetricsExplorer.tsx @@ -1,4 +1,4 @@ -import { EnvironmentResponse, ProjectResponse } from '@/api/client' +import { EnvironmentResponse, HistogramSummary, ProjectResponse } from '@/api/client' import { getEnvironmentsOptions, listMetricNamesOptions, @@ -21,6 +21,8 @@ import { usePageTitle } from '@/hooks/usePageTitle' import { useQuery } from '@tanstack/react-query' import { format } from 'date-fns' import { + ArrowLeft, + BarChart3, Gauge, LineChart as LineChartIcon, Plus, @@ -232,7 +234,17 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { }, [allNames, nameSearch]) // ── Time-bucketed series ── + // Both bounds are memoized on `timeRange` so they stay STABLE across renders. + // Using `new Date()` inline would change the query key every render and spin + // React Query into an infinite refetch loop. const fromDate = useMemo(() => timeRangeToFrom(timeRange), [timeRange]) + const toDate = useMemo( + () => new Date(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [timeRange], + ) + const fromIso = fromDate.toISOString() + const toIso = toDate.toISOString() const selectedEnv = environments?.find((e) => e.id === environmentId) ?? null const metricsQuery = useQuery({ @@ -242,25 +254,14 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { metric_name: metricName || undefined, service_name: serviceName || undefined, environment: selectedEnv?.name || undefined, - start_time: fromDate.toISOString(), - end_time: new Date().toISOString(), + start_time: fromIso, + end_time: toIso, bucket_interval: RANGE_BUCKET[timeRange], + // The AggKind strings (avg/sum/min/max/count/rate/pNN) map directly to + // the backend's `MetricAggregation::parse`. + aggregation, + label_filters: serializeLabelFilters(labelFilters) || undefined, limit: 1000, - // ── REGEN TODO (Phase C query surface) ── - // The following params exist on the backend handler - // (`MetricQueryParams` in query_handler.rs) but are NOT yet on the - // generated `QueryMetricsData` type in this worktree. Un-comment - // after `bun run openapi-ts` regenerates the SDK against a running - // Phase C server: - // - // metric_type: undefined, // 'gauge' | 'sum' | 'histogram' | ... - // aggregation, // AggKind string ('avg' | 'p95' | 'rate' | ...) - // label_filters: serializeLabelFilters(labelFilters) || undefined, - // group_by: undefined, // comma-separated label keys → per-series buckets - // - // and the response narrows from `MetricsResponse`/`MetricBucket` to - // the richer `OtelMetricsResponse`/`OtelMetricBucket` - // (value / quantiles / histogram_summary / series_key). }, }), // Only fetch once a concrete metric is chosen — querying every metric at @@ -270,29 +271,46 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { const buckets = metricsQuery.data?.data ?? [] - // Map the generated `MetricBucket` rows into the recharts point shape. - // - // REGEN NOTE: with the current (pre-Phase-C) SDK the only series available is - // `avg_value`. After regen, `OtelMetricBucket.value` carries the requested - // aggregation (avg/sum/p95/rate/…) and `series_key` enables multi-series - // rendering. Until then we always plot the average and the aggregation - // selector is advisory. + // Map the `MetricBucket` rows into the chart point shape. `value` carries the + // requested aggregation; for histogram metrics the percentile aggregations are + // recomputed client-side from the per-bucket `histogram_summary` layout, since + // the server's scalar quantile runs over the synthetic mean rather than the + // true distribution. + const isPercentile = aggregation.startsWith('p') const chartData = useMemo( () => - buckets.map((b) => ({ - bucket: b.bucket, - // `value` is the regen-ready aggregation field; the current SDK only - // ships avg_value, so fall back to it. `(b as { value?: number })` - // keeps TS happy before regen widens the type. - value: (b as { value?: number }).value ?? b.avg_value, - avg_value: b.avg_value, - min_value: b.min_value, - max_value: b.max_value, - count: b.count, - })), - [buckets], + buckets.map((b) => { + const hs = b.histogram_summary + const value = + isPercentile && hs && hs.bounds.length > 0 + ? histogramQuantile( + hs.bounds, + hs.bucket_counts, + percentileFromAgg(aggregation), + ) + : (b.value ?? b.avg_value) + return { + bucket: b.bucket, + value, + avg_value: b.avg_value, + min_value: b.min_value, + max_value: b.max_value, + count: b.count, + } + }), + [buckets, isPercentile, aggregation], ) + // Most recent histogram snapshot in range (for the distribution panel). Using + // the latest bucket avoids re-summing cumulative snapshots across time. + const latestHist = useMemo(() => { + for (let i = buckets.length - 1; i >= 0; i--) { + const hs = buckets[i].histogram_summary + if (hs && hs.bounds.length > 0) return hs + } + return null + }, [buckets]) + const aggLabel = AGGREGATIONS.find((a) => a.value === aggregation)?.label ?? 'Average' @@ -462,25 +480,222 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { - {/* Chart / states */} + {metricName ? ( + <> + {/* Selected-metric detail view */} + +
+ 0} + isError={metricsQuery.isError} + errorMessage={ + metricsQuery.error instanceof Error + ? metricsQuery.error.message + : 'Failed to load metric series.' + } + chartData={chartData} + /> +
+ {latestHist && ( +
+ +
+ )} + + ) : ( + // Default view: an overview of ALL metrics, each a mini chart. Click to + // drill into the detailed explorer above. + + )} + + ) +} + +// ── All-metrics overview ────────────────────────────────────────────────────── + +const OVERVIEW_LIMIT = 24 + +function MetricsOverview({ + project, + names, + fromIso, + toIso, + bucketInterval, + aggregation, + onSelect, + isLoadingNames, + totalCount, +}: { + project: ProjectResponse + names: string[] + fromIso: string + toIso: string + bucketInterval: string + aggregation: AggKind + onSelect: (name: string) => void + isLoadingNames: boolean + totalCount: number +}) { + if (isLoadingNames) { + return ( +
+ {[0, 1, 2, 3, 4, 5].map((i) => ( + + ))} +
+ ) + } + + if (names.length === 0) { + return (
- 0} - isError={metricsQuery.isError} - errorMessage={ - metricsQuery.error instanceof Error - ? metricsQuery.error.message - : 'Failed to load metric series.' +
+ ) + } + + const shown = names.slice(0, OVERVIEW_LIMIT) + return ( +
+
+ {shown.map((n) => ( + + ))} +
+ {names.length > shown.length && ( +

+ Showing {shown.length} of {names.length} metrics — use the search in the + Metric selector to find the rest. +

+ )}
) } +function MetricCard({ + project, + metricName, + fromIso, + toIso, + bucketInterval, + aggregation, + onSelect, +}: { + project: ProjectResponse + metricName: string + fromIso: string + toIso: string + bucketInterval: string + aggregation: AggKind + onSelect: (name: string) => void +}) { + const isPercentile = aggregation.startsWith('p') + const q = useQuery({ + ...queryMetricsOptions({ + query: { + project_id: project.id, + metric_name: metricName, + start_time: fromIso, + end_time: toIso, + bucket_interval: bucketInterval, + aggregation, + limit: 500, + }, + }), + enabled: !!project.id, + }) + + const buckets = q.data?.data ?? [] + const isHistogram = buckets.some((b) => b.histogram_summary) + const data = buckets.map((b) => { + const hs = b.histogram_summary + const value = + isPercentile && hs && hs.bounds.length > 0 + ? histogramQuantile(hs.bounds, hs.bucket_counts, percentileFromAgg(aggregation)) + : (b.value ?? b.avg_value) + return { label: formatBucketLabel(b.bucket), value } + }) + const latest = data.length ? data[data.length - 1].value : null + + return ( + + ) +} + // ── Label filter builder ───────────────────────────────────────────────────── function LabelFilterBuilder({ @@ -548,9 +763,9 @@ function LabelFilterBuilder({ ))}

- Label filters are applied server-side once the SDK is regenerated - against the Phase C backend (the label_filters{' '} - query param). They are URL-persisted now so the view stays shareable. + Label filters narrow the series to data points carrying the given + attribute values, server-side. They are URL-persisted so the view + stays shareable.

)} @@ -660,3 +875,118 @@ function formatMetricValue(v: number): string { if (abs >= 1) return v.toFixed(2) return v.toPrecision(3) } + +// ── Histogram helpers ───────────────────────────────────────────────────────── + +function percentileFromAgg(agg: AggKind): number { + switch (agg) { + case 'p50': + return 0.5 + case 'p90': + return 0.9 + case 'p95': + return 0.95 + case 'p99': + return 0.99 + default: + return 0.5 + } +} + +/** + * Quantile from an explicit-bucket histogram via linear interpolation within the + * bucket that crosses the target rank. `bounds` are ascending upper bounds; + * `counts` has length `bounds.length + 1` (the trailing entry is the +Inf + * bucket). Returns 0 for an empty histogram. + */ +function histogramQuantile( + bounds: number[], + counts: number[], + q: number, +): number { + const total = counts.reduce((a, c) => a + c, 0) + if (total === 0) return 0 + const target = q * total + let cumulative = 0 + for (let i = 0; i < counts.length; i++) { + const next = cumulative + counts[i] + if (next >= target && counts[i] > 0) { + const lower = i === 0 ? 0 : bounds[i - 1] + const upper = + i < bounds.length ? bounds[i] : (bounds[bounds.length - 1] ?? lower) + const within = (target - cumulative) / counts[i] + return lower + (upper - lower) * within + } + cumulative = next + } + return bounds[bounds.length - 1] ?? 0 +} + +// ── Histogram distribution panel ────────────────────────────────────────────── + +function HistogramDistribution({ hist }: { hist: HistogramSummary }) { + const total = hist.count + const maxCount = Math.max(1, ...hist.bucket_counts) + const mean = total > 0 ? hist.sum / total : 0 + const stats: { label: string; value: number }[] = [ + { label: 'Count', value: total }, + { label: 'Mean', value: mean }, + { label: 'p50', value: histogramQuantile(hist.bounds, hist.bucket_counts, 0.5) }, + { label: 'p90', value: histogramQuantile(hist.bounds, hist.bucket_counts, 0.9) }, + { label: 'p95', value: histogramQuantile(hist.bounds, hist.bucket_counts, 0.95) }, + { label: 'p99', value: histogramQuantile(hist.bounds, hist.bucket_counts, 0.99) }, + ] + // One row per bucket: label is the [lower, upper) range; the last is +Inf. + const rows = hist.bucket_counts.map((c, i) => { + const lower = i === 0 ? 0 : hist.bounds[i - 1] + const upper = i < hist.bounds.length ? hist.bounds[i] : Infinity + const range = + upper === Infinity + ? `≥ ${formatMetricValue(lower)}` + : `${formatMetricValue(lower)} – ${formatMetricValue(upper)}` + return { range, count: c } + }) + + return ( +
+
+ +

Distribution

+ latest bucket +
+
+ {stats.map((s) => ( +
+ + {s.label} + + + {formatMetricValue(s.value)} + +
+ ))} +
+
+ {rows.map((r, i) => ( +
+ + {r.range} + +
+
+
+ + {r.count} + +
+ ))} +
+
+ ) +} From 1b59c0e1b6b8d4dacc861b81f123708568d4d951 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 10:14:54 +0200 Subject: [PATCH 06/56] feat(otel): metric dashboards v1 (sections + metric tiles) Per-project saved dashboards persisted in Postgres (metric_dashboards table) as a typed JSON layout of sections -> metric tiles. Full CRUD under /api/otel/dashboards following Handler->Service->Data: - temps-entities: metric_dashboards entity + migration. - temps-otel: MetricDashboardService (CRUD, typed DashboardLayout/Section/Tile, aggregation + size/length-bounds validation), handlers (utoipa, permission_guard OtelRead/OtelWrite, audit-logged writes), routes + OtelApiDoc registration, DashboardNotFound -> 404. - get/update/delete are scoped by project_id (defense-in-depth against cross-tenant IDOR: a mismatched project_id returns 404, never another project's dashboard). A corrupt stored layout is logged, not silently swallowed. - 8 service tests (CRUD + validation + pagination cap). Frontend: Dashboards list, dashboard view (sections of metric-chart tiles, reusing the metrics-explorer chart + client-side histogram percentiles, with memoized time bounds), and a builder (add/rename sections, add tiles by metric + aggregation, save). SDK regenerated. Verified end-to-end against a running server + ClickHouse: CRUD round-trips the layout losslessly, IDOR is blocked (404 on mismatched project_id), and the UI renders saved dashboards with live charts. Follow-ups: domain-prefix the nested layout schema names (OtelDashboardLayout etc.) and add handler-layer 401/403 tests. --- Cargo.lock | 1 + crates/temps-entities/src/lib.rs | 3 + .../temps-entities/src/metric_dashboards.rs | 62 +++ ...0260626_000001_create_metric_dashboards.rs | 90 ++++ crates/temps-migrations/src/migration/mod.rs | 2 + crates/temps-otel/Cargo.toml | 5 + crates/temps-otel/src/error.rs | 3 + crates/temps-otel/src/handlers/audit.rs | 100 ++++ .../src/handlers/dashboard_handler.rs | 313 +++++++++++ .../temps-otel/src/handlers/ingest_handler.rs | 6 + crates/temps-otel/src/handlers/mod.rs | 13 + crates/temps-otel/src/lib.rs | 5 + crates/temps-otel/src/plugin.rs | 21 + .../src/services/dashboard_service.rs | 403 ++++++++++++++ crates/temps-otel/src/services/mod.rs | 2 + crates/temps-otel/tests/e2e_http_test.rs | 19 + .../api/client/@tanstack/react-query.gen.ts | 124 ++++- web/src/api/client/sdk.gen.ts | 90 +++- web/src/api/client/types.gen.ts | 304 +++++++++++ web/src/components/dashboard/Sidebar.tsx | 2 + web/src/components/metrics/MetricTile.tsx | 155 ++++++ web/src/components/metrics/metric-format.ts | 97 ++++ web/src/components/metrics/time-range.ts | 39 ++ web/src/pages/DashboardBuilder.tsx | 495 ++++++++++++++++++ web/src/pages/DashboardView.tsx | 190 +++++++ web/src/pages/Dashboards.tsx | 249 +++++++++ web/src/pages/DashboardsRouter.tsx | 28 + web/src/pages/ProjectDetail.tsx | 5 + 28 files changed, 2823 insertions(+), 3 deletions(-) create mode 100644 crates/temps-entities/src/metric_dashboards.rs create mode 100644 crates/temps-migrations/src/migration/m20260626_000001_create_metric_dashboards.rs create mode 100644 crates/temps-otel/src/handlers/audit.rs create mode 100644 crates/temps-otel/src/handlers/dashboard_handler.rs create mode 100644 crates/temps-otel/src/services/dashboard_service.rs create mode 100644 web/src/components/metrics/MetricTile.tsx create mode 100644 web/src/components/metrics/metric-format.ts create mode 100644 web/src/components/metrics/time-range.ts create mode 100644 web/src/pages/DashboardBuilder.tsx create mode 100644 web/src/pages/DashboardView.tsx create mode 100644 web/src/pages/Dashboards.tsx create mode 100644 web/src/pages/DashboardsRouter.tsx diff --git a/Cargo.lock b/Cargo.lock index 01d0cb5e4..06df932a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12166,6 +12166,7 @@ dependencies = [ name = "temps-otel" version = "0.1.0-beta.39" dependencies = [ + "anyhow", "async-trait", "aws-config", "aws-sdk-s3", diff --git a/crates/temps-entities/src/lib.rs b/crates/temps-entities/src/lib.rs index 6975ef626..de9bac186 100644 --- a/crates/temps-entities/src/lib.rs +++ b/crates/temps-entities/src/lib.rs @@ -122,6 +122,9 @@ pub mod status_monitors; // Metrics alert rules pub mod monitoring_alert_rules; +// Metric dashboards (saved per-project dashboard layouts) +pub mod metric_dashboards; + // Webhook entities pub mod webhook_deliveries; pub mod webhooks; diff --git a/crates/temps-entities/src/metric_dashboards.rs b/crates/temps-entities/src/metric_dashboards.rs new file mode 100644 index 000000000..8fa18c984 --- /dev/null +++ b/crates/temps-entities/src/metric_dashboards.rs @@ -0,0 +1,62 @@ +use async_trait::async_trait; +use sea_orm::entity::prelude::*; +use sea_orm::{ActiveValue::Set, ConnectionTrait, DbErr}; +use serde::{Deserialize, Serialize}; +use temps_core::DBDateTime; + +/// A saved metric dashboard for a project. +/// +/// Each dashboard owns a typed JSON `layout` (a `DashboardLayout` defined in +/// the `temps-otel` service layer) describing sections and metric tiles. This +/// table holds config/metadata only — it is Postgres-backed, never ClickHouse. +/// The service layer is responsible for converting between the typed +/// `DashboardLayout` struct and the `serde_json::Value` stored here, exactly as +/// `restore_runs` does for its typed PITR/override payloads. +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "metric_dashboards")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + /// FK to `projects(id)`. Indexed; the list view is scoped by project. + pub project_id: i32, + /// Human-readable dashboard name. + pub name: String, + /// The typed `DashboardLayout` serialized as JSON (JSONB column). + pub layout: serde_json::Value, + pub created_at: DBDateTime, + pub updated_at: DBDateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::projects::Entity", + from = "Column::ProjectId", + to = "super::projects::Column::Id", + on_delete = "Cascade" + )] + Project, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Project.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + async fn before_save(mut self, _db: &C, insert: bool) -> Result + where + C: ConnectionTrait, + { + let now = chrono::Utc::now(); + if insert && self.created_at.is_not_set() { + self.created_at = Set(now); + } + // Always bump updated_at — callers that want to preserve it can + // explicitly Set() before save, though no caller currently does. + self.updated_at = Set(now); + Ok(self) + } +} diff --git a/crates/temps-migrations/src/migration/m20260626_000001_create_metric_dashboards.rs b/crates/temps-migrations/src/migration/m20260626_000001_create_metric_dashboards.rs new file mode 100644 index 000000000..dd9beec4f --- /dev/null +++ b/crates/temps-migrations/src/migration/m20260626_000001_create_metric_dashboards.rs @@ -0,0 +1,90 @@ +//! Migration to create the `metric_dashboards` table. +//! +//! Backs the per-project saved metric dashboards feature. Each row holds a +//! `name` and a typed `layout` (a `DashboardLayout` struct serialized to JSONB +//! by the `temps-otel` service layer). This is config/metadata — Postgres, +//! never ClickHouse. +//! +//! The composite index `(project_id, created_at DESC)` serves the list view, +//! which is always scoped by project and ordered by `created_at` descending — +//! mirroring `otel_trace_summaries (project_id, start_time DESC)`. + +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(MetricDashboards::Table) + .if_not_exists() + .col( + ColumnDef::new(MetricDashboards::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(MetricDashboards::ProjectId) + .integer() + .not_null(), + ) + .col(ColumnDef::new(MetricDashboards::Name).text().not_null()) + .col( + ColumnDef::new(MetricDashboards::Layout) + .json_binary() + .not_null(), + ) + .col( + ColumnDef::new(MetricDashboards::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(MetricDashboards::UpdatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .to_owned(), + ) + .await?; + + // Composite index serving the list view: scoped by project, ordered by + // created_at DESC. SchemaManager's Index builder can't express DESC + // ordering, so we use raw DDL like the otel_trace_summaries migration. + manager + .get_connection() + .execute_unprepared( + "CREATE INDEX IF NOT EXISTS idx_metric_dashboards_project_created \ + ON metric_dashboards (project_id, created_at DESC);", + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(MetricDashboards::Table).to_owned()) + .await?; + Ok(()) + } +} + +#[derive(DeriveIden)] +enum MetricDashboards { + Table, + Id, + ProjectId, + Name, + Layout, + CreatedAt, + UpdatedAt, +} diff --git a/crates/temps-migrations/src/migration/mod.rs b/crates/temps-migrations/src/migration/mod.rs index 77396f726..b37935b51 100644 --- a/crates/temps-migrations/src/migration/mod.rs +++ b/crates/temps-migrations/src/migration/mod.rs @@ -122,6 +122,7 @@ mod m20260618_000001_create_on_demand_cert_attempts; mod m20260618_000002_add_domains_on_demand_backoff; mod m20260619_000001_add_settings_change_trigger; mod m20260621_000001_create_telemetry_milestones; +mod m20260626_000001_create_metric_dashboards; pub struct Migrator; @@ -247,6 +248,7 @@ impl MigratorTrait for Migrator { Box::new(m20260618_000002_add_domains_on_demand_backoff::Migration), Box::new(m20260619_000001_add_settings_change_trigger::Migration), Box::new(m20260621_000001_create_telemetry_milestones::Migration), + Box::new(m20260626_000001_create_metric_dashboards::Migration), ] } } diff --git a/crates/temps-otel/Cargo.toml b/crates/temps-otel/Cargo.toml index 0af3c1e8c..c97f27dc6 100644 --- a/crates/temps-otel/Cargo.toml +++ b/crates/temps-otel/Cargo.toml @@ -28,6 +28,7 @@ serde_json = { workspace = true } # Error handling & logging thiserror = { workspace = true } +anyhow = { workspace = true } tracing = { workspace = true } # Date/Time & UUID @@ -78,6 +79,10 @@ tokio = { workspace = true, features = ["test-util", "macros"] } tower = { workspace = true } http-body-util = { workspace = true } temps-migrations = { path = "../temps-migrations" } +# For the no-op AuditLogger used in the e2e handler test (AuditLogger is an +# #[async_trait] returning anyhow::Result). +async-trait = { workspace = true } +anyhow = { workspace = true } # Real-ClickHouse integration tests (clickhouse_storage_test.rs). Pinned to the # workspace version to avoid the transitive-tree conflict resolved at 0.27.2. # `http_wait_plain` enables the HTTP /ping readiness wait — the clickhouse-server diff --git a/crates/temps-otel/src/error.rs b/crates/temps-otel/src/error.rs index 3d52cd267..c1b59dff3 100644 --- a/crates/temps-otel/src/error.rs +++ b/crates/temps-otel/src/error.rs @@ -48,6 +48,9 @@ pub enum OtelError { #[error("Validation error: {message}")] Validation { message: String }, + #[error("Metric dashboard {dashboard_id} not found")] + DashboardNotFound { dashboard_id: i32 }, + #[error("IO error: {0}")] Io(#[from] std::io::Error), diff --git a/crates/temps-otel/src/handlers/audit.rs b/crates/temps-otel/src/handlers/audit.rs new file mode 100644 index 000000000..6693165a7 --- /dev/null +++ b/crates/temps-otel/src/handlers/audit.rs @@ -0,0 +1,100 @@ +//! Audit operations for metric dashboard write endpoints. + +use anyhow::Result; +use serde::Serialize; +pub use temps_core::AuditContext; +use temps_core::AuditOperation; + +/// Audit event for creating a metric dashboard. +#[derive(Debug, Clone, Serialize)] +pub struct OtelDashboardCreatedAudit { + pub context: AuditContext, + pub dashboard_id: i32, + pub project_id: i32, + pub name: String, +} + +/// Audit event for updating a metric dashboard. +#[derive(Debug, Clone, Serialize)] +pub struct OtelDashboardUpdatedAudit { + pub context: AuditContext, + pub dashboard_id: i32, + pub project_id: i32, +} + +/// Audit event for deleting a metric dashboard. +#[derive(Debug, Clone, Serialize)] +pub struct OtelDashboardDeletedAudit { + pub context: AuditContext, + pub dashboard_id: i32, + pub project_id: i32, +} + +impl AuditOperation for OtelDashboardCreatedAudit { + fn operation_type(&self) -> String { + "OTEL_DASHBOARD_CREATED".to_string() + } + + fn user_id(&self) -> i32 { + self.context.user_id + } + + fn ip_address(&self) -> Option { + self.context.ip_address.clone() + } + + fn user_agent(&self) -> &str { + &self.context.user_agent + } + + fn serialize(&self) -> Result { + serde_json::to_string(self) + .map_err(|e| anyhow::anyhow!("Failed to serialize audit operation: {}", e)) + } +} + +impl AuditOperation for OtelDashboardUpdatedAudit { + fn operation_type(&self) -> String { + "OTEL_DASHBOARD_UPDATED".to_string() + } + + fn user_id(&self) -> i32 { + self.context.user_id + } + + fn ip_address(&self) -> Option { + self.context.ip_address.clone() + } + + fn user_agent(&self) -> &str { + &self.context.user_agent + } + + fn serialize(&self) -> Result { + serde_json::to_string(self) + .map_err(|e| anyhow::anyhow!("Failed to serialize audit operation: {}", e)) + } +} + +impl AuditOperation for OtelDashboardDeletedAudit { + fn operation_type(&self) -> String { + "OTEL_DASHBOARD_DELETED".to_string() + } + + fn user_id(&self) -> i32 { + self.context.user_id + } + + fn ip_address(&self) -> Option { + self.context.ip_address.clone() + } + + fn user_agent(&self) -> &str { + &self.context.user_agent + } + + fn serialize(&self) -> Result { + serde_json::to_string(self) + .map_err(|e| anyhow::anyhow!("Failed to serialize audit operation: {}", e)) + } +} diff --git a/crates/temps-otel/src/handlers/dashboard_handler.rs b/crates/temps-otel/src/handlers/dashboard_handler.rs new file mode 100644 index 000000000..d6781019f --- /dev/null +++ b/crates/temps-otel/src/handlers/dashboard_handler.rs @@ -0,0 +1,313 @@ +//! CRUD handlers for per-project saved metric dashboards. +//! +//! Authenticated via the standard `RequireAuth` flow (JWT/session) since these +//! are accessed by the Temps dashboard UI, not by OTel collectors. GET uses the +//! `OtelRead` permission; writes use `OtelWrite` and are audit-logged +//! best-effort. + +use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::{Extension, Json}; +use serde::{Deserialize, Serialize}; +use tracing::error; +use utoipa::ToSchema; + +use crate::handlers::audit::{ + OtelDashboardCreatedAudit, OtelDashboardDeletedAudit, OtelDashboardUpdatedAudit, +}; +use crate::services::dashboard_service::DashboardLayout; +use crate::OtelAppState; +use temps_auth::{permission_guard, RequireAuth}; +use temps_core::problemdetails::Problem; +use temps_core::{AuditContext, ProblemDetails, RequestMetadata}; +use temps_entities::metric_dashboards::Model; + +// ── Request DTOs ──────────────────────────────────────────────────── + +#[derive(Debug, Deserialize)] +pub struct ListDashboardsParams { + pub project_id: i32, + pub page: Option, + pub page_size: Option, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct CreateDashboardRequest { + pub project_id: i32, + pub name: String, + pub layout: DashboardLayout, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct UpdateDashboardRequest { + pub name: Option, + pub layout: Option, +} + +// ── Response DTOs ─────────────────────────────────────────────────── + +#[derive(Debug, Serialize, ToSchema)] +pub struct OtelDashboardResponse { + pub id: i32, + pub project_id: i32, + pub name: String, + pub layout: DashboardLayout, + #[schema(example = "2025-10-12T12:15:47.609192Z")] + pub created_at: String, + #[schema(example = "2025-10-12T12:15:47.609192Z")] + pub updated_at: String, +} + +impl From for OtelDashboardResponse { + fn from(model: Model) -> Self { + let id = model.id; + // The layout column is JSON and is always written by the service from a + // typed DashboardLayout, so a decode failure means a corrupted row. Do + // not swallow it silently: log it at ERROR (with the id) so the + // corruption is visible, and degrade to an empty layout so a single bad + // row doesn't 500 a whole list. + let layout = serde_json::from_value(model.layout).unwrap_or_else(|e| { + error!( + dashboard_id = id, + error = %e, + "Stored dashboard layout failed to decode; returning empty layout" + ); + DashboardLayout { + sections: Vec::new(), + } + }); + Self { + id, + project_id: model.project_id, + name: model.name, + layout, + created_at: model.created_at.to_rfc3339(), + updated_at: model.updated_at.to_rfc3339(), + } + } +} + +/// Query params scoping a by-id dashboard operation to a project. Required on +/// get/update/delete so a caller cannot touch another project's dashboard by +/// guessing its id (cross-tenant IDOR). +#[derive(Debug, Deserialize)] +pub struct DashboardScopeParams { + pub project_id: i32, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct OtelDashboardsResponse { + pub data: Vec, + pub total: u64, +} + +// ── Handlers ──────────────────────────────────────────────────────── + +/// List dashboards for a project (newest first, paginated). +#[utoipa::path( + tag = "OTel", + get, + path = "/otel/dashboards", + params( + ("project_id" = i32, Query, description = "Project ID"), + ("page" = Option, Query, description = "Page number (default: 1)"), + ("page_size" = Option, Query, description = "Page size (default: 20, max: 100)"), + ), + responses( + (status = 200, description = "Dashboards for the project", body = OtelDashboardsResponse), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +pub async fn list_dashboards( + RequireAuth(auth): RequireAuth, + State(state): State, + Query(params): Query, +) -> Result { + permission_guard!(auth, OtelRead); + + let (items, total) = state + .dashboard_service + .list(params.project_id, params.page, params.page_size) + .await?; + + let data = items.into_iter().map(OtelDashboardResponse::from).collect(); + Ok(Json(OtelDashboardsResponse { data, total })) +} + +/// Create a new dashboard for a project. +#[utoipa::path( + tag = "OTel", + post, + path = "/otel/dashboards", + request_body = CreateDashboardRequest, + responses( + (status = 201, description = "Dashboard created", body = OtelDashboardResponse), + (status = 400, description = "Validation error", body = ProblemDetails), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +pub async fn create_dashboard( + RequireAuth(auth): RequireAuth, + State(state): State, + Extension(metadata): Extension, + Json(request): Json, +) -> Result { + permission_guard!(auth, OtelWrite); + + let model = state + .dashboard_service + .create(request.project_id, request.name, request.layout) + .await?; + + let audit = OtelDashboardCreatedAudit { + context: AuditContext { + user_id: auth.user_id(), + ip_address: Some(metadata.ip_address.clone()), + user_agent: metadata.user_agent.clone(), + }, + dashboard_id: model.id, + project_id: model.project_id, + name: model.name.clone(), + }; + if let Err(e) = state.audit_service.create_audit_log(&audit).await { + error!("Failed to create audit log: {}", e); + } + + Ok(( + StatusCode::CREATED, + Json(OtelDashboardResponse::from(model)), + )) +} + +/// Fetch a single dashboard by id. +#[utoipa::path( + tag = "OTel", + get, + path = "/otel/dashboards/{id}", + params( + ("id" = i32, Path, description = "Dashboard ID"), + ("project_id" = i32, Query, description = "Owning project ID (scopes the lookup)"), + ), + responses( + (status = 200, description = "Dashboard", body = OtelDashboardResponse), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 404, description = "Dashboard not found", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +pub async fn get_dashboard( + RequireAuth(auth): RequireAuth, + State(state): State, + Path(id): Path, + Query(scope): Query, +) -> Result { + permission_guard!(auth, OtelRead); + + let model = state.dashboard_service.get(scope.project_id, id).await?; + Ok(Json(OtelDashboardResponse::from(model))) +} + +/// Update a dashboard's name and/or layout. +#[utoipa::path( + tag = "OTel", + patch, + path = "/otel/dashboards/{id}", + params( + ("id" = i32, Path, description = "Dashboard ID"), + ("project_id" = i32, Query, description = "Owning project ID (scopes the update)"), + ), + request_body = UpdateDashboardRequest, + responses( + (status = 200, description = "Dashboard updated", body = OtelDashboardResponse), + (status = 400, description = "Validation error", body = ProblemDetails), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 404, description = "Dashboard not found", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +pub async fn update_dashboard( + RequireAuth(auth): RequireAuth, + State(state): State, + Extension(metadata): Extension, + Path(id): Path, + Query(scope): Query, + Json(request): Json, +) -> Result { + permission_guard!(auth, OtelWrite); + + let model = state + .dashboard_service + .update(scope.project_id, id, request.name, request.layout) + .await?; + + let audit = OtelDashboardUpdatedAudit { + context: AuditContext { + user_id: auth.user_id(), + ip_address: Some(metadata.ip_address.clone()), + user_agent: metadata.user_agent.clone(), + }, + dashboard_id: model.id, + project_id: model.project_id, + }; + if let Err(e) = state.audit_service.create_audit_log(&audit).await { + error!("Failed to create audit log: {}", e); + } + + Ok(Json(OtelDashboardResponse::from(model))) +} + +/// Delete a dashboard. +#[utoipa::path( + tag = "OTel", + delete, + path = "/otel/dashboards/{id}", + params( + ("id" = i32, Path, description = "Dashboard ID"), + ("project_id" = i32, Query, description = "Owning project ID (scopes the delete)"), + ), + responses( + (status = 204, description = "Dashboard deleted"), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 404, description = "Dashboard not found", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +pub async fn delete_dashboard( + RequireAuth(auth): RequireAuth, + State(state): State, + Extension(metadata): Extension, + Path(id): Path, + Query(scope): Query, +) -> Result { + permission_guard!(auth, OtelWrite); + + state.dashboard_service.delete(scope.project_id, id).await?; + + let audit = OtelDashboardDeletedAudit { + context: AuditContext { + user_id: auth.user_id(), + ip_address: Some(metadata.ip_address.clone()), + user_agent: metadata.user_agent.clone(), + }, + dashboard_id: id, + project_id: scope.project_id, + }; + if let Err(e) = state.audit_service.create_audit_log(&audit).await { + error!("Failed to create audit log: {}", e); + } + + Ok(StatusCode::NO_CONTENT) +} diff --git a/crates/temps-otel/src/handlers/ingest_handler.rs b/crates/temps-otel/src/handlers/ingest_handler.rs index 9653f0733..62879ba0f 100644 --- a/crates/temps-otel/src/handlers/ingest_handler.rs +++ b/crates/temps-otel/src/handlers/ingest_handler.rs @@ -64,6 +64,12 @@ impl From for Problem { .with_title("Project Not Found") .with_detail(error.to_string()) } + OtelError::DashboardNotFound { .. } => { + warn!(error = %error, "OTel dashboard not found"); + problemdetails::new(StatusCode::NOT_FOUND) + .with_title("Dashboard Not Found") + .with_detail(error.to_string()) + } OtelError::Storage { .. } | OtelError::Database(_) | OtelError::S3 { .. } diff --git a/crates/temps-otel/src/handlers/mod.rs b/crates/temps-otel/src/handlers/mod.rs index 38a3c9f36..a41343671 100644 --- a/crates/temps-otel/src/handlers/mod.rs +++ b/crates/temps-otel/src/handlers/mod.rs @@ -1,5 +1,7 @@ //! HTTP handlers for OTLP ingest and query endpoints. +pub mod audit; +pub mod dashboard_handler; pub mod ingest_handler; pub mod query_handler; @@ -81,4 +83,15 @@ pub fn configure_routes() -> Router { "/otel/genai/traces/{project_id}/{trace_id}", get(query_handler::get_genai_trace), ) + // Metric dashboards (per-project saved dashboard CRUD) + .route( + "/otel/dashboards", + get(dashboard_handler::list_dashboards).post(dashboard_handler::create_dashboard), + ) + .route( + "/otel/dashboards/{id}", + get(dashboard_handler::get_dashboard) + .patch(dashboard_handler::update_dashboard) + .delete(dashboard_handler::delete_dashboard), + ) } diff --git a/crates/temps-otel/src/lib.rs b/crates/temps-otel/src/lib.rs index a86fe34dd..66e11516f 100644 --- a/crates/temps-otel/src/lib.rs +++ b/crates/temps-otel/src/lib.rs @@ -71,4 +71,9 @@ pub struct OtelAppState { /// the payload are ignored by `otlp_to_store_point` — source assignment /// is an invariant of the ingest path, not a user-controlled field. pub metrics_write_tx: Option>>, + /// Service backing per-project saved metric dashboard CRUD (Postgres-backed + /// config/metadata, distinct from the ClickHouse/Timescale `OtelStorage`). + pub dashboard_service: std::sync::Arc, + /// Audit logger for dashboard write operations (best-effort, non-fatal). + pub audit_service: std::sync::Arc, } diff --git a/crates/temps-otel/src/plugin.rs b/crates/temps-otel/src/plugin.rs index 60969ad18..6aa0d142d 100644 --- a/crates/temps-otel/src/plugin.rs +++ b/crates/temps-otel/src/plugin.rs @@ -14,6 +14,7 @@ use utoipa::OpenApi as OpenApiTrait; use crate::anomaly::detector::{AnomalyDetector, AnomalyDetectorConfig}; use crate::handlers; +use crate::handlers::dashboard_handler; use crate::handlers::ingest_handler; use crate::handlers::query_handler; use crate::ingest::auth::OtelAuthService; @@ -161,6 +162,11 @@ impl OtelConfig { query_handler::get_pipeline_stats, query_handler::query_genai_traces, query_handler::get_genai_trace, + dashboard_handler::list_dashboards, + dashboard_handler::create_dashboard, + dashboard_handler::get_dashboard, + dashboard_handler::update_dashboard, + dashboard_handler::delete_dashboard, ), components( schemas( @@ -198,6 +204,13 @@ impl OtelConfig { crate::types::GenAiTraceSummary, crate::types::GenAiSpanDetail, crate::types::GenAiEvent, + dashboard_handler::CreateDashboardRequest, + dashboard_handler::UpdateDashboardRequest, + dashboard_handler::OtelDashboardResponse, + dashboard_handler::OtelDashboardsResponse, + crate::services::dashboard_service::DashboardLayout, + crate::services::dashboard_service::DashboardSection, + crate::services::dashboard_service::DashboardTile, ) ), info( @@ -381,11 +394,19 @@ impl TempsPlugin for OtelPlugin { let (metrics_write_tx, mut metrics_write_rx) = tokio::sync::mpsc::channel::>(512); + // Metric dashboards: Postgres-backed config/metadata service plus + // the global audit logger for dashboard write operations. + let dashboard_service = + Arc::new(crate::services::MetricDashboardService::new(db.clone())); + let audit_service = context.require_service::(); + // Create app state for handlers let app_state = OtelAppState { otel_service: otel_service.clone(), metrics_store: Some(metrics_store.clone()), metrics_write_tx: Some(metrics_write_tx), + dashboard_service: dashboard_service.clone(), + audit_service: audit_service.clone(), }; context.register_service(Arc::new(app_state.clone())); diff --git a/crates/temps-otel/src/services/dashboard_service.rs b/crates/temps-otel/src/services/dashboard_service.rs new file mode 100644 index 000000000..1f0f2a2b8 --- /dev/null +++ b/crates/temps-otel/src/services/dashboard_service.rs @@ -0,0 +1,403 @@ +//! Service for per-project saved metric dashboards. +//! +//! Dashboards are Postgres config/metadata (the `metric_dashboards` table), so +//! this service owns its own `Arc` rather than going through +//! the ClickHouse/TimescaleDB-backed `OtelStorage` trait used by `OtelService`. +//! +//! The persisted `layout` column is a `serde_json::Value`, but the API surface +//! and validation always go through the typed [`DashboardLayout`] struct: the +//! service converts at the boundary via `serde_json::to_value` / +//! `serde_json::from_value`, exactly as `restore_runs` does for its typed +//! payloads. No bare `serde_json::Value` ever crosses the handler boundary. + +use std::sync::Arc; + +use sea_orm::ActiveValue::Set; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, + QueryOrder, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use temps_entities::metric_dashboards::{ActiveModel, Column, Entity, Model}; + +use crate::error::OtelError; + +/// The allowlisted aggregations a tile may request. +/// +/// Mirrors the keyword/quantile forms accepted by `MetricAggregation::parse` +/// (`avg|sum|min|max|count|rate` plus `p50|p90|p95|p99`). +const ALLOWED_AGGREGATIONS: &[&str] = &[ + "avg", "sum", "min", "max", "count", "rate", "p50", "p90", "p95", "p99", +]; + +/// Upper bounds to prevent unbounded payloads (storage / render DoS). +const MAX_NAME_LEN: usize = 200; +const MAX_SECTIONS: usize = 50; +const MAX_TILES_PER_SECTION: usize = 50; +const MAX_METRIC_NAME_LEN: usize = 256; + +/// A single metric tile within a dashboard section. +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, PartialEq)] +pub struct DashboardTile { + /// Stable client-generated tile id (used as a React key / for reordering). + pub id: String, + /// The metric name to chart (e.g. `http.server.duration`). + pub metric_name: String, + /// Aggregation applied per bucket: one of + /// `avg|sum|min|max|count|rate|p50|p90|p95|p99`. + pub aggregation: String, + /// Optional display title; falls back to the metric name in the UI. + pub title: Option, +} + +/// A titled group of tiles within a dashboard. +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, PartialEq)] +pub struct DashboardSection { + /// Stable client-generated section id. + pub id: String, + /// Section heading. + pub title: String, + /// Tiles rendered within this section. + pub tiles: Vec, +} + +/// The typed layout persisted (as JSONB) in `metric_dashboards.layout`. +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, PartialEq)] +pub struct DashboardLayout { + /// Ordered sections that make up the dashboard. + pub sections: Vec, +} + +impl DashboardLayout { + /// Validate every tile's aggregation against the allowlist. + /// + /// Returns [`OtelError::Validation`] on the first invalid aggregation so the + /// caller surfaces a 400 rather than persisting an un-renderable tile. + pub fn validate(&self) -> Result<(), OtelError> { + if self.sections.len() > MAX_SECTIONS { + return Err(OtelError::Validation { + message: format!( + "Too many sections ({}, max {MAX_SECTIONS})", + self.sections.len() + ), + }); + } + for section in &self.sections { + if section.tiles.len() > MAX_TILES_PER_SECTION { + return Err(OtelError::Validation { + message: format!( + "Section '{}' has too many tiles ({}, max {MAX_TILES_PER_SECTION})", + section.title, + section.tiles.len() + ), + }); + } + for tile in §ion.tiles { + if tile.metric_name.len() > MAX_METRIC_NAME_LEN { + return Err(OtelError::Validation { + message: format!("Tile metric_name exceeds {MAX_METRIC_NAME_LEN} chars"), + }); + } + let agg = tile.aggregation.trim().to_ascii_lowercase(); + if !ALLOWED_AGGREGATIONS.contains(&agg.as_str()) { + return Err(OtelError::Validation { + message: format!( + "Invalid tile aggregation '{}' (allowed: {})", + tile.aggregation, + ALLOWED_AGGREGATIONS.join(", ") + ), + }); + } + } + } + Ok(()) + } +} + +/// Validate a dashboard name (non-empty after trim, within the length cap). +fn validate_name(name: &str) -> Result<(), OtelError> { + let trimmed = name.trim(); + if trimmed.is_empty() { + return Err(OtelError::Validation { + message: "Dashboard name cannot be empty".to_string(), + }); + } + if trimmed.len() > MAX_NAME_LEN { + return Err(OtelError::Validation { + message: format!("Dashboard name exceeds {MAX_NAME_LEN} characters"), + }); + } + Ok(()) +} + +/// Service managing CRUD over `metric_dashboards`. +pub struct MetricDashboardService { + db: Arc, +} + +impl MetricDashboardService { + pub fn new(db: Arc) -> Self { + Self { db } + } + + /// List dashboards for a project, newest first, paginated. + /// + /// Returns `(items, total)` where `total` is the unpaginated count. + pub async fn list( + &self, + project_id: i32, + page: Option, + page_size: Option, + ) -> Result<(Vec, u64), OtelError> { + let page = page.unwrap_or(1).max(1); + let page_size = std::cmp::min(page_size.unwrap_or(20), 100); + let paginator = Entity::find() + .filter(Column::ProjectId.eq(project_id)) + .order_by_desc(Column::CreatedAt) + .paginate(self.db.as_ref(), page_size); + let total = paginator.num_items().await?; + let items = paginator.fetch_page(page - 1).await?; + Ok((items, total)) + } + + /// Create a dashboard. Validates the layout before persisting. + pub async fn create( + &self, + project_id: i32, + name: String, + layout: DashboardLayout, + ) -> Result { + validate_name(&name)?; + layout.validate()?; + + let layout_value = serde_json::to_value(&layout)?; + let model = ActiveModel { + project_id: Set(project_id), + name: Set(name.trim().to_string()), + layout: Set(layout_value), + ..Default::default() + } + .insert(self.db.as_ref()) + .await?; + Ok(model) + } + + /// Fetch a single dashboard by id, SCOPED to `project_id`. + /// + /// Filtering by project_id (not a bare primary key) prevents a caller + /// operating in one project from reading another project's dashboard by + /// guessing its id (cross-tenant IDOR). + pub async fn get(&self, project_id: i32, id: i32) -> Result { + Entity::find_by_id(id) + .filter(Column::ProjectId.eq(project_id)) + .one(self.db.as_ref()) + .await? + .ok_or(OtelError::DashboardNotFound { dashboard_id: id }) + } + + /// Update a dashboard (scoped to `project_id`). Validates supplied fields. + pub async fn update( + &self, + project_id: i32, + id: i32, + name: Option, + layout: Option, + ) -> Result { + // Ensure the row exists AND belongs to the project (typed 404 otherwise). + let existing = self.get(project_id, id).await?; + + if let Some(ref n) = name { + validate_name(n)?; + } + if let Some(ref l) = layout { + l.validate()?; + } + + let mut active: ActiveModel = existing.into(); + if let Some(n) = name { + active.name = Set(n.trim().to_string()); + } + if let Some(l) = layout { + active.layout = Set(serde_json::to_value(&l)?); + } + let model = active.update(self.db.as_ref()).await?; + Ok(model) + } + + /// Delete a dashboard (scoped to `project_id`). Returns + /// [`OtelError::DashboardNotFound`] when no matching row was removed. + pub async fn delete(&self, project_id: i32, id: i32) -> Result<(), OtelError> { + let result = Entity::delete_many() + .filter(Column::Id.eq(id)) + .filter(Column::ProjectId.eq(project_id)) + .exec(self.db.as_ref()) + .await?; + if result.rows_affected == 0 { + return Err(OtelError::DashboardNotFound { dashboard_id: id }); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Value}; + use std::collections::BTreeMap; + use temps_core::DBDateTime; + + /// Build a MockRow representing a `COUNT(*) AS num_items` result for the + /// sea-orm paginator. `num_items()` reads `try_get::("", "num_items")`. + fn count_row(n: i64) -> BTreeMap { + let mut m = BTreeMap::new(); + m.insert("num_items".to_string(), Value::BigInt(Some(n))); + m + } + + fn sample_layout() -> DashboardLayout { + DashboardLayout { + sections: vec![DashboardSection { + id: "s1".to_string(), + title: "Overview".to_string(), + tiles: vec![DashboardTile { + id: "t1".to_string(), + metric_name: "http.server.duration".to_string(), + aggregation: "p95".to_string(), + title: Some("Latency".to_string()), + }], + }], + } + } + + fn sample_model(id: i32) -> Model { + let now: DBDateTime = chrono::Utc::now(); + Model { + id, + project_id: 7, + name: "My Dashboard".to_string(), + layout: serde_json::to_value(sample_layout()).unwrap(), + created_at: now, + updated_at: now, + } + } + + #[tokio::test] + async fn test_create_success() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![vec![sample_model(1)]]) + .into_connection(); + let service = MetricDashboardService::new(Arc::new(db)); + + let result = service + .create(7, "My Dashboard".to_string(), sample_layout()) + .await; + + assert!(result.is_ok()); + let model = result.unwrap(); + assert_eq!(model.id, 1); + assert_eq!(model.project_id, 7); + assert_eq!(model.name, "My Dashboard"); + } + + #[tokio::test] + async fn test_create_empty_name_validation() { + let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); + let service = MetricDashboardService::new(Arc::new(db)); + + let result = service.create(7, " ".to_string(), sample_layout()).await; + + assert!(matches!(result.unwrap_err(), OtelError::Validation { .. })); + } + + #[tokio::test] + async fn test_create_bad_aggregation_validation() { + let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); + let service = MetricDashboardService::new(Arc::new(db)); + + let mut layout = sample_layout(); + layout.sections[0].tiles[0].aggregation = "median".to_string(); + + let result = service.create(7, "Dash".to_string(), layout).await; + assert!(matches!(result.unwrap_err(), OtelError::Validation { .. })); + } + + #[tokio::test] + async fn test_get_not_found() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![Vec::::new()]) + .into_connection(); + let service = MetricDashboardService::new(Arc::new(db)); + + let result = service.get(7, 999).await; + assert!(matches!( + result.unwrap_err(), + OtelError::DashboardNotFound { dashboard_id: 999 } + )); + } + + #[tokio::test] + async fn test_update_not_found() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![Vec::::new()]) + .into_connection(); + let service = MetricDashboardService::new(Arc::new(db)); + + let result = service + .update(7, 999, Some("New Name".to_string()), None) + .await; + assert!(matches!( + result.unwrap_err(), + OtelError::DashboardNotFound { dashboard_id: 999 } + )); + } + + #[tokio::test] + async fn test_delete_not_found() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results(vec![MockExecResult { + last_insert_id: 0, + rows_affected: 0, + }]) + .into_connection(); + let service = MetricDashboardService::new(Arc::new(db)); + + let result = service.delete(7, 999).await; + assert!(matches!( + result.unwrap_err(), + OtelError::DashboardNotFound { dashboard_id: 999 } + )); + } + + #[tokio::test] + async fn test_delete_success() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results(vec![MockExecResult { + last_insert_id: 0, + rows_affected: 1, + }]) + .into_connection(); + let service = MetricDashboardService::new(Arc::new(db)); + + let result = service.delete(7, 1).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_list_pagination_caps_page_size() { + // Sea-ORM paginator: num_items (COUNT) first, then fetch_page (SELECT). + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![vec![count_row(2)]]) + .append_query_results(vec![vec![sample_model(1), sample_model(2)]]) + .into_connection(); + let service = MetricDashboardService::new(Arc::new(db)); + + // Request an over-cap page_size; should be clamped to 100 internally. + let result = service.list(7, Some(1), Some(10_000)).await; + assert!(result.is_ok()); + let (items, total) = result.unwrap(); + assert_eq!(total, 2); + assert_eq!(items.len(), 2); + } +} diff --git a/crates/temps-otel/src/services/mod.rs b/crates/temps-otel/src/services/mod.rs index 42254f3fa..9fb6e1dbd 100644 --- a/crates/temps-otel/src/services/mod.rs +++ b/crates/temps-otel/src/services/mod.rs @@ -1,7 +1,9 @@ //! Core OTel services. +pub mod dashboard_service; pub mod health_service; pub mod otel_service; +pub use dashboard_service::MetricDashboardService; pub use health_service::HealthComputeService; pub use otel_service::OtelService; diff --git a/crates/temps-otel/tests/e2e_http_test.rs b/crates/temps-otel/tests/e2e_http_test.rs index c13a83182..7e6308471 100644 --- a/crates/temps-otel/tests/e2e_http_test.rs +++ b/crates/temps-otel/tests/e2e_http_test.rs @@ -26,6 +26,20 @@ use temps_otel::services::OtelService; use temps_otel::storage::timescaledb::TimescaleDbStorage; use temps_otel::OtelAppState; +/// No-op audit logger so the handler app state can be built without a real +/// audit service (dashboard write endpoints audit-log best-effort). +struct NoOpAuditLogger; + +#[async_trait::async_trait] +impl temps_core::AuditLogger for NoOpAuditLogger { + async fn create_audit_log( + &self, + _operation: &dyn temps_core::AuditOperation, + ) -> anyhow::Result<()> { + Ok(()) + } +} + /// Known API key for testing. const TEST_API_KEY: &str = "tk_test_e2e_integration_key_12345"; @@ -120,10 +134,15 @@ async fn setup_e2e() -> Option<( let auth_service = Arc::new(OtelAuthService::new(db.clone())); let rate_limiter = Arc::new(RateLimiter::new(10000, Duration::from_secs(60))); let otel_service = Arc::new(OtelService::new(storage, auth_service, rate_limiter)); + let dashboard_service = Arc::new(temps_otel::services::MetricDashboardService::new( + db.clone(), + )); let app_state = OtelAppState { otel_service, metrics_store: None, metrics_write_tx: None, + dashboard_service, + audit_service: Arc::new(NoOpAuditLogger), }; // Create auth middleware that injects AuthContext into request extensions. diff --git a/web/src/api/client/@tanstack/react-query.gen.ts b/web/src/api/client/@tanstack/react-query.gen.ts index e634d8145..3ffca7913 100644 --- a/web/src/api/client/@tanstack/react-query.gen.ts +++ b/web/src/api/client/@tanstack/react-query.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, getAdminGate, patchAdminGate, listOidcProviders, createOidcProvider, deleteOidcProvider, updateOidcProvider, listOidcRoleMappings, createOidcRoleMapping, testOidcProvider, listOidcProviderUsers, deleteOidcRoleMapping, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, oidcCallback, startOidcLoginBySlug, listPublicProviders, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listServiceSchedules, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, listScheduleServices, attachScheduleServices, detachScheduleService, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, deploymentMetricsGetRange, deploymentMetricsToggle, deploymentMetricsGetLatest, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, getOnDemandCertStatus, listOnDemandCerts, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, updateEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, externalServiceMetricsGetRange, externalServiceMetricsGetAlertRules, externalServiceMetricsCreateAlertRule, externalServiceMetricsDeleteAlertRule, externalServiceMetricsUpdateAlertRule, externalServiceMetricsByDatabase, externalServiceMetricsToggle, externalServiceMetricsGetLatest, externalServiceMetricsStatus, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, nodeMetricsGetRange, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider2, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, listDeploymentContainerLogs, getDeploymentContainerLogContent, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, listKnownAiAgents, getProxyLogByRequestId, getAiAgentPages, getAiAgentBreakdown, getAiAgentTimeline, getAiPageBreakdown, getAiStatusBreakdown, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, getDiskStatus, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; +import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, getAdminGate, patchAdminGate, listOidcProviders, createOidcProvider, deleteOidcProvider, updateOidcProvider, listOidcRoleMappings, createOidcRoleMapping, testOidcProvider, listOidcProviderUsers, deleteOidcRoleMapping, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, oidcCallback, startOidcLoginBySlug, listPublicProviders, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listServiceSchedules, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, listScheduleServices, attachScheduleServices, detachScheduleService, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, deploymentMetricsGetRange, deploymentMetricsToggle, deploymentMetricsGetLatest, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, getOnDemandCertStatus, listOnDemandCerts, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, updateEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, externalServiceMetricsGetRange, externalServiceMetricsGetAlertRules, externalServiceMetricsCreateAlertRule, externalServiceMetricsDeleteAlertRule, externalServiceMetricsUpdateAlertRule, externalServiceMetricsByDatabase, externalServiceMetricsToggle, externalServiceMetricsGetLatest, externalServiceMetricsStatus, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, nodeMetricsGetRange, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider2, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, listDashboards, createDashboard, deleteDashboard, getDashboard, updateDashboard, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, listDeploymentContainerLogs, getDeploymentContainerLogContent, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, listKnownAiAgents, getProxyLogByRequestId, getAiAgentPages, getAiAgentBreakdown, getAiAgentTimeline, getAiPageBreakdown, getAiStatusBreakdown, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, getDiskStatus, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; import { queryOptions, type UseMutationOptions, type DefaultError, infiniteQueryOptions, type InfiniteData } from '@tanstack/react-query'; -import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, GetAdminGateData, PatchAdminGateData, PatchAdminGateResponse, ListOidcProvidersData, CreateOidcProviderData, CreateOidcProviderResponse, DeleteOidcProviderData, DeleteOidcProviderResponse, UpdateOidcProviderData, UpdateOidcProviderResponse, ListOidcRoleMappingsData, CreateOidcRoleMappingData, CreateOidcRoleMappingResponse, TestOidcProviderData, TestOidcProviderResponse, ListOidcProviderUsersData, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageRecentError, GetUsageRecentResponse, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, OidcCallbackData, StartOidcLoginBySlugData, ListPublicProvidersData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListServiceSchedulesData, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, ListScheduleServicesData, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesResponse2 as AttachScheduleServicesResponse, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, DeploymentMetricsGetRangeData, DeploymentMetricsToggleData, DeploymentMetricsGetLatestData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, GetOnDemandCertStatusData, ListOnDemandCertsData, ListOnDemandCertsResponse2 as ListOnDemandCertsResponse, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, UpdateEmailProviderData, UpdateEmailProviderResponse, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponse, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponse, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponse, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsToggleData, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsStatusData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, NodeMetricsGetRangeData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProvider2Data, UpdateEmailProvider2Response, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, ListDeploymentContainerLogsData, GetDeploymentContainerLogContentData, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, ListKnownAiAgentsData, GetProxyLogByRequestIdData, GetAiAgentPagesData, GetAiAgentBreakdownData, GetAiAgentTimelineData, GetAiPageBreakdownData, GetAiStatusBreakdownData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, GetDiskStatusData, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; +import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, GetAdminGateData, PatchAdminGateData, PatchAdminGateResponse, ListOidcProvidersData, CreateOidcProviderData, CreateOidcProviderResponse, DeleteOidcProviderData, DeleteOidcProviderResponse, UpdateOidcProviderData, UpdateOidcProviderResponse, ListOidcRoleMappingsData, CreateOidcRoleMappingData, CreateOidcRoleMappingResponse, TestOidcProviderData, TestOidcProviderResponse, ListOidcProviderUsersData, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageRecentError, GetUsageRecentResponse, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, OidcCallbackData, StartOidcLoginBySlugData, ListPublicProvidersData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListServiceSchedulesData, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, ListScheduleServicesData, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesResponse2 as AttachScheduleServicesResponse, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, DeploymentMetricsGetRangeData, DeploymentMetricsToggleData, DeploymentMetricsGetLatestData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, GetOnDemandCertStatusData, ListOnDemandCertsData, ListOnDemandCertsResponse2 as ListOnDemandCertsResponse, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, UpdateEmailProviderData, UpdateEmailProviderResponse, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponse, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponse, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponse, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsToggleData, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsStatusData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, NodeMetricsGetRangeData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProvider2Data, UpdateEmailProvider2Response, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, ListDashboardsData, ListDashboardsError, ListDashboardsResponse, CreateDashboardData, CreateDashboardError, CreateDashboardResponse, DeleteDashboardData, DeleteDashboardError, DeleteDashboardResponse, GetDashboardData, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, ListDeploymentContainerLogsData, GetDeploymentContainerLogContentData, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, ListKnownAiAgentsData, GetProxyLogByRequestIdData, GetAiAgentPagesData, GetAiAgentBreakdownData, GetAiAgentTimelineData, GetAiPageBreakdownData, GetAiStatusBreakdownData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, GetDiskStatusData, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; import { client } from '../client.gen'; export type QueryKey = [ @@ -7366,6 +7366,126 @@ export const listOrdersInfiniteOptions = (options?: Options) => }); }; +export const listDashboardsQueryKey = (options: Options) => createQueryKey('listDashboards', options); + +/** + * List dashboards for a project (newest first, paginated). + */ +export const listDashboardsOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listDashboards({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listDashboardsQueryKey(options) + }); +}; + +export const listDashboardsInfiniteQueryKey = (options: Options): QueryKey> => createQueryKey('listDashboards', options, true); + +/** + * List dashboards for a project (newest first, paginated). + */ +export const listDashboardsInfiniteOptions = (options: Options) => { + return infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { + query: { + page: pageParam + } + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await listDashboards({ + ...options, + ...params, + signal, + throwOnError: true + }); + return data; + }, + queryKey: listDashboardsInfiniteQueryKey(options) + }); +}; + +/** + * Create a new dashboard for a project. + */ +export const createDashboardMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await createDashboard({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Delete a dashboard. + */ +export const deleteDashboardMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteDashboard({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getDashboardQueryKey = (options: Options) => createQueryKey('getDashboard', options); + +/** + * Fetch a single dashboard by id. + */ +export const getDashboardOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getDashboard({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getDashboardQueryKey(options) + }); +}; + +/** + * Update a dashboard's name and/or layout. + */ +export const updateDashboardMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateDashboard({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + export const queryGenaiTracesQueryKey = (options: Options) => createQueryKey('queryGenaiTraces', options); /** diff --git a/web/src/api/client/sdk.gen.ts b/web/src/api/client/sdk.gen.ts index a2c2e8c4f..57c87053c 100644 --- a/web/src/api/client/sdk.gen.ts +++ b/web/src/api/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { type Options as ClientOptions, type Client, type TDataShape, formDataBodySerializer } from './client'; -import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, GetAdminGateData, GetAdminGateResponses, GetAdminGateErrors, PatchAdminGateData, PatchAdminGateResponses, PatchAdminGateErrors, ListOidcProvidersData, ListOidcProvidersResponses, CreateOidcProviderData, CreateOidcProviderResponses, CreateOidcProviderErrors, DeleteOidcProviderData, DeleteOidcProviderResponses, UpdateOidcProviderData, UpdateOidcProviderResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingResponses, TestOidcProviderData, TestOidcProviderResponses, ListOidcProviderUsersData, ListOidcProviderUsersResponses, ListOidcProviderUsersErrors, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponses, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, OidcCallbackData, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, ListPublicProvidersData, ListPublicProvidersResponses, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListServiceSchedulesData, ListServiceSchedulesResponses, ListServiceSchedulesErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, ListScheduleServicesData, ListScheduleServicesResponses, ListScheduleServicesErrors, AttachScheduleServicesData, AttachScheduleServicesResponses, AttachScheduleServicesErrors, DetachScheduleServiceData, DetachScheduleServiceResponses, DetachScheduleServiceErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeResponses, DeploymentMetricsGetRangeErrors, DeploymentMetricsToggleData, DeploymentMetricsToggleResponses, DeploymentMetricsToggleErrors, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetLatestErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, GetOnDemandCertStatusData, GetOnDemandCertStatusResponses, GetOnDemandCertStatusErrors, ListOnDemandCertsData, ListOnDemandCertsResponses, ListOnDemandCertsErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsStatusErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, NodeMetricsGetRangeData, NodeMetricsGetRangeResponses, NodeMetricsGetRangeErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProvider2Data, UpdateEmailProvider2Responses, UpdateEmailProvider2Errors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, ListDeploymentContainerLogsData, ListDeploymentContainerLogsResponses, ListDeploymentContainerLogsErrors, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentResponses, GetDeploymentContainerLogContentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, ListKnownAiAgentsData, ListKnownAiAgentsResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetAiAgentPagesData, GetAiAgentPagesResponses, GetAiAgentPagesErrors, GetAiAgentBreakdownData, GetAiAgentBreakdownResponses, GetAiAgentBreakdownErrors, GetAiAgentTimelineData, GetAiAgentTimelineResponses, GetAiAgentTimelineErrors, GetAiPageBreakdownData, GetAiPageBreakdownResponses, GetAiPageBreakdownErrors, GetAiStatusBreakdownData, GetAiStatusBreakdownResponses, GetAiStatusBreakdownErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, GetDiskStatusData, GetDiskStatusResponses, GetDiskStatusErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; +import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, GetAdminGateData, GetAdminGateResponses, GetAdminGateErrors, PatchAdminGateData, PatchAdminGateResponses, PatchAdminGateErrors, ListOidcProvidersData, ListOidcProvidersResponses, CreateOidcProviderData, CreateOidcProviderResponses, CreateOidcProviderErrors, DeleteOidcProviderData, DeleteOidcProviderResponses, UpdateOidcProviderData, UpdateOidcProviderResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingResponses, TestOidcProviderData, TestOidcProviderResponses, ListOidcProviderUsersData, ListOidcProviderUsersResponses, ListOidcProviderUsersErrors, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponses, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, OidcCallbackData, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, ListPublicProvidersData, ListPublicProvidersResponses, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListServiceSchedulesData, ListServiceSchedulesResponses, ListServiceSchedulesErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, ListScheduleServicesData, ListScheduleServicesResponses, ListScheduleServicesErrors, AttachScheduleServicesData, AttachScheduleServicesResponses, AttachScheduleServicesErrors, DetachScheduleServiceData, DetachScheduleServiceResponses, DetachScheduleServiceErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeResponses, DeploymentMetricsGetRangeErrors, DeploymentMetricsToggleData, DeploymentMetricsToggleResponses, DeploymentMetricsToggleErrors, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetLatestErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, GetOnDemandCertStatusData, GetOnDemandCertStatusResponses, GetOnDemandCertStatusErrors, ListOnDemandCertsData, ListOnDemandCertsResponses, ListOnDemandCertsErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsStatusErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, NodeMetricsGetRangeData, NodeMetricsGetRangeResponses, NodeMetricsGetRangeErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProvider2Data, UpdateEmailProvider2Responses, UpdateEmailProvider2Errors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, ListDashboardsData, ListDashboardsResponses, ListDashboardsErrors, CreateDashboardData, CreateDashboardResponses, CreateDashboardErrors, DeleteDashboardData, DeleteDashboardResponses, DeleteDashboardErrors, GetDashboardData, GetDashboardResponses, GetDashboardErrors, UpdateDashboardData, UpdateDashboardResponses, UpdateDashboardErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, ListDeploymentContainerLogsData, ListDeploymentContainerLogsResponses, ListDeploymentContainerLogsErrors, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentResponses, GetDeploymentContainerLogContentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, ListKnownAiAgentsData, ListKnownAiAgentsResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetAiAgentPagesData, GetAiAgentPagesResponses, GetAiAgentPagesErrors, GetAiAgentBreakdownData, GetAiAgentBreakdownResponses, GetAiAgentBreakdownErrors, GetAiAgentTimelineData, GetAiAgentTimelineResponses, GetAiAgentTimelineErrors, GetAiPageBreakdownData, GetAiPageBreakdownResponses, GetAiPageBreakdownErrors, GetAiStatusBreakdownData, GetAiStatusBreakdownResponses, GetAiStatusBreakdownErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, GetDiskStatusData, GetDiskStatusResponses, GetDiskStatusErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; import { client } from './client.gen'; export type Options = ClientOptions & { @@ -5757,6 +5757,94 @@ export const listOrders = (options?: Optio }); }; +/** + * List dashboards for a project (newest first, paginated). + */ +export const listDashboards = (options: Options) => { + return (options.client ?? client).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/otel/dashboards', + ...options + }); +}; + +/** + * Create a new dashboard for a project. + */ +export const createDashboard = (options: Options) => { + return (options.client ?? client).post({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/otel/dashboards', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Delete a dashboard. + */ +export const deleteDashboard = (options: Options) => { + return (options.client ?? client).delete({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/otel/dashboards/{id}', + ...options + }); +}; + +/** + * Fetch a single dashboard by id. + */ +export const getDashboard = (options: Options) => { + return (options.client ?? client).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/otel/dashboards/{id}', + ...options + }); +}; + +/** + * Update a dashboard's name and/or layout. + */ +export const updateDashboard = (options: Options) => { + return (options.client ?? client).patch({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/otel/dashboards/{id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + /** * Query GenAI trace summaries — traces containing spans with `gen_ai.*` attributes. */ diff --git a/web/src/api/client/types.gen.ts b/web/src/api/client/types.gen.ts index 3f6035b77..284542f05 100644 --- a/web/src/api/client/types.gen.ts +++ b/web/src/api/client/types.gen.ts @@ -2504,6 +2504,12 @@ export type CreateDsnRequest = { name?: string | null; }; +export type CreateDashboardRequest = { + layout: DashboardLayout; + name: string; + project_id: number; +}; + /** * Request to create a new DNS provider */ @@ -3174,6 +3180,16 @@ export type CustomerMovementResponse = { new_customers: number; }; +/** + * The typed layout persisted (as JSONB) in `metric_dashboards.layout`. + */ +export type DashboardLayout = { + /** + * Ordered sections that make up the dashboard. + */ + sections: Array; +}; + /** * Query parameters for batch dashboard analytics */ @@ -3204,6 +3220,47 @@ export type DashboardProjectsAnalyticsResponse = { }; }; +/** + * A titled group of tiles within a dashboard. + */ +export type DashboardSection = { + /** + * Stable client-generated section id. + */ + id: string; + /** + * Tiles rendered within this section. + */ + tiles: Array; + /** + * Section heading. + */ + title: string; +}; + +/** + * A single metric tile within a dashboard section. + */ +export type DashboardTile = { + /** + * Aggregation applied per bucket: one of + * `avg|sum|min|max|count|rate|p50|p90|p95|p99`. + */ + aggregation: string; + /** + * Stable client-generated tile id (used as a React key / for reordering). + */ + id: string; + /** + * The metric name to chart (e.g. `http.server.duration`). + */ + metric_name: string; + /** + * Optional display title; falls back to the metric name in the UI. + */ + title?: string | null; +}; + /** * A specific data implication the user needs to understand */ @@ -8800,6 +8857,20 @@ export type OperationResultsResponse = { operations: Array; }; +export type OtelDashboardResponse = { + created_at: string; + id: number; + layout: DashboardLayout; + name: string; + project_id: number; + updated_at: string; +}; + +export type OtelDashboardsResponse = { + data: Array; + total: number; +}; + export type OtelMetricNamesResponse = { names: Array; }; @@ -14255,6 +14326,11 @@ export type UpdateCustomDomainRequest = { status_code?: number | null; }; +export type UpdateDashboardRequest = { + layout?: null | DashboardLayout; + name?: string | null; +}; + export type UpdateDeploymentConfigRequest = { automaticDeploy?: boolean | null; cpuLimit?: number | null; @@ -28752,6 +28828,234 @@ export type ListOrdersResponses = { export type ListOrdersResponse2 = ListOrdersResponses[keyof ListOrdersResponses]; +export type ListDashboardsData = { + body?: never; + path?: never; + query: { + /** + * Project ID + */ + project_id: number; + /** + * Page number (default: 1) + */ + page?: number; + /** + * Page size (default: 20, max: 100) + */ + page_size?: number; + }; + url: '/otel/dashboards'; +}; + +export type ListDashboardsErrors = { + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type ListDashboardsError = ListDashboardsErrors[keyof ListDashboardsErrors]; + +export type ListDashboardsResponses = { + /** + * Dashboards for the project + */ + 200: OtelDashboardsResponse; +}; + +export type ListDashboardsResponse = ListDashboardsResponses[keyof ListDashboardsResponses]; + +export type CreateDashboardData = { + body: CreateDashboardRequest; + path?: never; + query?: never; + url: '/otel/dashboards'; +}; + +export type CreateDashboardErrors = { + /** + * Validation error + */ + 400: ProblemDetails; + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type CreateDashboardError = CreateDashboardErrors[keyof CreateDashboardErrors]; + +export type CreateDashboardResponses = { + /** + * Dashboard created + */ + 201: OtelDashboardResponse; +}; + +export type CreateDashboardResponse = CreateDashboardResponses[keyof CreateDashboardResponses]; + +export type DeleteDashboardData = { + body?: never; + path: { + /** + * Dashboard ID + */ + id: number; + }; + query: { + /** + * Owning project ID (scopes the delete) + */ + project_id: number; + }; + url: '/otel/dashboards/{id}'; +}; + +export type DeleteDashboardErrors = { + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Dashboard not found + */ + 404: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type DeleteDashboardError = DeleteDashboardErrors[keyof DeleteDashboardErrors]; + +export type DeleteDashboardResponses = { + /** + * Dashboard deleted + */ + 204: void; +}; + +export type DeleteDashboardResponse = DeleteDashboardResponses[keyof DeleteDashboardResponses]; + +export type GetDashboardData = { + body?: never; + path: { + /** + * Dashboard ID + */ + id: number; + }; + query: { + /** + * Owning project ID (scopes the lookup) + */ + project_id: number; + }; + url: '/otel/dashboards/{id}'; +}; + +export type GetDashboardErrors = { + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Dashboard not found + */ + 404: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type GetDashboardError = GetDashboardErrors[keyof GetDashboardErrors]; + +export type GetDashboardResponses = { + /** + * Dashboard + */ + 200: OtelDashboardResponse; +}; + +export type GetDashboardResponse = GetDashboardResponses[keyof GetDashboardResponses]; + +export type UpdateDashboardData = { + body: UpdateDashboardRequest; + path: { + /** + * Dashboard ID + */ + id: number; + }; + query: { + /** + * Owning project ID (scopes the update) + */ + project_id: number; + }; + url: '/otel/dashboards/{id}'; +}; + +export type UpdateDashboardErrors = { + /** + * Validation error + */ + 400: ProblemDetails; + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Dashboard not found + */ + 404: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type UpdateDashboardError = UpdateDashboardErrors[keyof UpdateDashboardErrors]; + +export type UpdateDashboardResponses = { + /** + * Dashboard updated + */ + 200: OtelDashboardResponse; +}; + +export type UpdateDashboardResponse = UpdateDashboardResponses[keyof UpdateDashboardResponses]; + export type QueryGenaiTracesData = { body?: never; path?: never; diff --git a/web/src/components/dashboard/Sidebar.tsx b/web/src/components/dashboard/Sidebar.tsx index 449bc468f..8b9537194 100644 --- a/web/src/components/dashboard/Sidebar.tsx +++ b/web/src/components/dashboard/Sidebar.tsx @@ -43,6 +43,7 @@ import { Key, KeyRound, Layers, + LayoutDashboard, LineChart, LogOut, Mail, @@ -827,6 +828,7 @@ const projectBaseNav: ProjectNavItem[] = [ { title: 'Uptime', url: 'monitors', icon: Activity }, { title: 'Metrics', url: 'monitoring', icon: Gauge }, { title: 'OTel Metrics', url: 'metrics', icon: LineChart }, + { title: 'Dashboards', url: 'dashboards', icon: LayoutDashboard }, { title: 'Traces', url: 'traces', icon: Network }, { title: 'AI Traces', url: 'ai-gateway?tab=activity', icon: Bot }, { title: 'Request Logs', url: 'request-logs', icon: Rss }, diff --git a/web/src/components/metrics/MetricTile.tsx b/web/src/components/metrics/MetricTile.tsx new file mode 100644 index 000000000..b2d094e02 --- /dev/null +++ b/web/src/components/metrics/MetricTile.tsx @@ -0,0 +1,155 @@ +import { ProjectResponse } from '@/api/client' +import { queryMetricsOptions } from '@/api/client/@tanstack/react-query.gen' +import { Badge } from '@/components/ui/badge' +import { Skeleton } from '@/components/ui/skeleton' +import { ThresholdLineChart } from '@/components/charts/threshold-line-chart' +import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' +import { + aggregationLabel, + formatBucketLabel, + formatMetricValue, + histogramQuantile, + percentileFromAgg, +} from './metric-format' + +interface MetricTileProps { + project: ProjectResponse + metricName: string + /** Aggregation token (avg|sum|min|max|count|rate|pNN). */ + aggregation: string + /** Optional display title; falls back to the metric name. */ + title?: string | null + /** ISO start bound — memoize upstream so the query key stays stable. */ + fromIso: string + /** ISO end bound — memoize upstream so the query key stays stable. */ + toIso: string + /** Canonical ClickHouse bucket interval (e.g. "15 minutes"). */ + bucketInterval: string + /** Chart body height in px. */ + height?: number +} + +/** + * A single dashboard metric tile: fetches a time-bucketed series for one metric + * + aggregation and renders it as a compact line chart. Reuses the exact query + * shape and histogram-percentile recomputation from MetricsExplorer so saved + * dashboards render identically to the ad-hoc explorer. + * + * Time bounds are passed in as already-memoized ISO strings — callers MUST NOT + * pass an inline `new Date()` (that bug was fixed in MetricsExplorer: a fresh + * Date every render changes the query key and spins React Query into an + * infinite refetch loop). + */ +export function MetricTile({ + project, + metricName, + aggregation, + title, + fromIso, + toIso, + bucketInterval, + height = 160, +}: MetricTileProps) { + const isPercentile = aggregation.startsWith('p') + + const q = useQuery({ + ...queryMetricsOptions({ + query: { + project_id: project.id, + metric_name: metricName, + start_time: fromIso, + end_time: toIso, + bucket_interval: bucketInterval, + // AggKind strings map directly to the backend's MetricAggregation::parse. + aggregation, + limit: 500, + }, + }), + enabled: !!project.id && metricName.length > 0, + }) + + const buckets = q.data?.data ?? [] + const isHistogram = buckets.some((b) => b.histogram_summary) + + const data = useMemo( + () => + buckets.map((b) => { + const hs = b.histogram_summary + const value = + isPercentile && hs && hs.bounds.length > 0 + ? histogramQuantile( + hs.bounds, + hs.bucket_counts, + percentileFromAgg(aggregation), + ) + : (b.value ?? b.avg_value) + return { label: formatBucketLabel(b.bucket), value } + }), + [buckets, isPercentile, aggregation], + ) + + const latest = data.length ? data[data.length - 1].value : null + const displayTitle = title?.trim() ? title : metricName + + return ( +
+
+ + {displayTitle} + +
+ {isHistogram && ( + + histogram + + )} + + {aggregationLabel(aggregation)} + +
+
+ + {q.isPending ? ( + + ) : q.isError ? ( +
+ Failed to load metric +
+ ) : data.length === 0 ? ( +
+ No data in range +
+ ) : ( + formatMetricValue(v)} + yTickFormatter={(v) => formatMetricValue(v)} + /> + )} + +
+ latest + + {latest != null ? formatMetricValue(latest) : '—'} + +
+
+ ) +} diff --git a/web/src/components/metrics/metric-format.ts b/web/src/components/metrics/metric-format.ts new file mode 100644 index 000000000..76e601bb0 --- /dev/null +++ b/web/src/components/metrics/metric-format.ts @@ -0,0 +1,97 @@ +// Shared metric formatting + histogram helpers. +// +// Extracted from MetricsExplorer.tsx so the dashboard tiles render values and +// percentiles identically to the explorer. Keep these in lockstep with the +// explorer's local copies (the explorer predates this module and still inlines +// them; both compute the same thing). + +import { format } from 'date-fns' + +/** The aggregation tokens the backend's `MetricAggregation::parse` accepts. */ +export type AggKind = + | 'avg' + | 'sum' + | 'min' + | 'max' + | 'count' + | 'rate' + | 'p50' + | 'p90' + | 'p95' + | 'p99' + +export const AGGREGATIONS: { value: AggKind; label: string }[] = [ + { value: 'avg', label: 'Average' }, + { value: 'sum', label: 'Sum' }, + { value: 'min', label: 'Min' }, + { value: 'max', label: 'Max' }, + { value: 'count', label: 'Count' }, + { value: 'rate', label: 'Rate / sec' }, + { value: 'p50', label: 'p50' }, + { value: 'p90', label: 'p90' }, + { value: 'p95', label: 'p95' }, + { value: 'p99', label: 'p99' }, +] + +export function aggregationLabel(agg: string): string { + return AGGREGATIONS.find((a) => a.value === agg)?.label ?? agg +} + +export function formatBucketLabel(bucket: string): string { + const d = new Date(bucket) + if (Number.isNaN(d.getTime())) return bucket + return format(d, 'MMM d HH:mm') +} + +export function formatMetricValue(v: number): string { + if (!Number.isFinite(v)) return '—' + const abs = Math.abs(v) + if (abs >= 1_000_000) return `${(v / 1_000_000).toFixed(2)}M` + if (abs >= 1_000) return `${(v / 1_000).toFixed(2)}k` + if (abs >= 1) return v.toFixed(2) + return v.toPrecision(3) +} + +export function percentileFromAgg(agg: string): number { + switch (agg) { + case 'p50': + return 0.5 + case 'p90': + return 0.9 + case 'p95': + return 0.95 + case 'p99': + return 0.99 + default: + return 0.5 + } +} + +/** + * Quantile from an explicit-bucket histogram via linear interpolation within the + * bucket that crosses the target rank. `bounds` are ascending upper bounds; + * `counts` has length `bounds.length + 1` (the trailing entry is the +Inf + * bucket). Returns 0 for an empty histogram. + */ +export function histogramQuantile( + bounds: number[], + counts: number[], + q: number, +): number { + const total = counts.reduce((a, c) => a + c, 0) + if (total === 0) return 0 + const target = q * total + let cumulative = 0 + for (let i = 0; i < counts.length; i++) { + const next = cumulative + counts[i] + if (next >= target && counts[i] > 0) { + const lower = i === 0 ? 0 : bounds[i - 1] + const upper = + i < bounds.length ? bounds[i] : (bounds[bounds.length - 1] ?? lower) + const within = (target - cumulative) / counts[i] + return lower + (upper - lower) * within + } + cumulative = next + } + return bounds[bounds.length - 1] ?? 0 +} diff --git a/web/src/components/metrics/time-range.ts b/web/src/components/metrics/time-range.ts new file mode 100644 index 000000000..492376943 --- /dev/null +++ b/web/src/components/metrics/time-range.ts @@ -0,0 +1,39 @@ +// Shared time-range selection for metric views (explorer + dashboards). +// +// Mirrors the RANGE_BUCKET / timeRangeToFrom contract from MetricsExplorer: the +// backend's `translate_bucket_interval` parses these canonical "N unit" strings +// through a strict allowlist, so keep this map in lockstep with it. + +export type TimeRange = '1h' | '6h' | '24h' | '7d' | '30d' + +export const TIME_RANGES: { value: TimeRange; label: string }[] = [ + { value: '1h', label: 'Last hour' }, + { value: '6h', label: 'Last 6 hours' }, + { value: '24h', label: 'Last 24 hours' }, + { value: '7d', label: 'Last 7 days' }, + { value: '30d', label: 'Last 30 days' }, +] + +export const RANGE_BUCKET: Record = { + '1h': '1 minute', + '6h': '5 minutes', + '24h': '15 minutes', + '7d': '1 hour', + '30d': '6 hours', +} + +const RANGE_MS: Record = { + '1h': 60 * 60 * 1000, + '6h': 6 * 60 * 60 * 1000, + '24h': 24 * 60 * 60 * 1000, + '7d': 7 * 24 * 60 * 60 * 1000, + '30d': 30 * 24 * 60 * 60 * 1000, +} + +export function timeRangeToFrom(range: TimeRange, now: number): Date { + return new Date(now - RANGE_MS[range]) +} + +export function isTimeRange(v: string | null | undefined): v is TimeRange { + return !!v && TIME_RANGES.some((t) => t.value === v) +} diff --git a/web/src/pages/DashboardBuilder.tsx b/web/src/pages/DashboardBuilder.tsx new file mode 100644 index 000000000..4e60dc2e1 --- /dev/null +++ b/web/src/pages/DashboardBuilder.tsx @@ -0,0 +1,495 @@ +import { ProjectResponse } from '@/api/client' +// REGEN: bun run openapi-ts — these builders/types come from the new +// /otel/dashboards endpoints (operationIds get_dashboard / create_dashboard / +// update_dashboard). They are absent from the committed SDK; imported here so +// the builder is regen-ready instead of hand-rolling fetch (project rule). +import { + createDashboardMutation, + getDashboardOptions, + listMetricNamesOptions, + updateDashboardMutation, +} from '@/api/client/@tanstack/react-query.gen' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { SearchableSelect } from '@/components/ui/searchable-select' +import { Skeleton } from '@/components/ui/skeleton' +import { AGGREGATIONS } from '@/components/metrics/metric-format' +import { zodResolver } from '@hookform/resolvers/zod' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { ArrowLeft, GripVertical, Plus, Trash2 } from 'lucide-react' +import { useMemo } from 'react' +import { + useFieldArray, + useForm, + type Control, +} from 'react-hook-form' +import { useNavigate, useParams } from 'react-router-dom' +import { toast } from 'sonner' +import { z } from 'zod' + +interface DashboardBuilderProps { + project: ProjectResponse +} + +const AGGREGATION_VALUES = AGGREGATIONS.map((a) => a.value) as [ + string, + ...string[], +] + +const tileSchema = z.object({ + id: z.string(), + metric_name: z.string().min(1, 'Pick a metric'), + aggregation: z.enum(AGGREGATION_VALUES), + title: z.string().optional(), +}) + +const sectionSchema = z.object({ + id: z.string(), + title: z.string().min(1, 'Section title is required'), + tiles: z.array(tileSchema), +}) + +const dashboardSchema = z.object({ + name: z.string().min(1, 'Name is required'), + sections: z.array(sectionSchema), +}) + +type DashboardFormData = z.infer + +/** Stable-enough client id for new sections/tiles (UI key + layout id). */ +function makeId(prefix: string): string { + return `${prefix}-${Math.random().toString(36).slice(2, 10)}` +} + +/** + * Narrow a stored aggregation string (the backend field is a plain `string`) + * into the form's enum union, falling back to `avg` for unknown values so the + * select stays controlled. + */ +function coerceAggregation(value: string): (typeof AGGREGATION_VALUES)[number] { + return (AGGREGATION_VALUES as readonly string[]).includes(value) + ? (value as (typeof AGGREGATION_VALUES)[number]) + : 'avg' +} + +function emptyDefaults(): DashboardFormData { + return { + name: '', + sections: [ + { + id: makeId('section'), + title: 'Overview', + tiles: [], + }, + ], + } +} + +export default function DashboardBuilder({ project }: DashboardBuilderProps) { + const navigate = useNavigate() + const queryClient = useQueryClient() + const { dashboardId } = useParams() + const isEditing = !!dashboardId + const id = Number(dashboardId) + + const existingQuery = useQuery({ + ...getDashboardOptions({ path: { id }, query: { project_id: project.id } }), + enabled: isEditing && Number.isFinite(id), + }) + + const namesQuery = useQuery({ + ...listMetricNamesOptions({ path: { project_id: project.id } }), + enabled: !!project.id, + }) + const metricOptions = useMemo( + () => + (namesQuery.data?.names ?? []).map((n) => ({ + value: n, + label: n, + })), + [namesQuery.data], + ) + + const defaultValues = useMemo(() => { + const existing = existingQuery.data + if (existing) { + const sections = existing.layout?.sections ?? [] + return { + name: existing.name, + sections: + sections.length > 0 + ? sections.map((s) => ({ + id: s.id || makeId('section'), + title: s.title, + tiles: (s.tiles ?? []).map((t) => ({ + id: t.id || makeId('tile'), + metric_name: t.metric_name, + aggregation: coerceAggregation(t.aggregation), + title: t.title ?? '', + })), + })) + : emptyDefaults().sections, + } + } + return emptyDefaults() + }, [existingQuery.data]) + + const form = useForm({ + resolver: zodResolver(dashboardSchema), + values: defaultValues, + }) + + const sectionsArray = useFieldArray({ + control: form.control, + name: 'sections', + }) + + const createMutation = useMutation({ + ...createDashboardMutation(), + meta: { errorTitle: 'Failed to create dashboard' }, + onSuccess: () => { + toast.success('Dashboard created') + queryClient.invalidateQueries({ + predicate: (query) => + (query.queryKey[0] as Record)?._id === + 'listDashboards', + }) + navigate('..') + }, + }) + + const updateMutation = useMutation({ + ...updateDashboardMutation(), + meta: { errorTitle: 'Failed to update dashboard' }, + onSuccess: () => { + toast.success('Dashboard updated') + queryClient.invalidateQueries({ + predicate: (query) => { + const key = (query.queryKey[0] as Record)?._id + return key === 'listDashboards' || key === 'getDashboard' + }, + }) + navigate(-1) + }, + }) + + const isMutating = createMutation.isPending || updateMutation.isPending + + const onSubmit = async (data: DashboardFormData) => { + const layout = { + sections: data.sections.map((s) => ({ + id: s.id, + title: s.title, + tiles: s.tiles.map((t) => ({ + id: t.id, + metric_name: t.metric_name, + aggregation: t.aggregation, + title: t.title?.trim() ? t.title.trim() : null, + })), + })), + } + + if (isEditing) { + await updateMutation.mutateAsync({ + path: { id }, + query: { project_id: project.id }, + body: { name: data.name, layout }, + }) + } else { + await createMutation.mutateAsync({ + body: { project_id: project.id, name: data.name, layout }, + }) + } + } + + if (isEditing && existingQuery.isPending) { + return ( +
+
+ + +
+ +
+ ) + } + + return ( +
+
+ +
+

+ {isEditing ? 'Edit dashboard' : 'New dashboard'} +

+

+ {isEditing + ? 'Update sections and metric tiles.' + : 'Group OpenTelemetry metrics into sections of charts.'} +

+
+
+ +
+ + + + Dashboard + + + ( + + Name + + + + + + )} + /> + + + +
+ {sectionsArray.fields.map((sectionField, sectionIndex) => ( + sectionsArray.remove(sectionIndex)} + canRemoveSection={sectionsArray.fields.length > 1} + /> + ))} +
+ + + +
+ + +
+
+ +
+ ) +} + +function SectionEditor({ + control, + sectionIndex, + metricOptions, + metricsLoading, + onRemoveSection, + canRemoveSection, +}: { + control: Control + sectionIndex: number + metricOptions: { value: string; label: string }[] + metricsLoading: boolean + onRemoveSection: () => void + canRemoveSection: boolean +}) { + const tilesArray = useFieldArray({ + control, + name: `sections.${sectionIndex}.tiles`, + }) + + return ( + + +
+ + ( + + + + + + + )} + /> +
+ +
+ + {tilesArray.fields.length === 0 ? ( +

+ No tiles yet. Add a metric tile to this section. +

+ ) : ( + tilesArray.fields.map((tileField, tileIndex) => ( +
+ ( + + Metric + + + + + + )} + /> + ( + + Aggregation + + + + )} + /> + ( + + Title (optional) + + + + + + )} + /> + +
+ )) + )} + +
+
+ ) +} diff --git a/web/src/pages/DashboardView.tsx b/web/src/pages/DashboardView.tsx new file mode 100644 index 000000000..661f54e83 --- /dev/null +++ b/web/src/pages/DashboardView.tsx @@ -0,0 +1,190 @@ +import { ProjectResponse } from '@/api/client' +// REGEN: bun run openapi-ts — getDashboardOptions is generated from the new +// GET /otel/dashboards/{id} endpoint (operationId get_dashboard). Not present +// in the committed SDK; imported so the page is regen-ready (project rule: +// always use the generated SDK, never hand-roll fetch). +import { getDashboardOptions } from '@/api/client/@tanstack/react-query.gen' +import { MetricTile } from '@/components/metrics/MetricTile' +import { + RANGE_BUCKET, + TIME_RANGES, + type TimeRange, + timeRangeToFrom, +} from '@/components/metrics/time-range' +import { Button } from '@/components/ui/button' +import { EmptyState } from '@/components/ui/empty-state' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Skeleton } from '@/components/ui/skeleton' +import { usePageTitle } from '@/hooks/usePageTitle' +import { useQuery } from '@tanstack/react-query' +import { ArrowLeft, LayoutDashboard, Pencil } from 'lucide-react' +import { useMemo, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' + +interface DashboardViewProps { + project: ProjectResponse +} + +export default function DashboardView({ project }: DashboardViewProps) { + const navigate = useNavigate() + const { dashboardId } = useParams() + const id = Number(dashboardId) + const [timeRange, setTimeRange] = useState('24h') + + // Memoize both time bounds so the per-tile query keys stay STABLE across + // renders — an inline `new Date()` would change every render and spin React + // Query into an infinite refetch loop (the bug fixed in MetricsExplorer). + const fromIso = useMemo( + () => timeRangeToFrom(timeRange, Date.now()).toISOString(), + [timeRange], + ) + const toIso = useMemo( + () => new Date().toISOString(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [timeRange], + ) + const bucketInterval = RANGE_BUCKET[timeRange] + + const dashboardQuery = useQuery({ + ...getDashboardOptions({ path: { id }, query: { project_id: project.id } }), + enabled: Number.isFinite(id), + }) + + const dashboard = dashboardQuery.data + usePageTitle( + dashboard ? `${dashboard.name} · Dashboards` : 'Dashboard', + ) + + const sections = dashboard?.layout?.sections ?? [] + + return ( +
+ {/* Header */} +
+ +
+
+ +

+ {dashboardQuery.isPending ? ( + + ) : ( + (dashboard?.name ?? 'Dashboard') + )} +

+
+
+ + +
+
+
+ + {dashboardQuery.isPending ? ( +
+ {[0, 1, 2, 3, 4, 5].map((i) => ( + + ))} +
+ ) : dashboardQuery.isError ? ( +
+ navigate('..')}> + Back to dashboards + + } + /> +
+ ) : sections.length === 0 ? ( +
+ navigate('edit')} + className="gap-1.5" + > + + Edit dashboard + + } + /> +
+ ) : ( +
+ {sections.map((section) => ( +
+

+ {section.title || 'Untitled section'} +

+ {(section.tiles?.length ?? 0) === 0 ? ( +

+ No tiles in this section. +

+ ) : ( +
+ {section.tiles.map((tile) => ( + + ))} +
+ )} +
+ ))} +
+ )} +
+ ) +} diff --git a/web/src/pages/Dashboards.tsx b/web/src/pages/Dashboards.tsx new file mode 100644 index 000000000..2c7253b3c --- /dev/null +++ b/web/src/pages/Dashboards.tsx @@ -0,0 +1,249 @@ +import { ProjectResponse } from '@/api/client' +// REGEN: bun run openapi-ts — these option/mutation builders are generated from +// the new /otel/dashboards endpoints (operationIds list_dashboards / +// delete_dashboard). They do not exist in the committed SDK yet; import them so +// the page is regen-ready rather than hand-rolling fetch (project rule: always +// use the generated SDK). +import { + listDashboardsOptions, + deleteDashboardMutation, +} from '@/api/client/@tanstack/react-query.gen' +// REGEN: OtelDashboardResponse comes from the regenerated types.gen. +import type { OtelDashboardResponse } from '@/api/client' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { EmptyState } from '@/components/ui/empty-state' +import { Skeleton } from '@/components/ui/skeleton' +import { usePageTitle } from '@/hooks/usePageTitle' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + ChevronRight, + LayoutDashboard, + MoreVertical, + Pencil, + Plus, + Trash2, +} from 'lucide-react' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { toast } from 'sonner' + +interface DashboardsProps { + project: ProjectResponse +} + +/** Count sections + tiles for the compact one-line row description. */ +function dashboardSummary(d: OtelDashboardResponse): string { + const sections = d.layout?.sections ?? [] + const sectionCount = sections.length + const tileCount = sections.reduce((n, s) => n + (s.tiles?.length ?? 0), 0) + const sectionLabel = `${sectionCount} section${sectionCount === 1 ? '' : 's'}` + const tileLabel = `${tileCount} tile${tileCount === 1 ? '' : 's'}` + return `${sectionLabel} · ${tileLabel}` +} + +export default function Dashboards({ project }: DashboardsProps) { + usePageTitle(`Dashboards · ${project.name}`) + const navigate = useNavigate() + const queryClient = useQueryClient() + const [pendingDelete, setPendingDelete] = + useState(null) + + const dashboardsQuery = useQuery({ + ...listDashboardsOptions({ query: { project_id: project.id } }), + enabled: !!project.id, + }) + + const deleteMutation = useMutation({ + ...deleteDashboardMutation(), + meta: { errorTitle: 'Failed to delete dashboard' }, + onSuccess: () => { + toast.success('Dashboard deleted') + queryClient.invalidateQueries({ + predicate: (query) => + (query.queryKey[0] as Record)?._id === + 'listDashboards', + }) + setPendingDelete(null) + }, + }) + + const dashboards = dashboardsQuery.data?.data ?? [] + + const goToNew = () => navigate('new') + + return ( +
+ {/* Header */} +
+
+
+ +

Dashboards

+
+

+ Saved metric dashboards for {project.name}. +

+
+ +
+ + {dashboardsQuery.isPending ? ( +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ ) : dashboardsQuery.isError ? ( +
+ +
+ ) : dashboards.length === 0 ? ( +
+ + + New dashboard + + } + /> +
+ ) : ( +
+ {dashboards.map((d) => ( + navigate(String(d.id))} + onEdit={() => navigate(`${d.id}/edit`)} + onDelete={() => setPendingDelete(d)} + /> + ))} +
+ )} + + { + if (!open) setPendingDelete(null) + }} + > + + + Delete dashboard? + + This permanently deletes + {pendingDelete ? ` “${pendingDelete.name}”` : ' this dashboard'}. + This action cannot be undone. + + + + + Cancel + + { + e.preventDefault() + if (pendingDelete) { + deleteMutation.mutate({ + path: { id: pendingDelete.id }, + query: { project_id: project.id }, + }) + } + }} + > + {deleteMutation.isPending ? 'Deleting…' : 'Delete'} + + + + +
+ ) +} + +function DashboardRow({ + dashboard, + onOpen, + onEdit, + onDelete, +}: { + dashboard: OtelDashboardResponse + onOpen: () => void + onEdit: () => void + onDelete: () => void +}) { + return ( +
+ + + + + + + + + + Edit + + + + Delete + + + + + +
+ ) +} diff --git a/web/src/pages/DashboardsRouter.tsx b/web/src/pages/DashboardsRouter.tsx new file mode 100644 index 000000000..8e16d69ee --- /dev/null +++ b/web/src/pages/DashboardsRouter.tsx @@ -0,0 +1,28 @@ +import { Routes, Route } from 'react-router-dom' +import { ProjectResponse } from '@/api/client' +import Dashboards from './Dashboards' +import DashboardView from './DashboardView' +import DashboardBuilder from './DashboardBuilder' + +interface DashboardsRouterProps { + project: ProjectResponse +} + +/** + * Metric-dashboards section router. Mirrors `Metrics.tsx`: an index list, a + * create/edit builder, and a per-dashboard view. Mounted at `dashboards/*` + * under the project (see ProjectDetail routes). + */ +export default function DashboardsRouter({ project }: DashboardsRouterProps) { + return ( + + } /> + } /> + } /> + } + /> + + ) +} diff --git a/web/src/pages/ProjectDetail.tsx b/web/src/pages/ProjectDetail.tsx index 2c0a284af..d0a7a123d 100644 --- a/web/src/pages/ProjectDetail.tsx +++ b/web/src/pages/ProjectDetail.tsx @@ -44,6 +44,7 @@ import RequestLogs from './RequestLogs' import ProjectAiCrawlers from './ProjectAiCrawlers' import Traces from './Traces' import Metrics from './Metrics' +import DashboardsRouter from './DashboardsRouter' import { ProjectAgentActivity } from './AiGateway' import { AutofixerPage } from '@/components/autofixer/AutofixerPage' import { AutofixRedirect } from '@/components/autofixer/AutofixRedirect' @@ -422,6 +423,10 @@ export function ProjectDetail() { path="metrics/*" element={} /> + } + /> } From a7dd43d88d71381540d717893a52a9b41700e253 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 10:29:20 +0200 Subject: [PATCH 07/56] style(web): full-width metrics + dashboards pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the mx-auto/max-w container constraint on the metrics explorer, dashboards list, dashboard view, and builder so they use the full content width (e.g. ~1600px instead of a centered 1152px on wide screens) — more columns in the metric overview grid and more room for tile charts. --- web/src/pages/DashboardBuilder.tsx | 4 ++-- web/src/pages/DashboardView.tsx | 2 +- web/src/pages/Dashboards.tsx | 2 +- web/src/pages/MetricsExplorer.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/pages/DashboardBuilder.tsx b/web/src/pages/DashboardBuilder.tsx index 4e60dc2e1..07928796e 100644 --- a/web/src/pages/DashboardBuilder.tsx +++ b/web/src/pages/DashboardBuilder.tsx @@ -225,7 +225,7 @@ export default function DashboardBuilder({ project }: DashboardBuilderProps) { if (isEditing && existingQuery.isPending) { return ( -
+
@@ -236,7 +236,7 @@ export default function DashboardBuilder({ project }: DashboardBuilderProps) { } return ( -
+
+
+
+

+ {isEditing ? 'Edit alert' : 'New alert'} +

+ {isEditing && existingQuery.data && ( + + )} +
+

+ {isEditing + ? 'Update the signal, threshold, and notification settings.' + : 'Fire a notification when a metric crosses a threshold.'} +

+
+
+ +
+ + + + Rule + + + ( + + Name + + + + + + )} + /> + + {/* Signal: metric + aggregation */} +
+ ( + + Metric + + + + + + )} + /> + ( + + Aggregation + + + + )} + /> +
+ + {/* Condition: comparator + threshold */} +
+ ( + + Comparator + + + + )} + /> + ( + + Threshold + + + field.onChange( + e.target.value === '' + ? undefined + : e.target.valueAsNumber, + ) + } + /> + + + + )} + /> +
+ + {/* Timing: window + for-duration */} +
+ ( + + Evaluation window (seconds) + + + field.onChange( + e.target.value === '' + ? undefined + : e.target.valueAsNumber, + ) + } + /> + + + The metric is aggregated over this trailing window each + evaluation (e.g. 300 = last 5 minutes). + + + + )} + /> + ( + + For duration (seconds) + + + field.onChange( + e.target.value === '' + ? undefined + : e.target.valueAsNumber, + ) + } + /> + + + The breach must persist this long before the alert fires, + to avoid flapping. + + + + )} + /> +
+ + {/* Severity */} + ( + + Severity + + + Maps to the notification severity. Alerts are delivered + through this project's configured notification channels. + + + + )} + /> + + {/* Enabled toggle */} + ( + +
+ Enabled + + Only enabled rules are evaluated by the background + evaluator. + +
+ + + +
+ )} + /> +
+
+ +
+ + +
+
+ +
+ ) +} diff --git a/web/src/pages/MetricAlerts.tsx b/web/src/pages/MetricAlerts.tsx new file mode 100644 index 000000000..4c54fbcdb --- /dev/null +++ b/web/src/pages/MetricAlerts.tsx @@ -0,0 +1,245 @@ +import { ProjectResponse } from '@/api/client' +// REGEN: bun run openapi-ts — these option/mutation builders are generated from +// the new /otel/alerts endpoints (operationIds list_alerts / delete_alert). +// They do not exist in the committed SDK yet; import them so the page is +// regen-ready rather than hand-rolling fetch (project rule: always use the +// generated SDK). +import { + listAlertsOptions, + deleteAlertMutation, +} from '@/api/client/@tanstack/react-query.gen' +// REGEN: OtelMetricAlertRuleResponse comes from the regenerated types.gen. +import type { OtelMetricAlertRuleResponse } from '@/api/client' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { EmptyState } from '@/components/ui/empty-state' +import { Skeleton } from '@/components/ui/skeleton' +import { usePageTitle } from '@/hooks/usePageTitle' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Bell, + ChevronRight, + MoreVertical, + Pencil, + Plus, + Trash2, +} from 'lucide-react' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { toast } from 'sonner' +import { AlertStateBadge, alertSummary } from '@/components/metrics/alert-format' + +interface MetricAlertsProps { + project: ProjectResponse +} + +export default function MetricAlerts({ project }: MetricAlertsProps) { + usePageTitle(`Alerts · ${project.name}`) + const navigate = useNavigate() + const queryClient = useQueryClient() + const [pendingDelete, setPendingDelete] = + useState(null) + + const alertsQuery = useQuery({ + ...listAlertsOptions({ query: { project_id: project.id } }), + enabled: !!project.id, + }) + + const deleteMutation = useMutation({ + ...deleteAlertMutation(), + meta: { errorTitle: 'Failed to delete alert rule' }, + onSuccess: () => { + toast.success('Alert rule deleted') + queryClient.invalidateQueries({ + predicate: (query) => + (query.queryKey[0] as Record)?._id === 'listAlerts', + }) + setPendingDelete(null) + }, + }) + + const alerts = alertsQuery.data?.data ?? [] + + const goToNew = () => navigate('new') + + return ( +
+ {/* Header */} +
+
+
+ +

Alerts

+
+

+ Threshold alerts on OpenTelemetry metrics for {project.name}. +

+
+ +
+ + {alertsQuery.isPending ? ( +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ ) : alertsQuery.isError ? ( +
+ +
+ ) : alerts.length === 0 ? ( +
+ + + New alert + + } + /> +
+ ) : ( +
+ {alerts.map((rule) => ( + navigate(`${rule.id}/edit`)} + onDelete={() => setPendingDelete(rule)} + /> + ))} +
+ )} + + { + if (!open) setPendingDelete(null) + }} + > + + + Delete alert rule? + + This permanently deletes + {pendingDelete ? ` “${pendingDelete.name}”` : ' this alert rule'}. + This action cannot be undone. + + + + + Cancel + + { + e.preventDefault() + if (pendingDelete) { + deleteMutation.mutate({ + path: { id: pendingDelete.id }, + query: { project_id: project.id }, + }) + } + }} + > + {deleteMutation.isPending ? 'Deleting…' : 'Delete'} + + + + +
+ ) +} + +function AlertRow({ + rule, + onEdit, + onDelete, +}: { + rule: OtelMetricAlertRuleResponse + onEdit: () => void + onDelete: () => void +}) { + return ( +
+ + + + + + + + + + + + Edit + + + + Delete + + + + + +
+ ) +} diff --git a/web/src/pages/Metrics.tsx b/web/src/pages/Metrics.tsx index 56315fdb8..fa02664c5 100644 --- a/web/src/pages/Metrics.tsx +++ b/web/src/pages/Metrics.tsx @@ -1,8 +1,9 @@ import { Routes, Route, Link, useLocation } from 'react-router-dom' import { ProjectResponse } from '@/api/client' -import { LayoutDashboard, LineChart } from 'lucide-react' +import { Bell, LayoutDashboard, LineChart } from 'lucide-react' import MetricsExplorer from './MetricsExplorer' import DashboardsRouter from './DashboardsRouter' +import AlertsRouter from './AlertsRouter' interface MetricsProps { project: ProjectResponse @@ -27,6 +28,7 @@ export default function Metrics({ project }: MetricsProps) { path="dashboards/*" element={} /> + } />
) @@ -40,15 +42,27 @@ function MetricsTabs() { const i = pathname.indexOf('/metrics') const base = i === -1 ? pathname : pathname.slice(0, i + '/metrics'.length) const onDashboards = pathname.startsWith(`${base}/dashboards`) + const onAlerts = pathname.startsWith(`${base}/alerts`) const tabs = [ - { to: base, label: 'Explore', icon: LineChart, active: !onDashboards }, + { + to: base, + label: 'Explore', + icon: LineChart, + active: !onDashboards && !onAlerts, + }, { to: `${base}/dashboards`, label: 'Dashboards', icon: LayoutDashboard, active: onDashboards, }, + { + to: `${base}/alerts`, + label: 'Alerts', + icon: Bell, + active: onAlerts, + }, ] return ( diff --git a/web/src/pages/MetricsExplorer.tsx b/web/src/pages/MetricsExplorer.tsx index ab948aaca..a54960257 100644 --- a/web/src/pages/MetricsExplorer.tsx +++ b/web/src/pages/MetricsExplorer.tsx @@ -3,7 +3,12 @@ import { getEnvironmentsOptions, listMetricNamesOptions, queryMetricsOptions, + // REGEN: bun run openapi-ts — generated from the new /otel/alerts endpoint + // (operationId list_alerts). Absent from the committed SDK until regen; used + // only to overlay a rule's threshold as a reference line on the explore chart. + listAlertsOptions, } from '@/api/client/@tanstack/react-query.gen' +import type { ThresholdBand } from '@/components/charts/threshold-line-chart' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -17,6 +22,7 @@ import { import { Skeleton } from '@/components/ui/skeleton' import { EmptyState } from '@/components/ui/empty-state' import { ThresholdLineChart } from '@/components/charts/threshold-line-chart' +import { comparatorSymbol } from '@/components/metrics/alert-format' import { usePageTitle } from '@/hooks/usePageTitle' import { useQuery } from '@tanstack/react-query' import { format } from 'date-fns' @@ -269,6 +275,24 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { enabled: !!project.id && metricName.length > 0, }) + // ── Alert thresholds for the charted metric ── + // Overlay any alert rule defined on the selected metric as a horizontal + // reference line, so the explorer visually shows where a rule would fire. + const alertsQuery = useQuery({ + ...listAlertsOptions({ query: { project_id: project.id } }), + enabled: !!project.id && metricName.length > 0, + }) + const thresholds = useMemo(() => { + if (!metricName) return [] + return (alertsQuery.data?.data ?? []) + .filter((r) => r.enabled && r.metric_name === metricName) + .map((r) => ({ + value: r.threshold, + tone: r.severity === 'critical' ? 'poor' : 'warn', + label: `${comparatorSymbol(r.comparator)} ${r.threshold}`, + })) + }, [alertsQuery.data, metricName]) + const buckets = metricsQuery.data?.data ?? [] // Map the `MetricBucket` rows into the chart point shape. `value` carries the @@ -504,6 +528,7 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { : 'Failed to load metric series.' } chartData={chartData} + thresholds={thresholds} />
{latestHist && ( @@ -791,6 +816,7 @@ function MetricChart({ isError, errorMessage, chartData, + thresholds, }: { metricName: string aggLabel: string @@ -798,6 +824,7 @@ function MetricChart({ isError: boolean errorMessage: string chartData: ChartPoint[] + thresholds?: ThresholdBand[] }) { if (!metricName) { return ( @@ -853,6 +880,7 @@ function MetricChart({ data={xData} xKey="label" series={{ dataKey: 'value', label: aggLabel, tone: 'primary' }} + thresholds={thresholds} height={320} tooltipValueFormatter={(v) => formatMetricValue(v)} yTickFormatter={(v) => formatMetricValue(v)} From 1a236ac80ffe617e477fc3095f71c87390d3ce74 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 14:04:57 +0200 Subject: [PATCH 10/56] refactor(otel): forward-compatible detector schema for metric alert rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reshape metric_alert_rules so future detector families (anomaly, EWMA, forecast, outlier, Watchdog-style auto-watch) ship as code-only changes — never another migration. Done now, before the table merges, so there is no backfill or transition dance. Schema: replace the static-only `comparator`/`threshold` columns with a coarse `detection_kind` string discriminator + a typed-in-Rust, jsonb `detection_config`. The cross-cutting eval envelope (metric, aggregation, window, for-duration, severity, enabled, last_* state) stays typed columns; only the detector-specific knobs move into the blob. Folded directly into the in-flight create migration (zero rows, no ALTER). detection_config is a serde internally-tagged enum (`DetectionConfig`) in the new `temps_otel::detectors` module — copied verbatim from the sanctioned `ProviderConfig`/`revenue_integrations.config` precedent: NO `#[schema(discriminator)]` (a compile error with serde(tag) in utoipa 5.5.0). The raw `serde_json::Value` lives only on the sea-orm column; every service and DTO layer is fully typed, so the generated SDK is a usable TS discriminated union — `(StaticParams & { kind: 'static' }) | …` — not `any`. Today only `static` is evaluable; anomaly/forecast/outlier/auto_watch are typed, schema-present stubs that validate() rejects at create time. Enabling each later = a new validate arm + evaluator branch + openapi-ts regen. `detection_kind` is a plain string (not a PG enum) so new kinds need no ALTER TYPE either. Evaluator now decodes the typed detector and branches on it (static = `Comparator::breaches`); the bad-input surface moves to serde (unknown kind / bad comparator / missing threshold -> 422 at deserialize) which is stronger than the old string allowlists. Frontend maps the static form to/from detection_config and the explorer/list narrow on `kind`. Verified live (ClickHouse-backed): create/get round-trip the typed config through jsonb; anomaly -> 400 (not yet supported); bad input -> 422; the static evaluator still fires. 25 unit tests pass; clippy -D warnings clean; SDK regenerated; frontend typechecks. --- .../temps-entities/src/metric_alert_rules.rs | 31 +- ...260626_000002_create_metric_alert_rules.rs | 25 +- crates/temps-otel/src/detectors/mod.rs | 363 ++++++++++++++++++ .../src/handlers/metric_alert_handler.rs | 41 +- crates/temps-otel/src/lib.rs | 1 + crates/temps-otel/src/plugin.rs | 12 + .../src/services/metric_alert_evaluator.rs | 73 ++-- .../src/services/metric_alert_service.rs | 104 +++-- web/src/api/client/types.gen.ts | 160 +++++++- web/src/components/metrics/alert-format.tsx | 19 +- web/src/pages/MetricAlertForm.tsx | 28 +- web/src/pages/MetricsExplorer.tsx | 17 +- 12 files changed, 724 insertions(+), 150 deletions(-) create mode 100644 crates/temps-otel/src/detectors/mod.rs diff --git a/crates/temps-entities/src/metric_alert_rules.rs b/crates/temps-entities/src/metric_alert_rules.rs index c893b13a6..03178ceae 100644 --- a/crates/temps-entities/src/metric_alert_rules.rs +++ b/crates/temps-entities/src/metric_alert_rules.rs @@ -6,13 +6,20 @@ use temps_core::DBDateTime; /// A first-class metric alert rule for a project. /// -/// A rule is defined on a *signal* (project + metric + aggregation + threshold), -/// independent of any dashboard. The background `MetricAlertEvaluator` evaluates -/// every enabled rule on an interval, compares the latest aggregated bucket value -/// against `threshold` via `comparator`, and fires/resolves a notification (via +/// A rule is defined on a *signal* (project + metric + aggregation) plus a +/// polymorphic, versionable detector (`detection_config`, jsonb) — independent of +/// any dashboard. The background `MetricAlertEvaluator` evaluates every enabled +/// rule on an interval, reduces the latest window to a scalar via `aggregation`, +/// asks the detector whether it breaches, and fires/resolves a notification (via /// the reused `temps-monitoring` alarm system) once a breach has persisted for at /// least `for_duration_secs`. This table holds config/metadata only — it is /// Postgres-backed, never ClickHouse. +/// +/// The detector is stored as a typed-in-Rust enum +/// (`temps_otel::detectors::DetectionConfig`) serialized to jsonb on this column +/// only; the service/DTO layers are fully typed. Adding a new detector family is +/// code-only (new enum variant + evaluator branch) — never a migration — +/// because `detection_kind` is a plain string and the params live in the blob. #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] #[sea_orm(table_name = "metric_alert_rules")] pub struct Model { @@ -25,12 +32,18 @@ pub struct Model { /// The metric name the rule evaluates (e.g. `http.server.duration`). pub metric_name: String, /// Aggregation applied over the window: one of - /// `avg|sum|min|max|count|rate|p50|p90|p95|p99`. + /// `avg|sum|min|max|count|rate|p50|p90|p95|p99`. Cross-cutting: every + /// detector family reduces the metric to a scalar through this first. pub aggregation: String, - /// Comparator applied to `value` vs `threshold`: one of `gt|gte|lt|lte`. - pub comparator: String, - /// The threshold the aggregated value is compared against. - pub threshold: f64, + /// Coarse detector discriminator mirroring `detection_config`'s `kind` tag: + /// one of `static|anomaly|forecast|outlier|auto_watch`. Kept as a column (not + /// in the blob) so the evaluator/UI can filter by kind without a JSON probe. + pub detection_kind: String, + /// Typed detector definition (`temps_otel::detectors::DetectionConfig`), + /// stored as jsonb. Raw `Value` on the Model only — the service/DTO layers + /// are fully typed. For a static rule this is `{kind:static,comparator,threshold}`. + #[sea_orm(column_type = "JsonBinary")] + pub detection_config: serde_json::Value, /// Aggregation/eval window in seconds (e.g. 300). pub window_secs: i32, /// How long (seconds) a breach must persist before the rule fires. diff --git a/crates/temps-migrations/src/migration/m20260626_000002_create_metric_alert_rules.rs b/crates/temps-migrations/src/migration/m20260626_000002_create_metric_alert_rules.rs index e0573a30e..77f60c286 100644 --- a/crates/temps-migrations/src/migration/m20260626_000002_create_metric_alert_rules.rs +++ b/crates/temps-migrations/src/migration/m20260626_000002_create_metric_alert_rules.rs @@ -1,8 +1,9 @@ //! Migration to create the `metric_alert_rules` table. //! //! Backs the first-class, metric-centric alert rules feature. Each row defines a -//! signal (project + metric + aggregation + threshold) the background evaluator -//! checks on an interval. This is config/metadata — Postgres, never ClickHouse. +//! signal (project + metric + aggregation) plus a polymorphic, versionable +//! detector (`detection_config`, jsonb) the background evaluator checks on an +//! interval. This is config/metadata — Postgres, never ClickHouse. //! //! Two indexes serve the access patterns: `(project_id)` for the per-project list //! view, and `(project_id, enabled)` for the evaluator's enabled-rules scan. @@ -43,14 +44,22 @@ impl MigrationTrait for Migration { .text() .not_null(), ) + // Coarse detector discriminator (static|anomaly|forecast| + // outlier|auto_watch). A plain string, so new kinds need no + // ALTER TYPE. .col( - ColumnDef::new(MetricAlertRules::Comparator) + ColumnDef::new(MetricAlertRules::DetectionKind) .text() - .not_null(), + .not_null() + .default("static"), ) + // Typed-in-Rust detector definition stored as jsonb. The + // comparator/threshold of a static rule live here (kind=static); + // anomaly/forecast/outlier params land here too — a new detector + // family is code-only, never a migration. .col( - ColumnDef::new(MetricAlertRules::Threshold) - .double() + ColumnDef::new(MetricAlertRules::DetectionConfig) + .json_binary() .not_null(), ) .col( @@ -128,8 +137,8 @@ enum MetricAlertRules { Name, MetricName, Aggregation, - Comparator, - Threshold, + DetectionKind, + DetectionConfig, WindowSecs, ForDurationSecs, Severity, diff --git a/crates/temps-otel/src/detectors/mod.rs b/crates/temps-otel/src/detectors/mod.rs new file mode 100644 index 000000000..1758f8c55 --- /dev/null +++ b/crates/temps-otel/src/detectors/mod.rs @@ -0,0 +1,363 @@ +//! Typed, polymorphic detector definitions for metric alert rules. +//! +//! A rule's detector lives in `metric_alert_rules.detection_config` (jsonb). The +//! raw `serde_json::Value` exists ONLY on the sea-orm entity column; every +//! service and DTO layer uses the typed [`DetectionConfig`] here — mirroring the +//! sanctioned `temps_revenue::providers::ProviderConfig` + +//! `revenue_integrations.config` precedent. This satisfies the "never expose +//! untyped `serde_json::Value` on the API surface" rule: the API surface is +//! 100% typed; only the storage column is a `Value`. +//! +//! Forward-compatibility is the whole point: a new detector family +//! (anomaly→EWMA→forecast→outlier→auto-watch) is a **new enum variant + a +//! `validate` arm + an evaluator branch + `openapi-ts` regen** — never a +//! migration. `detection_kind` is a plain `String` column (not a Postgres enum), +//! so a new kind also needs no `ALTER TYPE`. +//! +//! Tagging follows the `ProviderConfig` precedent *verbatim*: internally tagged +//! by `kind`, with NO `#[schema(discriminator)]` (which is a compile error +//! alongside `serde(tag)` in utoipa 5.x). utoipa + hey-api render this as a +//! usable `(StaticParams & { kind: 'static' }) | …` TS discriminated union. + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::error::OtelError; + +fn default_deviations() -> f64 { + 3.0 +} +fn default_pct_anomalous() -> f64 { + 1.0 +} + +/// The typed detector definition stored (as jsonb) in +/// `metric_alert_rules.detection_config`. +/// +/// Today only [`DetectionConfig::Static`] is evaluable; the other variants are +/// schema-present (so the SDK/UI and storage are already future-shaped) but +/// rejected by [`DetectionConfig::validate`] until their evaluator lands. Each is +/// then enabled code-only, with no schema migration. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum DetectionConfig { + /// v0 (shipping): static threshold comparison of the aggregated value. + Static(StaticParams), + /// Seasonal anomaly band (basic/agile/robust/ewma share this variant — the + /// algorithm is a field, not a new kind). Creation rejected until evaluated. + Anomaly(AnomalyParams), + /// Predict a future threshold breach (capacity planning). Stub. + Forecast(ForecastParams), + /// Cross-series population outlier (one host misbehaving vs its peers). Stub. + Outlier(OutlierParams), + /// Watchdog-style self-tuning auto-watch (engine picks bounds). Stub. + AutoWatch(AutoWatchParams), +} + +/// Static threshold detector: compare the aggregated `value` against `threshold`. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] +pub struct StaticParams { + /// How `value` is compared against `threshold`. + pub comparator: Comparator, + /// The threshold the aggregated value is compared against. + pub threshold: f64, +} + +/// Seasonal anomaly-band detector parameters (stub — not yet evaluated). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] +pub struct AnomalyParams { + /// Baseline model. `robust` is the default (seasonal, stable, flags level + /// shifts); `ewma`/`agile` adopt level shifts; `basic` is non-seasonal. + #[serde(default)] + pub algorithm: AnomalyAlgorithm, + /// Band width in robust standard deviations (Datadog's `bounds`). + #[serde(default = "default_deviations")] + pub deviations: f64, + /// Which side(s) of the band a deviation must be on to count. + #[serde(default)] + pub direction: Direction, + /// Seasonality model for the baseline. + #[serde(default)] + pub seasonality: Seasonality, + /// Fraction (0..=1) of points in the window that must be anomalous to fire. + #[serde(default = "default_pct_anomalous")] + pub pct_anomalous: f64, + /// How far back to build the baseline. `None` = an evaluator default. + #[serde(default)] + pub baseline_lookback_days: Option, +} + +/// Forecast detector parameters (stub — not yet evaluated). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] +pub struct ForecastParams { + #[serde(default)] + pub algorithm: ForecastAlgorithm, + /// How far ahead to project before checking the breach condition. + pub forecast_horizon_secs: i32, + #[serde(default = "default_deviations")] + pub deviations: f64, + /// Comparator + threshold the *forecast* is checked against. + pub comparator: Comparator, + pub threshold: f64, +} + +/// Outlier (cross-series population) detector parameters (stub — not evaluated). +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] +pub struct OutlierParams { + #[serde(default)] + pub algorithm: OutlierAlgorithm, + /// Sensitivity; higher tolerates larger spread before flagging. + #[serde(default = "default_deviations")] + pub tolerance: f64, + /// Label key defining the peer population compared across series (e.g. `host`). + pub peer_group_key: String, +} + +/// Auto-watch (Watchdog-style) detector parameters (stub — not evaluated). +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, ToSchema)] +pub struct AutoWatchParams { + /// The engine self-tunes the band; the user supplies only the direction. + #[serde(default)] + pub direction: Direction, +} + +/// Comparator for static/forecast threshold detectors. Serializes to the +/// keyword forms `gt|gte|lt|lte` (NOT the SQL operators used by +/// `temps-monitoring::compare`). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Comparator { + #[default] + Gt, + Gte, + Lt, + Lte, +} + +impl Comparator { + /// Whether `value` breaches `threshold` under this comparator. + pub fn breaches(self, value: f64, threshold: f64) -> bool { + match self { + Comparator::Gt => value > threshold, + Comparator::Gte => value >= threshold, + Comparator::Lt => value < threshold, + Comparator::Lte => value <= threshold, + } + } + + /// A human-readable operator symbol for alarm messages. + pub fn symbol(self) -> &'static str { + match self { + Comparator::Gt => ">", + Comparator::Gte => ">=", + Comparator::Lt => "<", + Comparator::Lte => "<=", + } + } +} + +/// Which side(s) of an anomaly band count as a deviation. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Direction { + #[default] + Both, + Above, + Below, +} + +/// Seasonality model for an anomaly baseline. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Seasonality { + #[default] + None, + Hourly, + Daily, + Weekly, +} + +/// Anomaly baseline algorithm. Adding one (e.g. a new robust variant) is a +/// code-only enum addition — no migration, since it lives inside the blob. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum AnomalyAlgorithm { + #[default] + Robust, + Basic, + Agile, + Ewma, +} + +/// Forecast model family. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ForecastAlgorithm { + #[default] + Linear, + Seasonal, +} + +/// Outlier detection algorithm. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum OutlierAlgorithm { + #[default] + Dbscan, + ScaledDbscan, + Mad, + ScaledMad, +} + +impl DetectionConfig { + /// The `detection_kind` column value mirroring this variant's serde tag. + pub fn kind_str(&self) -> &'static str { + match self { + DetectionConfig::Static(_) => "static", + DetectionConfig::Anomaly(_) => "anomaly", + DetectionConfig::Forecast(_) => "forecast", + DetectionConfig::Outlier(_) => "outlier", + DetectionConfig::AutoWatch(_) => "auto_watch", + } + } + + /// An infallible fallback used when a *stored* blob fails to deserialize. + /// Only reachable on DB corruption — every write is validated and round- + /// tripped through this type, so a persisted blob is always well-formed. + pub fn default_static() -> Self { + DetectionConfig::Static(StaticParams { + comparator: Comparator::Gt, + threshold: 0.0, + }) + } + + /// Deserialize from the stored jsonb value (typed error on malformed input). + pub fn from_value(value: &serde_json::Value) -> Result { + serde_json::from_value(value.clone()).map_err(|e| OtelError::Validation { + message: format!("invalid detection_config: {e}"), + }) + } + + /// Serialize to a jsonb value for persistence. + pub fn to_value(&self) -> Result { + serde_json::to_value(self).map_err(|e| OtelError::Validation { + message: format!("failed to serialize detection_config: {e}"), + }) + } + + /// Validate the detector's invariants. Currently only `static` rules are + /// evaluable; the other (typed, schema-present) kinds are rejected at + /// create/update time until their evaluator lands — enabling each later is + /// code-only, never a migration. + pub fn validate(&self) -> Result<(), OtelError> { + match self { + DetectionConfig::Static(p) => { + if !p.threshold.is_finite() { + return Err(OtelError::Validation { + message: "threshold must be a finite number".to_string(), + }); + } + Ok(()) + } + other => Err(OtelError::Validation { + message: format!("detector kind '{}' is not yet supported", other.kind_str()), + }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_static_round_trip() { + let cfg = DetectionConfig::Static(StaticParams { + comparator: Comparator::Gte, + threshold: 500.0, + }); + let v = cfg.to_value().unwrap(); + // Internally tagged: the discriminator + the variant's fields are flat. + assert_eq!(v["kind"], "static"); + assert_eq!(v["comparator"], "gte"); + assert_eq!(v["threshold"], 500.0); + let back = DetectionConfig::from_value(&v).unwrap(); + assert_eq!(back, cfg); + assert_eq!(back.kind_str(), "static"); + } + + #[test] + fn test_anomaly_round_trip_with_defaults() { + // A minimal anomaly blob (only the required-by-shape fields) fills the + // rest from serde defaults — proving additive fields are forward-compat. + let v = serde_json::json!({ "kind": "anomaly" }); + let cfg = DetectionConfig::from_value(&v).unwrap(); + match &cfg { + DetectionConfig::Anomaly(p) => { + assert_eq!(p.algorithm, AnomalyAlgorithm::Robust); + assert_eq!(p.deviations, 3.0); + assert_eq!(p.direction, Direction::Both); + assert_eq!(p.seasonality, Seasonality::None); + assert_eq!(p.pct_anomalous, 1.0); + assert_eq!(p.baseline_lookback_days, None); + } + _ => panic!("expected anomaly"), + } + assert_eq!(cfg.kind_str(), "anomaly"); + } + + #[test] + fn test_validate_static_ok() { + let cfg = DetectionConfig::Static(StaticParams { + comparator: Comparator::Lt, + threshold: 1.0, + }); + assert!(cfg.validate().is_ok()); + } + + #[test] + fn test_validate_static_rejects_non_finite_threshold() { + let cfg = DetectionConfig::Static(StaticParams { + comparator: Comparator::Gt, + threshold: f64::INFINITY, + }); + assert!(matches!(cfg.validate(), Err(OtelError::Validation { .. }))); + } + + #[test] + fn test_validate_rejects_unsupported_kinds() { + // The non-static kinds are typed (schema-present) but not yet evaluable. + let anomaly = + DetectionConfig::from_value(&serde_json::json!({ "kind": "anomaly" })).unwrap(); + assert!(matches!( + anomaly.validate(), + Err(OtelError::Validation { .. }) + )); + let auto = DetectionConfig::AutoWatch(AutoWatchParams::default()); + assert!(matches!(auto.validate(), Err(OtelError::Validation { .. }))); + } + + #[test] + fn test_comparator_breaches() { + assert!(Comparator::Gt.breaches(600.0, 500.0)); + assert!(!Comparator::Gt.breaches(500.0, 500.0)); + assert!(Comparator::Gte.breaches(500.0, 500.0)); + assert!(Comparator::Lt.breaches(400.0, 500.0)); + assert!(!Comparator::Lt.breaches(500.0, 500.0)); + assert!(Comparator::Lte.breaches(500.0, 500.0)); + } + + #[test] + fn test_unknown_kind_hard_fails() { + // An unknown top-level kind must NOT silently degrade — it fails to parse. + let v = serde_json::json!({ "kind": "telepathy", "threshold": 1.0 }); + assert!(DetectionConfig::from_value(&v).is_err()); + } + + #[test] + fn test_default_static_fallback() { + let cfg = DetectionConfig::default_static(); + assert_eq!(cfg.kind_str(), "static"); + assert!(cfg.validate().is_ok()); + } +} diff --git a/crates/temps-otel/src/handlers/metric_alert_handler.rs b/crates/temps-otel/src/handlers/metric_alert_handler.rs index 81038926f..06ab8fdcd 100644 --- a/crates/temps-otel/src/handlers/metric_alert_handler.rs +++ b/crates/temps-otel/src/handlers/metric_alert_handler.rs @@ -10,9 +10,10 @@ use axum::http::StatusCode; use axum::response::IntoResponse; use axum::{Extension, Json}; use serde::{Deserialize, Serialize}; -use tracing::error; +use tracing::{error, warn}; use utoipa::ToSchema; +use crate::detectors::DetectionConfig; use crate::handlers::audit::{ OtelMetricAlertCreatedAudit, OtelMetricAlertDeletedAudit, OtelMetricAlertUpdatedAudit, }; @@ -38,9 +39,9 @@ pub struct CreateMetricAlertRequest { pub metric_name: String, /// One of `avg|sum|min|max|count|rate|p50|p90|p95|p99`. pub aggregation: String, - /// One of `gt|gte|lt|lte`. - pub comparator: String, - pub threshold: f64, + /// The detector: a discriminated union keyed by `kind`. Today only + /// `{ "kind": "static", "comparator": "gt", "threshold": 500 }` is evaluable. + pub detection_config: DetectionConfig, pub window_secs: i32, pub for_duration_secs: i32, /// One of `info|warning|critical`. @@ -53,8 +54,8 @@ pub struct UpdateMetricAlertRequest { pub name: Option, pub metric_name: Option, pub aggregation: Option, - pub comparator: Option, - pub threshold: Option, + /// Replaces the detector wholesale when present (absent = leave unchanged). + pub detection_config: Option, pub window_secs: Option, pub for_duration_secs: Option, pub severity: Option, @@ -78,8 +79,10 @@ pub struct OtelMetricAlertRuleResponse { pub name: String, pub metric_name: String, pub aggregation: String, - pub comparator: String, - pub threshold: f64, + /// Coarse detector discriminator: `static|anomaly|forecast|outlier|auto_watch`. + pub detection_kind: String, + /// The typed detector definition (discriminated union keyed by `kind`). + pub detection_config: DetectionConfig, pub window_secs: i32, pub for_duration_secs: i32, pub severity: String, @@ -97,14 +100,26 @@ pub struct OtelMetricAlertRuleResponse { impl From for OtelMetricAlertRuleResponse { fn from(model: Model) -> Self { + // The stored blob is always valid (every write round-trips through the + // typed enum); a decode failure means DB corruption — log and fall back + // to a default static config so the API stays typed and serving. + let detection_config = + DetectionConfig::from_value(&model.detection_config).unwrap_or_else(|e| { + warn!( + rule_id = model.id, + error = %e, + "metric_alert_rules.detection_config failed to decode; using default" + ); + DetectionConfig::default_static() + }); Self { id: model.id, project_id: model.project_id, name: model.name, metric_name: model.metric_name, aggregation: model.aggregation, - comparator: model.comparator, - threshold: model.threshold, + detection_kind: model.detection_kind, + detection_config, window_secs: model.window_secs, for_duration_secs: model.for_duration_secs, severity: model.severity, @@ -193,8 +208,7 @@ pub async fn create_alert( request.name, request.metric_name, request.aggregation, - request.comparator, - request.threshold, + request.detection_config, request.window_secs, request.for_duration_secs, request.severity, @@ -290,8 +304,7 @@ pub async fn update_alert( request.name, request.metric_name, request.aggregation, - request.comparator, - request.threshold, + request.detection_config, request.window_secs, request.for_duration_secs, request.severity, diff --git a/crates/temps-otel/src/lib.rs b/crates/temps-otel/src/lib.rs index 3c153b8e7..aeb30542a 100644 --- a/crates/temps-otel/src/lib.rs +++ b/crates/temps-otel/src/lib.rs @@ -29,6 +29,7 @@ //! ``` pub mod anomaly; +pub mod detectors; pub mod error; pub mod handlers; pub mod ingest; diff --git a/crates/temps-otel/src/plugin.rs b/crates/temps-otel/src/plugin.rs index ae7694683..14aad5416 100644 --- a/crates/temps-otel/src/plugin.rs +++ b/crates/temps-otel/src/plugin.rs @@ -221,6 +221,18 @@ impl OtelConfig { metric_alert_handler::UpdateMetricAlertRequest, metric_alert_handler::OtelMetricAlertRuleResponse, metric_alert_handler::OtelMetricAlertsResponse, + crate::detectors::DetectionConfig, + crate::detectors::StaticParams, + crate::detectors::AnomalyParams, + crate::detectors::ForecastParams, + crate::detectors::OutlierParams, + crate::detectors::AutoWatchParams, + crate::detectors::Comparator, + crate::detectors::Direction, + crate::detectors::Seasonality, + crate::detectors::AnomalyAlgorithm, + crate::detectors::ForecastAlgorithm, + crate::detectors::OutlierAlgorithm, ) ), info( diff --git a/crates/temps-otel/src/services/metric_alert_evaluator.rs b/crates/temps-otel/src/services/metric_alert_evaluator.rs index 5ef6e5285..3469bf9be 100644 --- a/crates/temps-otel/src/services/metric_alert_evaluator.rs +++ b/crates/temps-otel/src/services/metric_alert_evaluator.rs @@ -28,6 +28,7 @@ use tracing::{debug, error, info, warn}; use temps_monitoring::{AlarmService, AlarmSeverity, AlarmType, FireAlarmRequest}; +use crate::detectors::DetectionConfig; use crate::services::metric_alert_service::MetricAlertService; use crate::services::OtelService; use crate::types::{MetricAggregation, MetricBucket, MetricQuery}; @@ -36,22 +37,6 @@ use temps_entities::metric_alert_rules::Model as AlertRule; /// How often the evaluator scans enabled rules. const EVAL_INTERVAL_SECS: u64 = 30; -/// Whether the value breaches the threshold under the given comparator. -/// -/// The comparator strings are the keyword forms `gt/gte/lt/lte` (NOT the SQL -/// operator strings used by `temps-monitoring::compare`). Unknown comparators are -/// treated as non-breaching (fail-closed) — the service validator rejects them at -/// write time, so this only guards against corrupt rows. -fn compare(value: f64, threshold: f64, comparator: &str) -> bool { - match comparator { - "gt" => value > threshold, - "gte" => value >= threshold, - "lt" => value < threshold, - "lte" => value <= threshold, - _ => false, - } -} - /// The state transition the evaluator should apply for a single rule this cycle. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AlertTransition { @@ -244,7 +229,31 @@ impl MetricAlertEvaluator { // per-point means, not the true distribution (same reason the explorer // recomputes percentiles client-side). let value = value_for_rule(latest, aggregation); - let breaching = compare(value, rule.threshold, &rule.comparator); + + // Decode the typed detector. A corrupt blob fails this rule's cycle (the + // outer loop logs and continues) rather than evaluating it incorrectly. + let config = DetectionConfig::from_value(&rule.detection_config)?; + let breaching = match &config { + DetectionConfig::Static(p) => p.comparator.breaches(value, p.threshold), + // Non-static detectors are typed/schema-present but not yet evaluable + // (creation is rejected by the service). Defensive guard for any rule + // that predates support: preserve state and skip without firing. + other => { + debug!( + rule_id = rule.id, + kind = other.kind_str(), + "Skipping rule: detector kind not yet evaluable" + ); + if let Err(e) = self + .alert_service + .persist_evaluation(rule.id, &rule.last_state, rule.last_value, now) + .await + { + warn!(rule_id = rule.id, error = %e, "Failed to persist skipped evaluation"); + } + return Ok(()); + } + }; // Track breach duration (in-memory, approximated like temps-monitoring). let breach_elapsed_secs = if breaching { @@ -266,7 +275,7 @@ impl MetricAlertEvaluator { AlertTransition::StayOk | AlertTransition::StartBreach => "ok", AlertTransition::StayFiring => "firing", AlertTransition::FireNow => { - self.fire(&rule, value).await; + self.fire(&rule, value, &config).await; "firing" } AlertTransition::Resolve => { @@ -287,7 +296,13 @@ impl MetricAlertEvaluator { } /// Fire an alarm via the reused alarm system and remember the alarm id. - async fn fire(&self, rule: &AlertRule, value: f64) { + async fn fire(&self, rule: &AlertRule, value: f64, config: &DetectionConfig) { + // The condition string + threshold come from the typed detector. Only + // static rules fire today; the fallback keeps the match exhaustive. + let (comparator, threshold) = match config { + DetectionConfig::Static(p) => (p.comparator.symbol(), p.threshold), + _ => ("?", f64::NAN), + }; let request = FireAlarmRequest { project_id: rule.project_id, environment_id: None, @@ -299,15 +314,16 @@ impl MetricAlertEvaluator { title: format!("Metric threshold breached: {}", rule.name), message: format!( "{} {} is {:.3} (threshold {} {:.3})", - rule.metric_name, rule.aggregation, value, rule.comparator, rule.threshold + rule.metric_name, rule.aggregation, value, comparator, threshold ), metadata: Some(json!({ "rule_id": rule.id, "metric_name": rule.metric_name, "aggregation": rule.aggregation, "value": value, - "threshold": rule.threshold, - "comparator": rule.comparator, + "threshold": threshold, + "comparator": comparator, + "detection_kind": config.kind_str(), "window_secs": rule.window_secs, "for_duration_secs": rule.for_duration_secs, "source": "otel_metric_alert", @@ -362,19 +378,6 @@ impl MetricAlertEvaluator { mod tests { use super::*; - #[test] - fn test_compare_all_comparators() { - assert!(compare(600.0, 500.0, "gt")); - assert!(!compare(500.0, 500.0, "gt")); - assert!(compare(500.0, 500.0, "gte")); - assert!(compare(400.0, 500.0, "lt")); - assert!(!compare(500.0, 500.0, "lt")); - assert!(compare(500.0, 500.0, "lte")); - // SQL-operator form is not a valid keyword comparator -> fail-closed. - assert!(!compare(600.0, 500.0, ">")); - assert!(!compare(600.0, 500.0, "unknown")); - } - #[test] fn test_transition_ok_to_firing_requires_for_duration() { // Breaching but not long enough yet -> StartBreach (stays effectively ok). diff --git a/crates/temps-otel/src/services/metric_alert_service.rs b/crates/temps-otel/src/services/metric_alert_service.rs index 6a3838e5a..e2febf1ff 100644 --- a/crates/temps-otel/src/services/metric_alert_service.rs +++ b/crates/temps-otel/src/services/metric_alert_service.rs @@ -20,6 +20,7 @@ use sea_orm::{ use temps_entities::metric_alert_rules::{ActiveModel, Column, Entity, Model}; +use crate::detectors::DetectionConfig; use crate::error::OtelError; /// The allowlisted aggregations a rule may request. Mirrors the keyword/quantile @@ -28,30 +29,25 @@ pub const ALLOWED_AGGREGATIONS: &[&str] = &[ "avg", "sum", "min", "max", "count", "rate", "p50", "p90", "p95", "p99", ]; -/// The allowlisted comparators. NOTE: these are the keyword forms `gt/gte/lt/lte`, -/// NOT the SQL-operator strings (`>`,`>=`,`<`,`<=`) used by `temps-monitoring`. -pub const ALLOWED_COMPARATORS: &[&str] = &["gt", "gte", "lt", "lte"]; - /// The allowlisted severities, mapped to `AlarmSeverity` by the evaluator. pub const ALLOWED_SEVERITIES: &[&str] = &["info", "warning", "critical"]; const MAX_NAME_LEN: usize = 200; const MAX_METRIC_NAME_LEN: usize = 256; -/// Validate the full set of rule fields against the allowlists and sane bounds. +/// Validate the cross-cutting rule fields against the allowlists and sane bounds, +/// then delegate detector-specific validation to the typed [`DetectionConfig`]. /// /// Returns [`OtelError::Validation`] on the first invalid field so the caller /// surfaces a 400 rather than persisting an un-evaluable rule. -#[allow(clippy::too_many_arguments)] fn validate_rule( name: &str, metric_name: &str, aggregation: &str, - comparator: &str, severity: &str, - threshold: f64, window_secs: i32, for_duration_secs: i32, + detection_config: &DetectionConfig, ) -> Result<(), OtelError> { let trimmed_name = name.trim(); if trimmed_name.is_empty() { @@ -85,16 +81,6 @@ fn validate_rule( ), }); } - let cmp = comparator.trim().to_ascii_lowercase(); - if !ALLOWED_COMPARATORS.contains(&cmp.as_str()) { - return Err(OtelError::Validation { - message: format!( - "Invalid comparator '{}' (allowed: {})", - comparator, - ALLOWED_COMPARATORS.join(", ") - ), - }); - } let sev = severity.trim().to_ascii_lowercase(); if !ALLOWED_SEVERITIES.contains(&sev.as_str()) { return Err(OtelError::Validation { @@ -105,11 +91,6 @@ fn validate_rule( ), }); } - if !threshold.is_finite() { - return Err(OtelError::Validation { - message: "threshold must be a finite number".to_string(), - }); - } if window_secs <= 0 { return Err(OtelError::Validation { message: "window_secs must be greater than 0".to_string(), @@ -120,6 +101,9 @@ fn validate_rule( message: "for_duration_secs must be greater than 0".to_string(), }); } + // Detector-specific invariants (threshold finiteness for static rules; the + // not-yet-supported guard for anomaly/forecast/outlier/auto_watch). + detection_config.validate()?; Ok(()) } @@ -162,8 +146,7 @@ impl MetricAlertService { name: String, metric_name: String, aggregation: String, - comparator: String, - threshold: f64, + detection_config: DetectionConfig, window_secs: i32, for_duration_secs: i32, severity: String, @@ -173,11 +156,10 @@ impl MetricAlertService { &name, &metric_name, &aggregation, - &comparator, &severity, - threshold, window_secs, for_duration_secs, + &detection_config, )?; let model = ActiveModel { @@ -185,8 +167,8 @@ impl MetricAlertService { name: Set(name.trim().to_string()), metric_name: Set(metric_name.trim().to_string()), aggregation: Set(aggregation.trim().to_ascii_lowercase()), - comparator: Set(comparator.trim().to_ascii_lowercase()), - threshold: Set(threshold), + detection_kind: Set(detection_config.kind_str().to_string()), + detection_config: Set(detection_config.to_value()?), window_secs: Set(window_secs), for_duration_secs: Set(for_duration_secs), severity: Set(severity.trim().to_ascii_lowercase()), @@ -221,8 +203,7 @@ impl MetricAlertService { name: Option, metric_name: Option, aggregation: Option, - comparator: Option, - threshold: Option, + detection_config: Option, window_secs: Option, for_duration_secs: Option, severity: Option, @@ -232,7 +213,8 @@ impl MetricAlertService { let existing = self.get(project_id, id).await?; // Validate the merged effective field set so partial updates can't leave - // the row in an un-evaluable state. + // the row in an un-evaluable state. A detector is replaced wholesale: the + // supplied config, or the existing one decoded from the stored blob. let eff_name = name.clone().unwrap_or_else(|| existing.name.clone()); let eff_metric = metric_name .clone() @@ -240,24 +222,23 @@ impl MetricAlertService { let eff_agg = aggregation .clone() .unwrap_or_else(|| existing.aggregation.clone()); - let eff_cmp = comparator - .clone() - .unwrap_or_else(|| existing.comparator.clone()); let eff_sev = severity .clone() .unwrap_or_else(|| existing.severity.clone()); - let eff_threshold = threshold.unwrap_or(existing.threshold); let eff_window = window_secs.unwrap_or(existing.window_secs); let eff_for = for_duration_secs.unwrap_or(existing.for_duration_secs); + let eff_config = match &detection_config { + Some(c) => c.clone(), + None => DetectionConfig::from_value(&existing.detection_config)?, + }; validate_rule( &eff_name, &eff_metric, &eff_agg, - &eff_cmp, &eff_sev, - eff_threshold, eff_window, eff_for, + &eff_config, )?; let mut active: ActiveModel = existing.into(); @@ -270,11 +251,9 @@ impl MetricAlertService { if let Some(a) = aggregation { active.aggregation = Set(a.trim().to_ascii_lowercase()); } - if let Some(c) = comparator { - active.comparator = Set(c.trim().to_ascii_lowercase()); - } - if let Some(t) = threshold { - active.threshold = Set(t); + if let Some(c) = detection_config { + active.detection_kind = Set(c.kind_str().to_string()); + active.detection_config = Set(c.to_value()?); } if let Some(w) = window_secs { active.window_secs = Set(w); @@ -343,10 +322,19 @@ impl MetricAlertService { #[cfg(test)] mod tests { use super::*; + use crate::detectors::{Comparator, StaticParams}; use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Value}; use std::collections::BTreeMap; use temps_core::DBDateTime; + /// A valid static detector config for test rules. + fn static_cfg() -> DetectionConfig { + DetectionConfig::Static(StaticParams { + comparator: Comparator::Gt, + threshold: 500.0, + }) + } + /// Build a MockRow representing a `COUNT(*) AS num_items` result for the /// sea-orm paginator. `num_items()` reads `try_get::("", "num_items")`. fn count_row(n: i64) -> BTreeMap { @@ -363,8 +351,10 @@ mod tests { name: "High latency".to_string(), metric_name: "http.server.duration".to_string(), aggregation: "p95".to_string(), - comparator: "gt".to_string(), - threshold: 500.0, + detection_kind: "static".to_string(), + detection_config: serde_json::json!({ + "kind": "static", "comparator": "gt", "threshold": 500.0 + }), window_secs: 300, for_duration_secs: 120, severity: "warning".to_string(), @@ -390,8 +380,7 @@ mod tests { "High latency".to_string(), "http.server.duration".to_string(), "p95".to_string(), - "gt".to_string(), - 500.0, + static_cfg(), 300, 120, "warning".to_string(), @@ -403,7 +392,7 @@ mod tests { let model = result.unwrap(); assert_eq!(model.id, 1); assert_eq!(model.project_id, 7); - assert_eq!(model.comparator, "gt"); + assert_eq!(model.detection_kind, "static"); } #[tokio::test] @@ -417,8 +406,7 @@ mod tests { " ".to_string(), "http.server.duration".to_string(), "p95".to_string(), - "gt".to_string(), - 500.0, + static_cfg(), 300, 120, "warning".to_string(), @@ -439,8 +427,7 @@ mod tests { "Rule".to_string(), "http.server.duration".to_string(), "median".to_string(), - "gt".to_string(), - 500.0, + static_cfg(), 300, 120, "warning".to_string(), @@ -451,18 +438,21 @@ mod tests { } #[tokio::test] - async fn test_create_bad_comparator_validation() { + async fn test_create_anomaly_kind_rejected_until_supported() { + // Anomaly (and other non-static) detectors are typed/schema-present but + // not yet evaluable, so creation is rejected with a Validation error. let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); let service = MetricAlertService::new(Arc::new(db)); + let anomaly = DetectionConfig::from_value(&serde_json::json!({ "kind": "anomaly" })) + .expect("anomaly config parses"); let result = service .create( 7, "Rule".to_string(), "http.server.duration".to_string(), "p95".to_string(), - ">".to_string(), // SQL operator form is NOT allowed here - 500.0, + anomaly, 300, 120, "warning".to_string(), @@ -483,8 +473,7 @@ mod tests { "Rule".to_string(), "http.server.duration".to_string(), "p95".to_string(), - "gt".to_string(), - 500.0, + static_cfg(), 0, 120, "warning".to_string(), @@ -527,7 +516,6 @@ mod tests { None, None, None, - None, ) .await; assert!(matches!( diff --git a/web/src/api/client/types.gen.ts b/web/src/api/client/types.gen.ts index 085087b35..a3c7731c1 100644 --- a/web/src/api/client/types.gen.ts +++ b/web/src/api/client/types.gen.ts @@ -750,6 +750,43 @@ export type AnalyticsSessionEventsResponse = { total_events: number; }; +/** + * Anomaly baseline algorithm. Adding one (e.g. a new robust variant) is a + * code-only enum addition — no migration, since it lives inside the blob. + */ +export type AnomalyAlgorithm = 'robust' | 'basic' | 'agile' | 'ewma'; + +/** + * Seasonal anomaly-band detector parameters (stub — not yet evaluated). + */ +export type AnomalyParams = { + /** + * Baseline model. `robust` is the default (seasonal, stable, flags level + * shifts); `ewma`/`agile` adopt level shifts; `basic` is non-seasonal. + */ + algorithm?: AnomalyAlgorithm; + /** + * How far back to build the baseline. `None` = an evaluator default. + */ + baseline_lookback_days?: number | null; + /** + * Band width in robust standard deviations (Datadog's `bounds`). + */ + deviations?: number; + /** + * Which side(s) of the band a deviation must be on to count. + */ + direction?: Direction; + /** + * Fraction (0..=1) of points in the window that must be anomalous to fire. + */ + pct_anomalous?: number; + /** + * Seasonality model for the baseline. + */ + seasonality?: Seasonality; +}; + export type ApiKeyListResponse = { api_keys: Array; total: number; @@ -1021,6 +1058,16 @@ export type AuthTokenResponse = { refresh_token: string; }; +/** + * Auto-watch (Watchdog-style) detector parameters (stub — not evaluated). + */ +export type AutoWatchParams = { + /** + * The engine self-tunes the band; the user supplies only the direction. + */ + direction?: Direction; +}; + export type AutofixerRunResponse = { ai_model?: string | null; ai_output?: string | null; @@ -1852,6 +1899,13 @@ export type CommitListResponse = { commits: Array; }; +/** + * Comparator for static/forecast threshold detectors. Serializes to the + * keyword forms `gt|gte|lt|lte` (NOT the SQL operators used by + * `temps-monitoring::compare`). + */ +export type Comparator = 'gt' | 'gte' | 'lt' | 'lte'; + export type ConnectionListQuery = { direction?: string | null; page?: number | null; @@ -2703,9 +2757,10 @@ export type CreateMetricAlertRequest = { */ aggregation: string; /** - * One of `gt|gte|lt|lte`. + * The detector: a discriminated union keyed by `kind`. Today only + * `{ "kind": "static", "comparator": "gt", "threshold": 500 }` is evaluable. */ - comparator: string; + detection_config: DetectionConfig; enabled: boolean; for_duration_secs: number; metric_name: string; @@ -2715,7 +2770,6 @@ export type CreateMetricAlertRequest = { * One of `info|warning|critical`. */ severity: string; - threshold: number; window_secs: number; }; @@ -3853,6 +3907,27 @@ export type DeploymentStateResponse = { */ export type DeploymentStrategy = 'replace' | 'blue-green' | 'rolling'; +/** + * The typed detector definition stored (as jsonb) in + * `metric_alert_rules.detection_config`. + * + * Today only [`DetectionConfig::Static`] is evaluable; the other variants are + * schema-present (so the SDK/UI and storage are already future-shaped) but + * rejected by [`DetectionConfig::validate`] until their evaluator lands. Each is + * then enabled code-only, with no schema migration. + */ +export type DetectionConfig = (StaticParams & { + kind: 'static'; +}) | (AnomalyParams & { + kind: 'anomaly'; +}) | (ForecastParams & { + kind: 'forecast'; +}) | (OutlierParams & { + kind: 'outlier'; +}) | (AutoWatchParams & { + kind: 'auto_watch'; +}); + export type DeviceCount = { count: number; device_type: string; @@ -3872,6 +3947,11 @@ export type DigestSections = { projects?: boolean; }; +/** + * Which side(s) of an anomaly band count as a deviation. + */ +export type Direction = 'both' | 'above' | 'below'; + /** * Response after disabling Blob service */ @@ -5854,6 +5934,28 @@ export type FieldResponse = { nullable: boolean; }; +/** + * Forecast model family. + */ +export type ForecastAlgorithm = 'linear' | 'seasonal'; + +/** + * Forecast detector parameters (stub — not yet evaluated). + */ +export type ForecastParams = { + algorithm?: ForecastAlgorithm; + /** + * Comparator + threshold the *forecast* is checked against. + */ + comparator: Comparator; + deviations?: number; + /** + * How far ahead to project before checking the breach condition. + */ + forecast_horizon_secs: number; + threshold: number; +}; + export type FullError = { /** * Full JSONB blob from `error_events.data` — stack trace, breadcrumbs, @@ -8895,8 +8997,15 @@ export type OtelDashboardsResponse = { export type OtelMetricAlertRuleResponse = { aggregation: string; - comparator: string; created_at: string; + /** + * The typed detector definition (discriminated union keyed by `kind`). + */ + detection_config: DetectionConfig; + /** + * Coarse detector discriminator: `static|anomaly|forecast|outlier|auto_watch`. + */ + detection_kind: string; enabled: boolean; for_duration_secs: number; id: number; @@ -8910,7 +9019,6 @@ export type OtelMetricAlertRuleResponse = { name: string; project_id: number; severity: string; - threshold: number; updated_at: string; window_secs: number; }; @@ -8929,6 +9037,26 @@ export type OtelMetricsResponse = { data: Array; }; +/** + * Outlier detection algorithm. + */ +export type OutlierAlgorithm = 'dbscan' | 'scaled_dbscan' | 'mad' | 'scaled_mad'; + +/** + * Outlier (cross-series population) detector parameters (stub — not evaluated). + */ +export type OutlierParams = { + algorithm?: OutlierAlgorithm; + /** + * Label key defining the peer population compared across series (e.g. `host`). + */ + peer_group_key: string; + /** + * Sensitivity; higher tolerates larger spread before flagging. + */ + tolerance?: number; +}; + /** * Time bucket data point for page activity graph */ @@ -12036,6 +12164,11 @@ export type SearchLogsResponse = { */ export type SearchMode = 'index' | 'archive'; +/** + * Seasonality model for an anomaly baseline. + */ +export type Seasonality = 'none' | 'hourly' | 'daily' | 'weekly'; + export type SecretResponse = { created_at: string; description?: string | null; @@ -13485,6 +13618,20 @@ export type StaticBundleResponse = { uploaded_at: string; }; +/** + * Static threshold detector: compare the aggregated `value` against `threshold`. + */ +export type StaticParams = { + /** + * How `value` is compared against `threshold`. + */ + comparator: Comparator; + /** + * The threshold the aggregated value is compared against. + */ + threshold: number; +}; + /** * Configuration for static site presets (Vite, Next.js, Docusaurus, etc.) * These presets build static sites that are served via a web server @@ -14657,13 +14804,12 @@ export type UpdateMcpRequest = { export type UpdateMetricAlertRequest = { aggregation?: string | null; - comparator?: string | null; + detection_config?: null | DetectionConfig; enabled?: boolean | null; for_duration_secs?: number | null; metric_name?: string | null; name?: string | null; severity?: string | null; - threshold?: number | null; window_secs?: number | null; }; diff --git a/web/src/components/metrics/alert-format.tsx b/web/src/components/metrics/alert-format.tsx index ff10605c1..cbc96ce8f 100644 --- a/web/src/components/metrics/alert-format.tsx +++ b/web/src/components/metrics/alert-format.tsx @@ -3,6 +3,7 @@ // Keeps the list rows, the form header, and any future surfaces rendering the // firing-state badge + one-line rule summary identically. +import type { OtelMetricAlertRuleResponse } from '@/api/client' import { Badge } from '@/components/ui/badge' import { aggregationLabel } from './metric-format' @@ -27,16 +28,18 @@ export function comparatorSymbol(comparator: string): string { /** * Compact one-line description of a rule for the list row, e.g. * `avg http.server.duration > 500 · warning`. + * + * Reads the detector from the typed `detection_config` discriminated union. + * Today only `static` rules exist; the fallback covers future kinds + * (anomaly/forecast/outlier/auto_watch) without breaking the row. */ -export function alertSummary(rule: { - metric_name: string - aggregation: string - comparator: string - threshold: number - severity: string -}): string { +export function alertSummary(rule: OtelMetricAlertRuleResponse): string { const agg = aggregationLabel(rule.aggregation) - return `${agg} ${rule.metric_name} ${comparatorSymbol(rule.comparator)} ${rule.threshold} · ${rule.severity}` + const cfg = rule.detection_config + if (cfg.kind === 'static') { + return `${agg} ${rule.metric_name} ${comparatorSymbol(cfg.comparator)} ${cfg.threshold} · ${rule.severity}` + } + return `${agg} ${rule.metric_name} · ${cfg.kind} · ${rule.severity}` } /** Firing-state badge derived from `rule.last_state` (ok|firing|unknown). */ diff --git a/web/src/pages/MetricAlertForm.tsx b/web/src/pages/MetricAlertForm.tsx index f188d63a5..2dc40593d 100644 --- a/web/src/pages/MetricAlertForm.tsx +++ b/web/src/pages/MetricAlertForm.tsx @@ -1,4 +1,5 @@ import { ProjectResponse } from '@/api/client' +import type { Comparator } from '@/api/client' // REGEN: bun run openapi-ts — these builders/types come from the new // /otel/alerts endpoints (operationIds get_alert / create_alert / update_alert). // They are absent from the committed SDK; imported here so the form is @@ -139,12 +140,20 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { const defaultValues = useMemo(() => { const existing = existingQuery.data if (existing) { + // The form edits a static threshold; pull comparator/threshold out of the + // typed detector union (only `static` is editable here for now). + const cfg = existing.detection_config + const isStatic = cfg.kind === 'static' return { name: existing.name, metric_name: existing.metric_name, aggregation: coerce(AGGREGATION_VALUES, existing.aggregation, 'avg'), - comparator: coerce(COMPARATOR_VALUES, existing.comparator, 'gt'), - threshold: existing.threshold, + comparator: coerce( + COMPARATOR_VALUES, + isStatic ? cfg.comparator : 'gt', + 'gt', + ), + threshold: isStatic ? cfg.threshold : 0, window_secs: existing.window_secs, for_duration_secs: existing.for_duration_secs, severity: coerce(SEVERITY_VALUES, existing.severity, 'warning'), @@ -190,6 +199,15 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { const isMutating = createMutation.isPending || updateMutation.isPending const onSubmit = async (data: AlertFormData) => { + // The form authors a static threshold detector; wrap the flat comparator + + // threshold into the typed `detection_config` discriminated union. The Zod + // enum guarantees `comparator` is one of the Comparator literals, so the + // narrowing cast is sound. + const detection_config = { + kind: 'static' as const, + comparator: data.comparator as Comparator, + threshold: data.threshold, + } if (isEditing) { await updateMutation.mutateAsync({ path: { id }, @@ -198,8 +216,7 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { name: data.name, metric_name: data.metric_name, aggregation: data.aggregation, - comparator: data.comparator, - threshold: data.threshold, + detection_config, window_secs: data.window_secs, for_duration_secs: data.for_duration_secs, severity: data.severity, @@ -213,8 +230,7 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { name: data.name, metric_name: data.metric_name, aggregation: data.aggregation, - comparator: data.comparator, - threshold: data.threshold, + detection_config, window_secs: data.window_secs, for_duration_secs: data.for_duration_secs, severity: data.severity, diff --git a/web/src/pages/MetricsExplorer.tsx b/web/src/pages/MetricsExplorer.tsx index a54960257..81f6dcd59 100644 --- a/web/src/pages/MetricsExplorer.tsx +++ b/web/src/pages/MetricsExplorer.tsx @@ -286,11 +286,18 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { if (!metricName) return [] return (alertsQuery.data?.data ?? []) .filter((r) => r.enabled && r.metric_name === metricName) - .map((r) => ({ - value: r.threshold, - tone: r.severity === 'critical' ? 'poor' : 'warn', - label: `${comparatorSymbol(r.comparator)} ${r.threshold}`, - })) + .flatMap((r) => { + // Only static rules have a single horizontal threshold to overlay. + const cfg = r.detection_config + if (cfg.kind !== 'static') return [] + return [ + { + value: cfg.threshold, + tone: r.severity === 'critical' ? 'poor' : 'warn', + label: `${comparatorSymbol(cfg.comparator)} ${cfg.threshold}`, + } satisfies ThresholdBand, + ] + }) }, [alertsQuery.data, metricName]) const buckets = metricsQuery.data?.data ?? [] From bb9f4d9d144dd5ecf7ffaf196fb3e3dffa85879a Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 15:15:33 +0200 Subject: [PATCH 11/56] feat(otel): v1 metric anomaly detection (robust seasonal MAD band) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the `anomaly` detector evaluable end to end — it was a typed, creation-rejected stub. A rule now learns a baseline band from history and fires when the current value deviates from it, reusing the same for-duration state machine and AlarmService as static threshold rules. Detection math (pure, unit-tested) in `detectors`: - robust_band = median + MAD·1.4826 (consistent-with-σ scale). - anomaly_breaches = direction-aware (above/below/both) z-score test, with a MIN_BAND_SCALE floor so a flat baseline can't divide by zero. - season_cell buckets a timestamp into none/hourly/daily/weekly cells. - validate() now accepts anomaly (robust/basic); agile/ewma and bad hyperparameters (deviations≤0, pct∉(0,1], lookback∉1..=90) are rejected. Evaluator branch (`metric_alert_evaluator`): - Baseline fetched via the SAME query_metrics aggregation path as the scored point (so counter-rate / histogram-percentile compare like-for-like — NOT get_metric_baseline, which bypasses aggregation), cached per rule for 1h. - Seasonal-cell filter with cold-start fallback to the global band; an insufficient (<8 samples) or degenerate (flat) baseline PRESERVES state rather than firing — no spurious alerts on thin history. - fire() refactored to a detector-agnostic FireDetails (static vs anomaly message/metadata, e.g. "820ms is 4.2σ from the baseline 210 ± 90"). - run_cycle prunes breach-timer + baseline caches for disabled/deleted rules (also fixes a pre-existing breach_start leak). Latent bug fixed (affected static AND anomaly): translate_bucket_interval only accepted space-separated forms, so the evaluator's `format!("{}s", secs)` ("300s") silently fell back to INTERVAL 1 HOUR — every windowed query was coarsened to hourly regardless of window_secs. Now also parses the compact "300s"/"5m"/"1h"/"2d"/"1w" form. Frontend: the alert form authors anomaly rules — a Detection selector swaps the static comparator/threshold for algorithm / sensitivity (σ) / direction / seasonality; the list summary reads the typed config. Verified live (ClickHouse-backed): anomaly create accepted (was 400); insufficient baseline preserves state; a normal value stays ok (no false positive); an injected spike (100000 vs a ~100±15 band) transitions to firing and raises an alarm. 31 unit tests pass; clippy -D warnings clean; frontend typechecks; the form renders anomaly fields. --- crates/temps-otel/src/detectors/mod.rs | 201 ++++++++++- .../src/services/metric_alert_evaluator.rs | 301 ++++++++++++++-- .../src/services/metric_alert_service.rs | 15 +- .../temps-otel/src/storage/clickhouse/mod.rs | 53 ++- web/src/components/metrics/alert-format.tsx | 30 ++ web/src/pages/MetricAlertForm.tsx | 341 ++++++++++++++---- 6 files changed, 824 insertions(+), 117 deletions(-) diff --git a/crates/temps-otel/src/detectors/mod.rs b/crates/temps-otel/src/detectors/mod.rs index 1758f8c55..209939237 100644 --- a/crates/temps-otel/src/detectors/mod.rs +++ b/crates/temps-otel/src/detectors/mod.rs @@ -19,6 +19,7 @@ //! alongside `serde(tag)` in utoipa 5.x). utoipa + hey-api render this as a //! usable `(StaticParams & { kind: 'static' }) | …` TS discriminated union. +use chrono::{DateTime, Datelike, Timelike, Utc}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -31,6 +32,75 @@ fn default_pct_anomalous() -> f64 { 1.0 } +/// Floor applied to a band's scale before dividing, so a (near-)flat baseline +/// can't produce an infinite z-score. A scale below this is treated as +/// degenerate by the evaluator (insufficient baseline → preserve state). +pub const MIN_BAND_SCALE: f64 = 1e-9; + +/// The robust band center + scale for an anomaly detector, from baseline values: +/// `(median, MAD · 1.4826)`. The 1.4826 factor makes MAD a consistent estimator +/// of the standard deviation for normal data, so `deviations` keeps the same +/// "sigmas" meaning as Datadog's `bounds`. Returns `None` for an empty baseline. +pub fn robust_band(values: &[f64]) -> Option<(f64, f64)> { + if values.is_empty() { + return None; + } + let center = median(values); + let deviations: Vec = values.iter().map(|v| (v - center).abs()).collect(); + let mad = median(&deviations); + Some((center, mad * 1.4826)) +} + +/// Median of `values` (ignoring non-finite entries). Returns 0.0 if empty. +fn median(values: &[f64]) -> f64 { + let mut v: Vec = values.iter().copied().filter(|x| x.is_finite()).collect(); + if v.is_empty() { + return 0.0; + } + v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let n = v.len(); + if n % 2 == 1 { + v[n / 2] + } else { + (v[n / 2 - 1] + v[n / 2]) / 2.0 + } +} + +/// Whether `value` deviates from the band `(center, scale)` by more than +/// `deviations` scaled units, honouring `direction`. `scale` is floored by +/// [`MIN_BAND_SCALE`] so a flat band can't divide by zero (the evaluator treats +/// a degenerate scale as insufficient before calling this — this is defence). +pub fn anomaly_breaches( + value: f64, + center: f64, + scale: f64, + deviations: f64, + direction: Direction, +) -> bool { + let z = (value - center) / scale.max(MIN_BAND_SCALE); + match direction { + Direction::Both => z.abs() > deviations, + Direction::Above => z > deviations, + Direction::Below => -z > deviations, + } +} + +/// A comparable "seasonal cell" id for `ts` under `seasonality`. Baseline buckets +/// are filtered to those sharing the scored bucket's cell, so e.g. a Tuesday-2pm +/// value is compared only against historical Tuesday-2pm values. +pub fn season_cell(ts: DateTime, seasonality: Seasonality) -> i64 { + match seasonality { + // One global cell — the band spans the whole lookback. + Seasonality::None => 0, + // Pattern repeats each hour: cell = minute-of-hour. + Seasonality::Hourly => ts.minute() as i64, + // Pattern repeats each day: cell = hour-of-day. + Seasonality::Daily => ts.hour() as i64, + // Pattern repeats each week: cell = (weekday, hour-of-day). + Seasonality::Weekly => ts.weekday().num_days_from_monday() as i64 * 24 + ts.hour() as i64, + } +} + /// The typed detector definition stored (as jsonb) in /// `metric_alert_rules.detection_config`. /// @@ -245,10 +315,11 @@ impl DetectionConfig { }) } - /// Validate the detector's invariants. Currently only `static` rules are - /// evaluable; the other (typed, schema-present) kinds are rejected at - /// create/update time until their evaluator lands — enabling each later is - /// code-only, never a migration. + /// Validate the detector's invariants. `static` and `anomaly` (robust/basic + /// band) are evaluable; `forecast`/`outlier`/`auto_watch` (and the + /// agile/ewma anomaly algorithms) are typed, schema-present stubs rejected + /// here until their evaluator lands — enabling each later is code-only, + /// never a migration. pub fn validate(&self) -> Result<(), OtelError> { match self { DetectionConfig::Static(p) => { @@ -259,6 +330,38 @@ impl DetectionConfig { } Ok(()) } + DetectionConfig::Anomaly(p) => { + if !p.deviations.is_finite() || p.deviations <= 0.0 { + return Err(OtelError::Validation { + message: "anomaly deviations must be a finite number > 0".to_string(), + }); + } + if !(p.pct_anomalous > 0.0 && p.pct_anomalous <= 1.0) { + return Err(OtelError::Validation { + message: "anomaly pct_anomalous must be in (0, 1]".to_string(), + }); + } + if let Some(days) = p.baseline_lookback_days { + if !(1..=90).contains(&days) { + return Err(OtelError::Validation { + message: "anomaly baseline_lookback_days must be between 1 and 90" + .to_string(), + }); + } + } + match p.algorithm { + // v1 implements a robust seasonal MAD band; `basic` is the + // same math with `seasonality=none`. + AnomalyAlgorithm::Robust | AnomalyAlgorithm::Basic => Ok(()), + AnomalyAlgorithm::Agile | AnomalyAlgorithm::Ewma => { + Err(OtelError::Validation { + message: "anomaly algorithm 'agile'/'ewma' is not yet supported \ + (use 'robust' or 'basic')" + .to_string(), + }) + } + } + } other => Err(OtelError::Validation { message: format!("detector kind '{}' is not yet supported", other.kind_str()), }), @@ -325,16 +428,100 @@ mod tests { } #[test] - fn test_validate_rejects_unsupported_kinds() { - // The non-static kinds are typed (schema-present) but not yet evaluable. + fn test_validate_anomaly_ok_and_rejections() { + // Default anomaly (robust) is now evaluable. let anomaly = DetectionConfig::from_value(&serde_json::json!({ "kind": "anomaly" })).unwrap(); + assert!(anomaly.validate().is_ok()); + + // agile/ewma algorithms are typed but not yet implemented → rejected. + let ewma = DetectionConfig::from_value( + &serde_json::json!({ "kind": "anomaly", "algorithm": "ewma" }), + ) + .unwrap(); + assert!(matches!(ewma.validate(), Err(OtelError::Validation { .. }))); + + // Bad hyperparameters are rejected. + let bad_dev = DetectionConfig::from_value( + &serde_json::json!({ "kind": "anomaly", "deviations": 0.0 }), + ) + .unwrap(); + assert!(matches!( + bad_dev.validate(), + Err(OtelError::Validation { .. }) + )); + let bad_pct = DetectionConfig::from_value( + &serde_json::json!({ "kind": "anomaly", "pct_anomalous": 1.5 }), + ) + .unwrap(); + assert!(matches!( + bad_pct.validate(), + Err(OtelError::Validation { .. }) + )); + let bad_lookback = DetectionConfig::from_value( + &serde_json::json!({ "kind": "anomaly", "baseline_lookback_days": 9999 }), + ) + .unwrap(); assert!(matches!( - anomaly.validate(), + bad_lookback.validate(), Err(OtelError::Validation { .. }) )); + } + + #[test] + fn test_validate_other_kinds_still_rejected() { + // forecast/outlier/auto_watch remain typed-but-unsupported stubs. let auto = DetectionConfig::AutoWatch(AutoWatchParams::default()); assert!(matches!(auto.validate(), Err(OtelError::Validation { .. }))); + let outlier = DetectionConfig::from_value( + &serde_json::json!({ "kind": "outlier", "peer_group_key": "host" }), + ) + .unwrap(); + assert!(matches!( + outlier.validate(), + Err(OtelError::Validation { .. }) + )); + } + + #[test] + fn test_robust_band() { + // Symmetric data: median 30, abs-devs [20,10,0,10,20] → MAD 10 → scale 14.826. + let (center, scale) = robust_band(&[10.0, 20.0, 30.0, 40.0, 50.0]).unwrap(); + assert!((center - 30.0).abs() < 1e-9); + assert!((scale - 14.826).abs() < 1e-3); + // A flat baseline has zero scale (the evaluator treats this as degenerate). + let (c, s) = robust_band(&[5.0, 5.0, 5.0]).unwrap(); + assert_eq!(c, 5.0); + assert_eq!(s, 0.0); + assert!(robust_band(&[]).is_none()); + } + + #[test] + fn test_anomaly_breaches_direction() { + // center 100, scale 10, deviations 3 → band [70, 130]. + assert!(anomaly_breaches(140.0, 100.0, 10.0, 3.0, Direction::Both)); + assert!(anomaly_breaches(50.0, 100.0, 10.0, 3.0, Direction::Both)); + assert!(!anomaly_breaches(120.0, 100.0, 10.0, 3.0, Direction::Both)); + // Above only catches high excursions. + assert!(anomaly_breaches(140.0, 100.0, 10.0, 3.0, Direction::Above)); + assert!(!anomaly_breaches(50.0, 100.0, 10.0, 3.0, Direction::Above)); + // Below only catches low excursions. + assert!(anomaly_breaches(50.0, 100.0, 10.0, 3.0, Direction::Below)); + assert!(!anomaly_breaches(140.0, 100.0, 10.0, 3.0, Direction::Below)); + // A flat band (scale 0) is floored, not a divide-by-zero panic. + assert!(anomaly_breaches(5.0001, 5.0, 0.0, 3.0, Direction::Both)); + } + + #[test] + fn test_season_cell() { + use chrono::TimeZone; + // 2026-06-23 is a Tuesday, 14:35 UTC. + let t = Utc.with_ymd_and_hms(2026, 6, 23, 14, 35, 0).unwrap(); + assert_eq!(season_cell(t, Seasonality::None), 0); + assert_eq!(season_cell(t, Seasonality::Hourly), 35); // minute-of-hour + assert_eq!(season_cell(t, Seasonality::Daily), 14); // hour-of-day + // Tuesday = num_days_from_monday 1 → 1*24 + 14 = 38. + assert_eq!(season_cell(t, Seasonality::Weekly), 38); } #[test] diff --git a/crates/temps-otel/src/services/metric_alert_evaluator.rs b/crates/temps-otel/src/services/metric_alert_evaluator.rs index 3469bf9be..fc34c5b1d 100644 --- a/crates/temps-otel/src/services/metric_alert_evaluator.rs +++ b/crates/temps-otel/src/services/metric_alert_evaluator.rs @@ -17,18 +17,21 @@ //! `last_state`/`last_value`/`last_evaluated_at` are persisted so the UI badge //! survives restarts. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, Instant}; -use chrono::Utc; +use chrono::{DateTime, Utc}; use serde_json::json; use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; use temps_monitoring::{AlarmService, AlarmSeverity, AlarmType, FireAlarmRequest}; -use crate::detectors::DetectionConfig; +use crate::detectors::{ + anomaly_breaches, robust_band, season_cell, AnomalyParams, DetectionConfig, StaticParams, + MIN_BAND_SCALE, +}; use crate::services::metric_alert_service::MetricAlertService; use crate::services::OtelService; use crate::types::{MetricAggregation, MetricBucket, MetricQuery}; @@ -37,6 +40,19 @@ use temps_entities::metric_alert_rules::Model as AlertRule; /// How often the evaluator scans enabled rules. const EVAL_INTERVAL_SECS: u64 = 30; +/// How long a per-rule anomaly baseline is cached before refetching from +/// storage. The band moves slowly, so the hot 30s tick scores the current value +/// against the cached baseline instead of re-querying the full lookback window. +const BASELINE_REFRESH_SECS: u64 = 3600; + +/// Default anomaly baseline lookback when a rule doesn't set one. +const DEFAULT_LOOKBACK_DAYS: i32 = 14; + +/// Minimum baseline samples required to compute a trustworthy band. Below this — +/// even after the seasonal→global fallback — the rule preserves state rather +/// than firing off a thin baseline (cold-start safety). +const MIN_BASELINE_SAMPLES: usize = 8; + /// The state transition the evaluator should apply for a single rule this cycle. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AlertTransition { @@ -133,6 +149,90 @@ fn map_severity(severity: &str) -> AlarmSeverity { } } +/// A per-rule anomaly baseline cached across evaluator ticks. `values` are +/// `(bucket_timestamp, aggregated_value)` over the rule's lookback window, +/// fetched through the same aggregation path as the scored point. +struct CachedBaseline { + fetched_at: Instant, + values: Vec<(DateTime, f64)>, +} + +/// The result of evaluating an anomaly rule against its baseline band. +struct AnomalyEval { + breaching: bool, + center: f64, + scale: f64, +} + +/// What to put on a fired alarm — built by the detector branch so `fire` stays +/// detector-agnostic. +struct FireDetails { + title: String, + message: String, + metadata: serde_json::Value, +} + +impl FireDetails { + fn static_breach(rule: &AlertRule, value: f64, p: &StaticParams) -> Self { + FireDetails { + title: format!("Metric threshold breached: {}", rule.name), + message: format!( + "{} {} is {:.3} (threshold {} {:.3})", + rule.metric_name, + rule.aggregation, + value, + p.comparator.symbol(), + p.threshold + ), + metadata: json!({ + "rule_id": rule.id, + "metric_name": rule.metric_name, + "aggregation": rule.aggregation, + "value": value, + "threshold": p.threshold, + "comparator": p.comparator.symbol(), + "detection_kind": "static", + "window_secs": rule.window_secs, + "for_duration_secs": rule.for_duration_secs, + "source": "otel_metric_alert", + }), + } + } + + fn anomaly_breach(rule: &AlertRule, value: f64, p: &AnomalyParams, ev: &AnomalyEval) -> Self { + let z = (value - ev.center) / ev.scale.max(MIN_BAND_SCALE); + FireDetails { + title: format!("Metric anomaly: {}", rule.name), + message: format!( + "{} {} is {:.3} — {:.1}σ from the baseline {:.3} ± {:.3} (band ±{}σ)", + rule.metric_name, + rule.aggregation, + value, + z.abs(), + ev.center, + ev.scale, + p.deviations + ), + metadata: json!({ + "rule_id": rule.id, + "metric_name": rule.metric_name, + "aggregation": rule.aggregation, + "value": value, + "baseline_center": ev.center, + "baseline_scale": ev.scale, + "z_score": z, + "deviations": p.deviations, + "algorithm": format!("{:?}", p.algorithm).to_lowercase(), + "seasonality": format!("{:?}", p.seasonality).to_lowercase(), + "detection_kind": "anomaly", + "window_secs": rule.window_secs, + "for_duration_secs": rule.for_duration_secs, + "source": "otel_metric_anomaly", + }), + } + } +} + /// Background task that evaluates enabled metric alert rules and fires/resolves /// notifications via the reused alarm system. pub struct MetricAlertEvaluator { @@ -143,6 +243,8 @@ pub struct MetricAlertEvaluator { breach_start: Arc>>, /// rule_id -> alarm_id, for resolving the matching alarm on recovery. firing: Arc>>, + /// rule_id -> cached anomaly baseline (refreshed every `BASELINE_REFRESH_SECS`). + baseline_cache: Arc>>, } impl MetricAlertEvaluator { @@ -157,6 +259,7 @@ impl MetricAlertEvaluator { alarm_service, breach_start: Arc::new(RwLock::new(HashMap::new())), firing: Arc::new(RwLock::new(HashMap::new())), + baseline_cache: Arc::new(RwLock::new(HashMap::new())), } } @@ -177,6 +280,19 @@ impl MetricAlertEvaluator { async fn run_cycle(&self) -> Result<(), crate::error::OtelError> { let rules = self.alert_service.list_enabled().await?; debug!(rule_count = rules.len(), "Evaluating metric alert rules"); + + // Prune transient per-rule state for rules that are no longer enabled, + // so deleted/disabled rules don't leak breach timers or baseline caches. + let live: HashSet = rules.iter().map(|r| r.id).collect(); + self.breach_start + .write() + .await + .retain(|id, _| live.contains(id)); + self.baseline_cache + .write() + .await + .retain(|id, _| live.contains(id)); + for rule in rules { let rule_id = rule.id; // A single rule's failure must never abort the loop. @@ -233,9 +349,35 @@ impl MetricAlertEvaluator { // Decode the typed detector. A corrupt blob fails this rule's cycle (the // outer loop logs and continues) rather than evaluating it incorrectly. let config = DetectionConfig::from_value(&rule.detection_config)?; - let breaching = match &config { - DetectionConfig::Static(p) => p.comparator.breaches(value, p.threshold), - // Non-static detectors are typed/schema-present but not yet evaluable + let (breaching, fire_details) = match &config { + DetectionConfig::Static(p) => ( + p.comparator.breaches(value, p.threshold), + FireDetails::static_breach(&rule, value, p), + ), + DetectionConfig::Anomaly(p) => { + match self + .anomaly_eval(&rule, latest.bucket, value, p, now) + .await? + { + Some(ev) => { + let details = FireDetails::anomaly_breach(&rule, value, p, &ev); + (ev.breaching, details) + } + // Insufficient/degenerate baseline: preserve state, neither + // fire nor resolve (no spurious alerts on thin history). + None => { + if let Err(e) = self + .alert_service + .persist_evaluation(rule.id, &rule.last_state, rule.last_value, now) + .await + { + warn!(rule_id = rule.id, error = %e, "Failed to persist insufficient-baseline evaluation"); + } + return Ok(()); + } + } + } + // Other detectors are typed/schema-present but not yet evaluable // (creation is rejected by the service). Defensive guard for any rule // that predates support: preserve state and skip without firing. other => { @@ -275,7 +417,7 @@ impl MetricAlertEvaluator { AlertTransition::StayOk | AlertTransition::StartBreach => "ok", AlertTransition::StayFiring => "firing", AlertTransition::FireNow => { - self.fire(&rule, value, &config).await; + self.fire(&rule, fire_details).await; "firing" } AlertTransition::Resolve => { @@ -295,14 +437,125 @@ impl MetricAlertEvaluator { Ok(()) } - /// Fire an alarm via the reused alarm system and remember the alarm id. - async fn fire(&self, rule: &AlertRule, value: f64, config: &DetectionConfig) { - // The condition string + threshold come from the typed detector. Only - // static rules fire today; the fallback keeps the match exhaustive. - let (comparator, threshold) = match config { - DetectionConfig::Static(p) => (p.comparator.symbol(), p.threshold), - _ => ("?", f64::NAN), + /// Evaluate an anomaly rule's current `value` against a baseline band. + /// + /// Returns `Ok(None)` when the baseline is insufficient or degenerate + /// (too few samples even after the seasonal→global fallback, or a flat band) + /// — the caller then preserves state rather than firing. The band is the + /// robust median+MAD of the lookback buckets in the scored point's seasonal + /// cell, computed through the SAME aggregation as the scored point so counter + /// rates and histogram percentiles compare like-for-like. + async fn anomaly_eval( + &self, + rule: &AlertRule, + scored_ts: DateTime, + value: f64, + p: &AnomalyParams, + now: DateTime, + ) -> Result, crate::error::OtelError> { + let baseline = self.baseline_values(rule, p, now).await?; + let cell = season_cell(scored_ts, p.seasonality); + + // Same seasonal cell as the scored point, and strictly before it (so the + // current — possibly anomalous — window can't contaminate its own band). + let mut samples: Vec = baseline + .iter() + .filter(|(ts, _)| *ts < scored_ts && season_cell(*ts, p.seasonality) == cell) + .map(|(_, v)| *v) + .collect(); + + // Cold-start fallback: too few seasonal samples → use the global band. + if samples.len() < MIN_BASELINE_SAMPLES { + samples = baseline + .iter() + .filter(|(ts, _)| *ts < scored_ts) + .map(|(_, v)| *v) + .collect(); + } + if samples.len() < MIN_BASELINE_SAMPLES { + debug!( + rule_id = rule.id, + samples = samples.len(), + "Anomaly: insufficient baseline, preserving state" + ); + return Ok(None); + } + + let Some((center, scale)) = robust_band(&samples) else { + return Ok(None); }; + if scale < MIN_BAND_SCALE { + // A flat baseline gives no meaningful band; preserve rather than fire + // on every wobble (the reviewer's divide-by-zero / always-firing case). + debug!( + rule_id = rule.id, + "Anomaly: degenerate (flat) baseline, preserving state" + ); + return Ok(None); + } + + let breaching = anomaly_breaches(value, center, scale, p.deviations, p.direction); + Ok(Some(AnomalyEval { + breaching, + center, + scale, + })) + } + + /// Fetch (and cache) the per-rule baseline values over its lookback window. + /// + /// Cached for `BASELINE_REFRESH_SECS` so the hot tick scores against the + /// cached buckets instead of re-querying weeks of data. Values are mapped + /// through `value_for_rule` so each baseline bucket is the same quantity as + /// the scored point (rate for counters, percentile for histograms). + async fn baseline_values( + &self, + rule: &AlertRule, + p: &AnomalyParams, + now: DateTime, + ) -> Result, f64)>, crate::error::OtelError> { + { + let cache = self.baseline_cache.read().await; + if let Some(c) = cache.get(&rule.id) { + if c.fetched_at.elapsed().as_secs() < BASELINE_REFRESH_SECS { + return Ok(c.values.clone()); + } + } + } + + let lookback_days = p + .baseline_lookback_days + .unwrap_or(DEFAULT_LOOKBACK_DAYS) + .clamp(1, 90); + let aggregation = MetricAggregation::parse(&rule.aggregation); + let query = MetricQuery { + project_id: rule.project_id, + metric_name: Some(rule.metric_name.clone()), + start_time: Some(now - chrono::Duration::days(lookback_days as i64)), + end_time: Some(now), + bucket_interval: Some(format!("{}s", rule.window_secs.max(1))), + limit: None, + aggregation, + ..Default::default() + }; + let buckets = self.otel_service.query_metrics(query).await?; + let values: Vec<(DateTime, f64)> = buckets + .iter() + .map(|b| (b.bucket, value_for_rule(b, aggregation))) + .collect(); + + self.baseline_cache.write().await.insert( + rule.id, + CachedBaseline { + fetched_at: Instant::now(), + values: values.clone(), + }, + ); + Ok(values) + } + + /// Fire an alarm via the reused alarm system and remember the alarm id. + async fn fire(&self, rule: &AlertRule, details: FireDetails) { let request = FireAlarmRequest { project_id: rule.project_id, environment_id: None, @@ -311,23 +564,9 @@ impl MetricAlertEvaluator { service_id: None, alarm_type: AlarmType::DeploymentMetricThreshold, severity: map_severity(&rule.severity), - title: format!("Metric threshold breached: {}", rule.name), - message: format!( - "{} {} is {:.3} (threshold {} {:.3})", - rule.metric_name, rule.aggregation, value, comparator, threshold - ), - metadata: Some(json!({ - "rule_id": rule.id, - "metric_name": rule.metric_name, - "aggregation": rule.aggregation, - "value": value, - "threshold": threshold, - "comparator": comparator, - "detection_kind": config.kind_str(), - "window_secs": rule.window_secs, - "for_duration_secs": rule.for_duration_secs, - "source": "otel_metric_alert", - })), + title: details.title, + message: details.message, + metadata: Some(details.metadata), }; match self.alarm_service.fire_alarm(request).await { Ok(Some(alarm_id)) => { diff --git a/crates/temps-otel/src/services/metric_alert_service.rs b/crates/temps-otel/src/services/metric_alert_service.rs index e2febf1ff..02de92655 100644 --- a/crates/temps-otel/src/services/metric_alert_service.rs +++ b/crates/temps-otel/src/services/metric_alert_service.rs @@ -438,21 +438,24 @@ mod tests { } #[tokio::test] - async fn test_create_anomaly_kind_rejected_until_supported() { - // Anomaly (and other non-static) detectors are typed/schema-present but - // not yet evaluable, so creation is rejected with a Validation error. + async fn test_create_unsupported_kind_rejected() { + // Forecast/outlier/auto_watch detectors are typed/schema-present but not + // yet evaluable, so creation is rejected with a Validation error. + // (Anomaly IS now supported — see the detectors module tests.) let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); let service = MetricAlertService::new(Arc::new(db)); - let anomaly = DetectionConfig::from_value(&serde_json::json!({ "kind": "anomaly" })) - .expect("anomaly config parses"); + let forecast = DetectionConfig::from_value(&serde_json::json!({ + "kind": "forecast", "forecast_horizon_secs": 3600, "comparator": "gt", "threshold": 1.0 + })) + .expect("forecast config parses"); let result = service .create( 7, "Rule".to_string(), "http.server.duration".to_string(), "p95".to_string(), - anomaly, + forecast, 300, 120, "warning".to_string(), diff --git a/crates/temps-otel/src/storage/clickhouse/mod.rs b/crates/temps-otel/src/storage/clickhouse/mod.rs index 4eefa3811..add07ff79 100644 --- a/crates/temps-otel/src/storage/clickhouse/mod.rs +++ b/crates/temps-otel/src/storage/clickhouse/mod.rs @@ -782,27 +782,46 @@ pub(crate) fn escape_like_pattern(pattern: &str) -> String { pub(crate) fn translate_bucket_interval(interval: &str) -> String { const DEFAULT: &str = "INTERVAL 1 HOUR"; let trimmed = interval.trim(); + // Accept two shapes: + // - space-separated " " (e.g. "5 minutes", "1 hour"), and + // - compact "" (e.g. "300s", "5m", "1h") which the evaluator + // and SDK emit via `format!("{}s", secs)`. Without the compact form, + // "300s" failed to parse and silently fell back to 1-hour buckets — + // coarsening every windowed query (static + anomaly baseline). let mut parts = trimmed.split_whitespace(); - let (Some(count_str), Some(unit_raw)) = (parts.next(), parts.next()) else { - return DEFAULT.to_string(); + let (count_str, unit_raw): (String, String) = match (parts.next(), parts.next()) { + (Some(count), Some(unit)) => { + // Reject anything with extra tokens (e.g. "1 hour; DROP"). + if parts.next().is_some() { + return DEFAULT.to_string(); + } + (count.to_string(), unit.to_string()) + } + (Some(single), None) => { + // Compact form: split leading ASCII digits from the trailing unit. + let split = single + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(single.len()); + if split == 0 || split == single.len() { + return DEFAULT.to_string(); + } + (single[..split].to_string(), single[split..].to_string()) + } + _ => return DEFAULT.to_string(), }; - // Reject anything with extra tokens (e.g. "1 hour; DROP"). - if parts.next().is_some() { - return DEFAULT.to_string(); - } let Ok(count) = count_str.parse::() else { return DEFAULT.to_string(); }; if count == 0 || count > 100_000 { return DEFAULT.to_string(); } - // Normalise the unit (accept singular and plural) to a fixed keyword. + // Normalise the unit (singular/plural and single-letter) to a fixed keyword. let unit = match unit_raw.to_ascii_lowercase().as_str() { - "second" | "seconds" | "sec" | "secs" => "SECOND", - "minute" | "minutes" | "min" | "mins" => "MINUTE", - "hour" | "hours" | "hr" | "hrs" => "HOUR", - "day" | "days" => "DAY", - "week" | "weeks" => "WEEK", + "second" | "seconds" | "sec" | "secs" | "s" => "SECOND", + "minute" | "minutes" | "min" | "mins" | "m" => "MINUTE", + "hour" | "hours" | "hr" | "hrs" | "h" => "HOUR", + "day" | "days" | "d" => "DAY", + "week" | "weeks" | "w" => "WEEK", _ => return DEFAULT.to_string(), }; format!("INTERVAL {count} {unit}") @@ -3324,6 +3343,16 @@ mod tests { assert_eq!(translate_bucket_interval("2 weeks"), "INTERVAL 2 WEEK"); // Singular + abbreviations. assert_eq!(translate_bucket_interval("15 min"), "INTERVAL 15 MINUTE"); + // Compact (no-space) form, as emitted by `format!("{}s", secs)`. + assert_eq!(translate_bucket_interval("300s"), "INTERVAL 300 SECOND"); + assert_eq!(translate_bucket_interval("5m"), "INTERVAL 5 MINUTE"); + assert_eq!(translate_bucket_interval("1h"), "INTERVAL 1 HOUR"); + assert_eq!(translate_bucket_interval("2d"), "INTERVAL 2 DAY"); + assert_eq!(translate_bucket_interval("1w"), "INTERVAL 1 WEEK"); + // Compact garbage still falls back to the safe default. + assert_eq!(translate_bucket_interval("300"), "INTERVAL 1 HOUR"); + assert_eq!(translate_bucket_interval("abc"), "INTERVAL 1 HOUR"); + assert_eq!(translate_bucket_interval("5x"), "INTERVAL 1 HOUR"); } #[test] diff --git a/web/src/components/metrics/alert-format.tsx b/web/src/components/metrics/alert-format.tsx index cbc96ce8f..161d64403 100644 --- a/web/src/components/metrics/alert-format.tsx +++ b/web/src/components/metrics/alert-format.tsx @@ -21,6 +21,32 @@ export const SEVERITIES: { value: string; label: string }[] = [ { value: 'critical', label: 'Critical' }, ] +/** Detector kinds the form can author. Only static + anomaly are evaluable. */ +export const DETECTION_KINDS: { value: 'static' | 'anomaly'; label: string }[] = + [ + { value: 'static', label: 'Static threshold' }, + { value: 'anomaly', label: 'Anomaly (auto baseline)' }, + ] + +/** Anomaly baseline algorithms accepted today (agile/ewma are rejected v1). */ +export const ANOMALY_ALGORITHMS: { value: string; label: string }[] = [ + { value: 'robust', label: 'Robust (seasonal, stable)' }, + { value: 'basic', label: 'Basic (non-seasonal)' }, +] + +export const DIRECTIONS: { value: string; label: string }[] = [ + { value: 'both', label: 'Above or below' }, + { value: 'above', label: 'Above only' }, + { value: 'below', label: 'Below only' }, +] + +export const SEASONALITIES: { value: string; label: string }[] = [ + { value: 'none', label: 'None' }, + { value: 'hourly', label: 'Hourly' }, + { value: 'daily', label: 'Daily' }, + { value: 'weekly', label: 'Weekly' }, +] + export function comparatorSymbol(comparator: string): string { return COMPARATORS.find((c) => c.value === comparator)?.symbol ?? comparator } @@ -39,6 +65,10 @@ export function alertSummary(rule: OtelMetricAlertRuleResponse): string { if (cfg.kind === 'static') { return `${agg} ${rule.metric_name} ${comparatorSymbol(cfg.comparator)} ${cfg.threshold} · ${rule.severity}` } + if (cfg.kind === 'anomaly') { + // e.g. `avg http.server.duration · anomaly ±3σ · warning` + return `${agg} ${rule.metric_name} · anomaly ±${cfg.deviations}σ · ${rule.severity}` + } return `${agg} ${rule.metric_name} · ${cfg.kind} · ${rule.severity}` } diff --git a/web/src/pages/MetricAlertForm.tsx b/web/src/pages/MetricAlertForm.tsx index 2dc40593d..9d1dfeb70 100644 --- a/web/src/pages/MetricAlertForm.tsx +++ b/web/src/pages/MetricAlertForm.tsx @@ -1,5 +1,10 @@ import { ProjectResponse } from '@/api/client' -import type { Comparator } from '@/api/client' +import type { + AnomalyAlgorithm, + Comparator, + Direction, + Seasonality, +} from '@/api/client' // REGEN: bun run openapi-ts — these builders/types come from the new // /otel/alerts endpoints (operationIds get_alert / create_alert / update_alert). // They are absent from the committed SDK; imported here so the form is @@ -40,7 +45,11 @@ import { Switch } from '@/components/ui/switch' import { AGGREGATIONS } from '@/components/metrics/metric-format' import { AlertStateBadge, + ANOMALY_ALGORITHMS, COMPARATORS, + DETECTION_KINDS, + DIRECTIONS, + SEASONALITIES, SEVERITIES, } from '@/components/metrics/alert-format' import { zodResolver } from '@hookform/resolvers/zod' @@ -65,13 +74,32 @@ const COMPARATOR_VALUES = COMPARATORS.map((c) => c.value) as [ ...string[], ] const SEVERITY_VALUES = SEVERITIES.map((s) => s.value) as [string, ...string[]] +const ALGORITHM_VALUES = ANOMALY_ALGORITHMS.map((a) => a.value) as [ + string, + ...string[], +] +const DIRECTION_VALUES = DIRECTIONS.map((d) => d.value) as [string, ...string[]] +const SEASONALITY_VALUES = SEASONALITIES.map((s) => s.value) as [ + string, + ...string[], +] +// Flat schema: comparator/threshold (static) and the anomaly knobs always carry +// form values (with defaults); `onSubmit` builds the right `detection_config` +// variant from `detection_kind`, and the backend validates the final config. const alertSchema = z.object({ name: z.string().min(1, 'Name is required').max(200, 'Name is too long'), metric_name: z.string().min(1, 'Pick a metric'), aggregation: z.enum(AGGREGATION_VALUES), + detection_kind: z.enum(['static', 'anomaly']), comparator: z.enum(COMPARATOR_VALUES), threshold: z.number({ message: 'Threshold must be a number' }), + algorithm: z.enum(ALGORITHM_VALUES), + deviations: z + .number({ message: 'Sensitivity must be a number' }) + .positive('Sensitivity must be greater than 0'), + direction: z.enum(DIRECTION_VALUES), + seasonality: z.enum(SEASONALITY_VALUES), window_secs: z .number({ message: 'Window must be a number' }) .int() @@ -102,8 +130,13 @@ function emptyDefaults(): AlertFormData { name: '', metric_name: '', aggregation: 'avg', + detection_kind: 'static', comparator: 'gt', threshold: 0, + algorithm: 'robust', + deviations: 3, + direction: 'both', + seasonality: 'none', window_secs: 300, // Must be > 0 (backend + Zod reject 0); default to one eval interval. for_duration_secs: 60, @@ -140,20 +173,37 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { const defaultValues = useMemo(() => { const existing = existingQuery.data if (existing) { - // The form edits a static threshold; pull comparator/threshold out of the - // typed detector union (only `static` is editable here for now). + // Unpack the typed detector union into the flat form fields. const cfg = existing.detection_config const isStatic = cfg.kind === 'static' + const isAnomaly = cfg.kind === 'anomaly' return { name: existing.name, metric_name: existing.metric_name, aggregation: coerce(AGGREGATION_VALUES, existing.aggregation, 'avg'), + detection_kind: isAnomaly ? 'anomaly' : 'static', comparator: coerce( COMPARATOR_VALUES, isStatic ? cfg.comparator : 'gt', 'gt', ), threshold: isStatic ? cfg.threshold : 0, + algorithm: coerce( + ALGORITHM_VALUES, + isAnomaly ? (cfg.algorithm ?? 'robust') : 'robust', + 'robust', + ), + deviations: isAnomaly ? (cfg.deviations ?? 3) : 3, + direction: coerce( + DIRECTION_VALUES, + isAnomaly ? (cfg.direction ?? 'both') : 'both', + 'both', + ), + seasonality: coerce( + SEASONALITY_VALUES, + isAnomaly ? (cfg.seasonality ?? 'none') : 'none', + 'none', + ), window_secs: existing.window_secs, for_duration_secs: existing.for_duration_secs, severity: coerce(SEVERITY_VALUES, existing.severity, 'warning'), @@ -167,6 +217,9 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { resolver: zodResolver(alertSchema), values: defaultValues, }) + // Drives which detector fields are shown. Hidden fields keep their values in + // form state (RHF does not unregister), so `onSubmit` reads the right ones. + const detectionKind = form.watch('detection_kind') const createMutation = useMutation({ ...createAlertMutation(), @@ -199,15 +252,23 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { const isMutating = createMutation.isPending || updateMutation.isPending const onSubmit = async (data: AlertFormData) => { - // The form authors a static threshold detector; wrap the flat comparator + - // threshold into the typed `detection_config` discriminated union. The Zod - // enum guarantees `comparator` is one of the Comparator literals, so the - // narrowing cast is sound. - const detection_config = { - kind: 'static' as const, - comparator: data.comparator as Comparator, - threshold: data.threshold, - } + // Build the typed `detection_config` variant the user chose. The Zod enums + // guarantee the string fields are valid literals, so the narrowing casts are + // sound, and the backend re-validates the final config. + const detection_config = + data.detection_kind === 'anomaly' + ? { + kind: 'anomaly' as const, + algorithm: data.algorithm as AnomalyAlgorithm, + deviations: data.deviations, + direction: data.direction as Direction, + seasonality: data.seasonality as Seasonality, + } + : { + kind: 'static' as const, + comparator: data.comparator as Comparator, + threshold: data.threshold, + } if (isEditing) { await updateMutation.mutateAsync({ path: { id }, @@ -353,57 +414,215 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { />
- {/* Condition: comparator + threshold */} -
- ( - - Comparator - - - - )} - /> - ( - - Threshold + {/* Detector type */} + ( + + Detection + - field.onChange( - e.target.value === '' - ? undefined - : e.target.valueAsNumber, - ) - } - /> + + + - - - )} - /> -
+ + {DETECTION_KINDS.map((d) => ( + + {d.label} + + ))} + + + + {detectionKind === 'anomaly' + ? 'Learns a baseline band from history and fires when the metric deviates from it — no fixed number to pick.' + : 'Fires when the aggregated value crosses a fixed threshold.'} + + + )} + /> + + {detectionKind === 'anomaly' ? ( + <> + {/* Anomaly: algorithm + sensitivity */} +
+ ( + + Algorithm + + + + )} + /> + ( + + Sensitivity (σ) + + + field.onChange( + e.target.value === '' + ? undefined + : e.target.valueAsNumber, + ) + } + /> + + + Band width in standard deviations. Lower = more + sensitive (more alerts); 3 is a good default. + + + + )} + /> +
+ {/* Anomaly: direction + seasonality */} +
+ ( + + Direction + + + + )} + /> + ( + + Seasonality + + + Compare like-for-like times (e.g. weekly = same + weekday & hour). Needs enough history. + + + + )} + /> +
+ + ) : ( + /* Static: comparator + threshold */ +
+ ( + + Comparator + + + + )} + /> + ( + + Threshold + + + field.onChange( + e.target.value === '' + ? undefined + : e.target.valueAsNumber, + ) + } + /> + + + + )} + /> +
+ )} {/* Timing: window + for-duration */}
From 9ec874a5bfa384185854d78d23a54cfef60caa11 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 16:34:50 +0200 Subject: [PATCH 12/56] feat(web): make the anomaly alert form legible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anomaly detection was exposed but not explained. Three fixes: - History/eligibility banner: when a metric is picked for an anomaly rule, the form checks how much history it has and warns if it's under ~14 days — spelling out that the rule will sit at "unknown" and not alert until a baseline can be built (the silent-inert trap). The "Unknown" badge now also carries a tooltip explaining the same. - Sensitivity presets: the raw σ number is replaced with High/Medium/Low presets (2/3/4σ); the exact σ stays available under "Custom". - Advanced disclosure: algorithm + seasonality (sensible defaults most users won't touch) move behind an "Advanced" details block, leaving Sensitivity + Direction as the two primary knobs. --- web/src/components/metrics/alert-format.tsx | 32 ++- web/src/pages/MetricAlertForm.tsx | 275 +++++++++++++------- 2 files changed, 219 insertions(+), 88 deletions(-) diff --git a/web/src/components/metrics/alert-format.tsx b/web/src/components/metrics/alert-format.tsx index 161d64403..7b8effb84 100644 --- a/web/src/components/metrics/alert-format.tsx +++ b/web/src/components/metrics/alert-format.tsx @@ -47,6 +47,32 @@ export const SEASONALITIES: { value: string; label: string }[] = [ { value: 'weekly', label: 'Weekly' }, ] +/** + * Friendly sensitivity presets that map to the raw `deviations` (σ) the backend + * expects. Lower σ = a tighter band = more alerts, so "High sensitivity" is the + * lowest σ. The raw number stays available under "Custom". + */ +export const SENSITIVITY_PRESETS: { + value: string + label: string + deviations: number +}[] = [ + { value: 'high', label: 'High — more alerts (2σ)', deviations: 2 }, + { value: 'medium', label: 'Medium — balanced (3σ)', deviations: 3 }, + { value: 'low', label: 'Low — fewer alerts (4σ)', deviations: 4 }, +] + +/** The preset key matching a σ value, or `'custom'` if it's not a preset. */ +export function presetForDeviations(deviations: number): string { + return ( + SENSITIVITY_PRESETS.find((p) => p.deviations === deviations)?.value ?? + 'custom' + ) +} + +/** Roughly how many days of history an anomaly baseline wants before it alerts. */ +export const ANOMALY_MIN_HISTORY_DAYS = 14 + export function comparatorSymbol(comparator: string): string { return COMPARATORS.find((c) => c.value === comparator)?.symbol ?? comparator } @@ -89,7 +115,11 @@ export function AlertStateBadge({ state }: { state: string }) { ) } return ( - + Unknown ) diff --git a/web/src/pages/MetricAlertForm.tsx b/web/src/pages/MetricAlertForm.tsx index 9d1dfeb70..f235db3e7 100644 --- a/web/src/pages/MetricAlertForm.tsx +++ b/web/src/pages/MetricAlertForm.tsx @@ -13,6 +13,7 @@ import { createAlertMutation, getAlertOptions, listMetricNamesOptions, + queryMetricsOptions, updateAlertMutation, } from '@/api/client/@tanstack/react-query.gen' import { Button } from '@/components/ui/button' @@ -46,15 +47,18 @@ import { AGGREGATIONS } from '@/components/metrics/metric-format' import { AlertStateBadge, ANOMALY_ALGORITHMS, + ANOMALY_MIN_HISTORY_DAYS, COMPARATORS, DETECTION_KINDS, DIRECTIONS, + presetForDeviations, SEASONALITIES, + SENSITIVITY_PRESETS, SEVERITIES, } from '@/components/metrics/alert-format' import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { ArrowLeft } from 'lucide-react' +import { AlertTriangle, ArrowLeft, CheckCircle2 } from 'lucide-react' import { useMemo } from 'react' import { useForm } from 'react-hook-form' import { useNavigate, useParams } from 'react-router-dom' @@ -220,6 +224,41 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { // Drives which detector fields are shown. Hidden fields keep their values in // form state (RHF does not unregister), so `onSubmit` reads the right ones. const detectionKind = form.watch('detection_kind') + const watchedMetric = form.watch('metric_name') + const isAnomaly = detectionKind === 'anomaly' + + // History/eligibility for anomaly rules: a metric needs enough past data for a + // trustworthy baseline, otherwise the rule sits at "unknown" and never alerts. + // Bounds are memoised once so the query key is stable (no refetch loop). + const historyRange = useMemo(() => { + const now = Date.now() + return { + start: new Date(now - 90 * 86_400_000).toISOString(), + end: new Date(now).toISOString(), + } + }, []) + const historyQuery = useQuery({ + ...queryMetricsOptions({ + query: { + project_id: project.id, + metric_name: watchedMetric, + aggregation: 'count', + bucket_interval: '1d', + start_time: historyRange.start, + end_time: historyRange.end, + }, + }), + enabled: isAnomaly && !!watchedMetric, + }) + const historyDays = useMemo(() => { + const buckets = historyQuery.data?.data ?? [] + if (!buckets.length) return 0 + const earliest = Math.min( + ...buckets.map((b) => new Date(b.bucket).getTime()), + ) + return Math.max(0, Math.round((Date.now() - earliest) / 86_400_000)) + }, [historyQuery.data]) + const enoughHistory = historyDays >= ANOMALY_MIN_HISTORY_DAYS const createMutation = useMutation({ ...createAlertMutation(), @@ -446,67 +485,93 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { {detectionKind === 'anomaly' ? ( <> - {/* Anomaly: algorithm + sensitivity */} + {/* History / eligibility — anomaly rules are silently inert + until the metric has enough past data to baseline. */} + {watchedMetric && + !historyQuery.isPending && + (enoughHistory ? ( +
+ + + ~{historyDays} days of history available — enough to + baseline this metric. + +
+ ) : ( +
+ + + Only ~{historyDays} day{historyDays === 1 ? '' : 's'}{' '} + of history for this metric. Anomaly detection needs + about {ANOMALY_MIN_HISTORY_DAYS} days to learn a + baseline — until then this rule stays{' '} + “unknown” and won't alert. You + can still save it; it starts working as history + accrues. + +
+ ))} + + {/* Primary anomaly controls: sensitivity + direction. */}
- ( - - Algorithm - - - - )} - /> ( - - Sensitivity (σ) - - - field.onChange( - e.target.value === '' - ? undefined - : e.target.valueAsNumber, + render={({ field }) => { + const preset = presetForDeviations(field.value) + return ( + + Sensitivity + + {preset === 'custom' && ( + + + field.onChange( + e.target.value === '' + ? undefined + : e.target.valueAsNumber, + ) + } + /> + + )} + + How far from normal counts as an anomaly. Higher = + more alerts. + + + + ) + }} /> -
- {/* Anomaly: direction + seasonality */} -
)} /> - ( - - Seasonality - - - Compare like-for-like times (e.g. weekly = same - weekday & hour). Needs enough history. - - - - )} - />
+ + {/* Advanced: algorithm + seasonality (sensible defaults). */} +
+ + Advanced + +
+ ( + + Algorithm + + + + )} + /> + ( + + Seasonality + + + Compare like-for-like times (e.g. weekly = same + weekday & hour). Needs more history. + + + + )} + /> +
+
) : ( /* Static: comparator + threshold */ From 830a83456946de657d32ba5406e36ac2b4fb4aa8 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 17:01:22 +0200 Subject: [PATCH 13/56] fix(web): edit form dropped changed Select values (aggregation, detection) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editing an alert rule showed empty Aggregation + Detection selects and fell back to the static fields, regardless of the saved rule. Root cause: the form was created with placeholder defaults while the rule loaded, then updated via react-hook-form's `values` prop — and that reset drops Radix Select values that *change* during it (same-value selects like severity were unaffected, masking the bug). Fix: load the rule in a thin parent, then mount the form body once (keyed on the rule id) with the resolved `defaultValues` from the start, so no Select is reset post-mount. Verified: editing the anomaly rule now restores aggregation (max), detector (anomaly), sensitivity, direction, seasonality. --- web/src/pages/MetricAlertForm.tsx | 87 +++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/web/src/pages/MetricAlertForm.tsx b/web/src/pages/MetricAlertForm.tsx index f235db3e7..9d463a08d 100644 --- a/web/src/pages/MetricAlertForm.tsx +++ b/web/src/pages/MetricAlertForm.tsx @@ -3,6 +3,7 @@ import type { AnomalyAlgorithm, Comparator, Direction, + OtelMetricAlertRuleResponse, Seasonality, } from '@/api/client' // REGEN: bun run openapi-ts — these builders/types come from the new @@ -149,17 +150,24 @@ function emptyDefaults(): AlertFormData { } } -export default function MetricAlertForm({ project }: MetricAlertFormProps) { +interface AlertFormBodyProps { + project: ProjectResponse + isEditing: boolean + id: number + /** The rule being edited, already loaded by the parent (null when creating). */ + existing: OtelMetricAlertRuleResponse | null +} + +/** + * The actual form. Mounted only once `existing` is resolved (the parent gates on + * loading and remounts via `key`), so `useForm` initializes with the correct + * values from the start. This is deliberate: initializing with placeholder + * values and then resetting via the `values` prop drops Radix `Select` values + * whose value changes during the reset (e.g. aggregation, detection kind). + */ +function AlertFormBody({ project, isEditing, id, existing }: AlertFormBodyProps) { const navigate = useNavigate() const queryClient = useQueryClient() - const { alertId } = useParams() - const isEditing = !!alertId - const id = Number(alertId) - - const existingQuery = useQuery({ - ...getAlertOptions({ path: { id }, query: { project_id: project.id } }), - enabled: isEditing && Number.isFinite(id), - }) const namesQuery = useQuery({ ...listMetricNamesOptions({ path: { project_id: project.id } }), @@ -175,7 +183,6 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { ) const defaultValues = useMemo(() => { - const existing = existingQuery.data if (existing) { // Unpack the typed detector union into the flat form fields. const cfg = existing.detection_config @@ -215,11 +222,13 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { } } return emptyDefaults() - }, [existingQuery.data]) + }, [existing]) + // `defaultValues` (mount-once), not `values`: this body is remounted via `key` + // when the edited rule loads, so the form never resets a Select post-mount. const form = useForm({ resolver: zodResolver(alertSchema), - values: defaultValues, + defaultValues, }) // Drives which detector fields are shown. Hidden fields keep their values in // form state (RHF does not unregister), so `onSubmit` reads the right ones. @@ -340,18 +349,6 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) { } } - if (isEditing && existingQuery.isPending) { - return ( -
-
- - -
- -
- ) - } - return (
@@ -363,9 +360,7 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) {

{isEditing ? 'Edit alert' : 'New alert'}

- {isEditing && existingQuery.data && ( - - )} + {existing && }

{isEditing @@ -857,3 +852,41 @@ export default function MetricAlertForm({ project }: MetricAlertFormProps) {

) } + +/** + * Loads the edited rule (if any), then mounts {@link AlertFormBody} with a `key` + * so the form is constructed once with the resolved values — never initialized + * empty and reset, which would drop changed Radix `Select` values. + */ +export default function MetricAlertForm({ project }: MetricAlertFormProps) { + const { alertId } = useParams() + const isEditing = !!alertId + const id = Number(alertId) + + const existingQuery = useQuery({ + ...getAlertOptions({ path: { id }, query: { project_id: project.id } }), + enabled: isEditing && Number.isFinite(id), + }) + + if (isEditing && existingQuery.isPending) { + return ( +
+
+ + +
+ +
+ ) + } + + return ( + + ) +} From 0ea7d705daba9dfc5820b4f69f1eaf95937ab255 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 17:11:12 +0200 Subject: [PATCH 14/56] =?UTF-8?q?feat(otel):=20anomaly=20backtest=20?= =?UTF-8?q?=E2=80=94=20"would=20this=20have=20fired=3F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a read-only preview/backtest so an anomaly rule is legible before you save it (and tunable after). The single highest-value affordance for a feature that otherwise fails silently. Backend: - Extract a shared `BandModel` in `detectors` (per-seasonal-cell robust bands + global fallback, built once and queried per timestamp). The evaluator's `anomaly_eval` now uses it too, so the preview can never diverge from what production would actually do. - New `services::anomaly_preview` + `POST /otel/alerts/preview`: replays a metric over a range against the band and returns per-bucket {value, lower, upper, breaching} + breach_count + baseline sufficiency, through the SAME query_metrics aggregation path as the evaluator. Frontend: - `AnomalyBacktest`: for an anomaly rule, calls the endpoint (debounced) and shows "would have fired N× in the last 7 days" plus a chart of the value against its expected band with breach markers, or an explicit "not enough history" state when the baseline is thin. - SDK regen for the new endpoint. Verified live: backtest of the seeded anomaly rule reports 2 breaches over 7d with band [55.5, 144.5] (median 100 ± 3·MAD); UI renders the count + chart. 31+ unit tests pass (incl. new BandModel test); clippy clean. --- crates/temps-otel/src/detectors/mod.rs | 119 ++++++++++ .../src/handlers/metric_alert_handler.rs | 133 +++++++++++ crates/temps-otel/src/handlers/mod.rs | 6 + crates/temps-otel/src/plugin.rs | 4 + .../src/services/anomaly_preview.rs | 126 +++++++++++ .../src/services/metric_alert_evaluator.rs | 81 +++---- crates/temps-otel/src/services/mod.rs | 1 + .../api/client/@tanstack/react-query.gen.ts | 24 +- web/src/api/client/sdk.gen.ts | 25 ++- web/src/api/client/types.gen.ts | 89 ++++++++ .../components/metrics/AnomalyBacktest.tsx | 208 ++++++++++++++++++ web/src/pages/MetricAlertForm.tsx | 18 ++ 12 files changed, 782 insertions(+), 52 deletions(-) create mode 100644 crates/temps-otel/src/services/anomaly_preview.rs create mode 100644 web/src/components/metrics/AnomalyBacktest.tsx diff --git a/crates/temps-otel/src/detectors/mod.rs b/crates/temps-otel/src/detectors/mod.rs index 209939237..65cc13589 100644 --- a/crates/temps-otel/src/detectors/mod.rs +++ b/crates/temps-otel/src/detectors/mod.rs @@ -19,6 +19,8 @@ //! alongside `serde(tag)` in utoipa 5.x). utoipa + hey-api render this as a //! usable `(StaticParams & { kind: 'static' }) | …` TS discriminated union. +use std::collections::HashMap; + use chrono::{DateTime, Datelike, Timelike, Utc}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -37,6 +39,13 @@ fn default_pct_anomalous() -> f64 { /// degenerate by the evaluator (insufficient baseline → preserve state). pub const MIN_BAND_SCALE: f64 = 1e-9; +/// Minimum baseline samples for a trustworthy band. Below this the band is not +/// used (preserve state). Shared by the evaluator and the preview endpoint. +pub const MIN_BASELINE_SAMPLES: usize = 8; + +/// Default anomaly baseline lookback (days) when a rule doesn't set one. +pub const DEFAULT_LOOKBACK_DAYS: i32 = 14; + /// The robust band center + scale for an anomaly detector, from baseline values: /// `(median, MAD · 1.4826)`. The 1.4826 factor makes MAD a consistent estimator /// of the standard deviation for normal data, so `deviations` keeps the same @@ -85,6 +94,80 @@ pub fn anomaly_breaches( } } +/// A precomputed anomaly band: per-seasonal-cell robust `(center, scale)` bands +/// plus a global fallback, built once from baseline values and then queried per +/// timestamp. Shared by the evaluator (scores the current point) and the preview +/// endpoint (scores a whole range) so they can never diverge. +pub struct BandModel { + seasonality: Seasonality, + /// cell id -> (center, scale), only for cells with enough samples. + cells: HashMap, + /// Global band over all baseline values, used when a cell is too thin. + global: Option<(f64, f64)>, + /// Total baseline samples (for the insufficiency check). + pub samples: usize, +} + +impl BandModel { + /// Build from `(timestamp, value)` baseline pairs. A seasonal cell gets its + /// own band only when it has `>= min_cell_samples`; otherwise that cell falls + /// back to the global band (cold-start behaviour). + pub fn from_baseline( + values: &[(DateTime, f64)], + seasonality: Seasonality, + min_cell_samples: usize, + ) -> Self { + let mut by_cell: HashMap> = HashMap::new(); + for (ts, v) in values { + by_cell + .entry(season_cell(*ts, seasonality)) + .or_default() + .push(*v); + } + let mut cells = HashMap::new(); + for (cell, vals) in &by_cell { + if vals.len() >= min_cell_samples { + if let Some(band) = robust_band(vals) { + cells.insert(*cell, band); + } + } + } + let all: Vec = values.iter().map(|(_, v)| *v).collect(); + BandModel { + seasonality, + cells, + global: robust_band(&all), + samples: values.len(), + } + } + + /// The `(center, scale)` band that applies at `ts` — its seasonal cell's band + /// if that cell was dense enough, else the global band. + pub fn band_for(&self, ts: DateTime) -> Option<(f64, f64)> { + let cell = season_cell(ts, self.seasonality); + self.cells.get(&cell).copied().or(self.global) + } + + /// Whether `value` at `ts` breaches the band. `None` when there is no usable + /// band (no baseline, or a degenerate/flat scale) — the caller then preserves + /// state rather than firing. + pub fn breaches( + &self, + ts: DateTime, + value: f64, + deviations: f64, + direction: Direction, + ) -> Option { + let (center, scale) = self.band_for(ts)?; + if scale < MIN_BAND_SCALE { + return None; + } + Some(anomaly_breaches( + value, center, scale, deviations, direction, + )) + } +} + /// A comparable "seasonal cell" id for `ts` under `seasonality`. Baseline buckets /// are filtered to those sharing the scored bucket's cell, so e.g. a Tuesday-2pm /// value is compared only against historical Tuesday-2pm values. @@ -524,6 +607,42 @@ mod tests { assert_eq!(season_cell(t, Seasonality::Weekly), 38); } + #[test] + fn test_band_model() { + use chrono::TimeZone; + // Non-seasonal: one global band over all values. + let vals: Vec<(DateTime, f64)> = (0..12) + .map(|i| { + ( + Utc.with_ymd_and_hms(2026, 6, 1, i, 0, 0).unwrap(), + 100.0 + (i as f64 % 7.0 - 3.0) * 5.0, + ) + }) + .collect(); + let band = BandModel::from_baseline(&vals, Seasonality::None, 8); + assert_eq!(band.samples, 12); + let any_ts = Utc.with_ymd_and_hms(2026, 6, 2, 5, 0, 0).unwrap(); + let (center, scale) = band.band_for(any_ts).expect("global band"); + assert!((center - 100.0).abs() < 10.0); + assert!(scale > 0.0); + // A value far outside the band breaches; one near the center does not. + assert_eq!( + band.breaches(any_ts, 100_000.0, 3.0, Direction::Both), + Some(true) + ); + assert_eq!( + band.breaches(any_ts, center, 3.0, Direction::Both), + Some(false) + ); + + // A flat baseline → degenerate band → breaches returns None (preserve). + let flat: Vec<(DateTime, f64)> = (0..10) + .map(|i| (Utc.with_ymd_and_hms(2026, 6, 1, i, 0, 0).unwrap(), 42.0)) + .collect(); + let flat_band = BandModel::from_baseline(&flat, Seasonality::None, 8); + assert_eq!(flat_band.breaches(any_ts, 99.0, 3.0, Direction::Both), None); + } + #[test] fn test_comparator_breaches() { assert!(Comparator::Gt.breaches(600.0, 500.0)); diff --git a/crates/temps-otel/src/handlers/metric_alert_handler.rs b/crates/temps-otel/src/handlers/metric_alert_handler.rs index 06ab8fdcd..f9d784d35 100644 --- a/crates/temps-otel/src/handlers/metric_alert_handler.rs +++ b/crates/temps-otel/src/handlers/metric_alert_handler.rs @@ -14,9 +14,11 @@ use tracing::{error, warn}; use utoipa::ToSchema; use crate::detectors::DetectionConfig; +use crate::error::OtelError; use crate::handlers::audit::{ OtelMetricAlertCreatedAudit, OtelMetricAlertDeletedAudit, OtelMetricAlertUpdatedAudit, }; +use crate::services::anomaly_preview::compute_anomaly_preview; use crate::OtelAppState; use temps_auth::{permission_guard, RequireAuth}; use temps_core::problemdetails::Problem; @@ -139,6 +141,54 @@ pub struct OtelMetricAlertsResponse { pub total: u64, } +// ── Anomaly preview / backtest ────────────────────────────────────── + +#[derive(Debug, Deserialize, ToSchema)] +pub struct AnomalyPreviewRequest { + pub project_id: i32, + pub metric_name: String, + /// One of `avg|sum|min|max|count|rate|p50|p90|p95|p99`. + pub aggregation: String, + pub window_secs: i32, + /// Must be an `anomaly` detector — the band to backtest. + pub detection_config: DetectionConfig, + /// RFC 3339; defaults to 7 days before `end_time`. + #[schema(example = "2025-10-12T12:15:47Z")] + pub start_time: Option, + /// RFC 3339; defaults to now. + #[schema(example = "2025-10-12T12:15:47Z")] + pub end_time: Option, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct AnomalyPreviewPointResponse { + #[schema(example = "2025-10-12T12:15:47Z")] + pub bucket: String, + pub value: f64, + /// Lower edge of the expected band at this point. + pub lower: f64, + /// Upper edge of the expected band at this point. + pub upper: f64, + pub breaching: bool, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct AnomalyPreviewResponse { + pub points: Vec, + /// How many points in the range would have fired. + pub breach_count: i64, + /// Baseline sample count (drives the `sufficient` flag). + pub baseline_samples: i64, + /// Whether the baseline had enough history for a trustworthy band. + pub sufficient: bool, +} + +fn parse_rfc3339(s: &str) -> Option> { + chrono::DateTime::parse_from_rfc3339(s) + .ok() + .map(|dt| dt.with_timezone(&chrono::Utc)) +} + // ── Handlers ──────────────────────────────────────────────────────── /// List alert rules for a project (newest first, paginated). @@ -375,3 +425,86 @@ pub async fn delete_alert( Ok(StatusCode::NO_CONTENT) } + +/// Backtest an anomaly detector over a time range without saving a rule. +/// +/// Replays the metric against the same band the evaluator would use, returning +/// the per-bucket band + which points would have fired. Powers the form's +/// "would this have fired?" preview and the explorer band overlay. Read-only. +#[utoipa::path( + tag = "OTel", + post, + path = "/otel/alerts/preview", + request_body = AnomalyPreviewRequest, + responses( + (status = 200, description = "Per-bucket band + breach points", body = AnomalyPreviewResponse), + (status = 400, description = "Not an anomaly detector / bad input", body = ProblemDetails), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +pub async fn preview_alert( + RequireAuth(auth): RequireAuth, + State(state): State, + Json(req): Json, +) -> Result { + permission_guard!(auth, OtelRead); + + // Preview only makes sense for a band-based (anomaly) detector. + let params = match &req.detection_config { + DetectionConfig::Anomaly(p) => p.clone(), + other => { + return Err(OtelError::Validation { + message: format!( + "preview is only available for anomaly detectors, not '{}'", + other.kind_str() + ), + } + .into()); + } + }; + + let end = req + .end_time + .as_deref() + .and_then(parse_rfc3339) + .unwrap_or_else(chrono::Utc::now); + let start = req + .start_time + .as_deref() + .and_then(parse_rfc3339) + .unwrap_or_else(|| end - chrono::Duration::days(7)); + + let preview = compute_anomaly_preview( + &state.otel_service, + req.project_id, + &req.metric_name, + &req.aggregation, + req.window_secs, + ¶ms, + start, + end, + ) + .await?; + + let points = preview + .points + .into_iter() + .map(|p| AnomalyPreviewPointResponse { + bucket: p.bucket.to_rfc3339(), + value: p.value, + lower: p.lower, + upper: p.upper, + breaching: p.breaching, + }) + .collect(); + + Ok(Json(AnomalyPreviewResponse { + points, + breach_count: preview.breach_count, + baseline_samples: preview.baseline_samples, + sufficient: preview.sufficient, + })) +} diff --git a/crates/temps-otel/src/handlers/mod.rs b/crates/temps-otel/src/handlers/mod.rs index 1dc6cb2d2..9ce6e9a97 100644 --- a/crates/temps-otel/src/handlers/mod.rs +++ b/crates/temps-otel/src/handlers/mod.rs @@ -100,6 +100,12 @@ pub fn configure_routes() -> Router { "/otel/alerts", get(metric_alert_handler::list_alerts).post(metric_alert_handler::create_alert), ) + // Anomaly backtest/preview. Static segment registered before `{id}` so it + // isn't captured as a rule id (matchit prefers static, but be explicit). + .route( + "/otel/alerts/preview", + post(metric_alert_handler::preview_alert), + ) .route( "/otel/alerts/{id}", get(metric_alert_handler::get_alert) diff --git a/crates/temps-otel/src/plugin.rs b/crates/temps-otel/src/plugin.rs index 14aad5416..2fdbe27e1 100644 --- a/crates/temps-otel/src/plugin.rs +++ b/crates/temps-otel/src/plugin.rs @@ -173,6 +173,7 @@ impl OtelConfig { metric_alert_handler::get_alert, metric_alert_handler::update_alert, metric_alert_handler::delete_alert, + metric_alert_handler::preview_alert, ), components( schemas( @@ -221,6 +222,9 @@ impl OtelConfig { metric_alert_handler::UpdateMetricAlertRequest, metric_alert_handler::OtelMetricAlertRuleResponse, metric_alert_handler::OtelMetricAlertsResponse, + metric_alert_handler::AnomalyPreviewRequest, + metric_alert_handler::AnomalyPreviewResponse, + metric_alert_handler::AnomalyPreviewPointResponse, crate::detectors::DetectionConfig, crate::detectors::StaticParams, crate::detectors::AnomalyParams, diff --git a/crates/temps-otel/src/services/anomaly_preview.rs b/crates/temps-otel/src/services/anomaly_preview.rs new file mode 100644 index 000000000..067db169e --- /dev/null +++ b/crates/temps-otel/src/services/anomaly_preview.rs @@ -0,0 +1,126 @@ +//! Backtest / band preview for anomaly alert rules. +//! +//! Replays a metric over a time range against the SAME [`BandModel`] the +//! evaluator uses, so a "would this have fired?" preview in the UI can never +//! diverge from what production would actually do. The band is built from the +//! lookback ending at the range end and applied to each displayed bucket, mapped +//! through the same `value_for_rule` (so counter rates / histogram percentiles +//! compare like-for-like). + +use chrono::{DateTime, Duration, Utc}; + +use crate::detectors::{AnomalyParams, BandModel, DEFAULT_LOOKBACK_DAYS, MIN_BASELINE_SAMPLES}; +use crate::error::OtelError; +use crate::services::metric_alert_evaluator::value_for_rule; +use crate::services::OtelService; +use crate::types::{MetricAggregation, MetricQuery}; + +/// One point in the backtest: the value, the band around it, and whether it +/// breached. +pub struct PreviewPoint { + pub bucket: DateTime, + pub value: f64, + pub lower: f64, + pub upper: f64, + pub breaching: bool, +} + +/// The result of replaying a metric against an anomaly band. +pub struct AnomalyPreview { + pub points: Vec, + pub breach_count: i64, + pub baseline_samples: i64, + /// Whether the baseline had enough samples to be trustworthy. + pub sufficient: bool, +} + +/// Compute the anomaly band over `[start, end]` plus which points would breach. +#[allow(clippy::too_many_arguments)] +pub async fn compute_anomaly_preview( + otel: &OtelService, + project_id: i32, + metric_name: &str, + aggregation_str: &str, + window_secs: i32, + params: &AnomalyParams, + start: DateTime, + end: DateTime, +) -> Result { + let aggregation = MetricAggregation::parse(aggregation_str); + let interval = format!("{}s", window_secs.max(1)); + + // 1. Baseline over the lookback ending at `end`, mapped through the rule's + // aggregation so each baseline bucket is the same quantity as the points. + let lookback_days = params + .baseline_lookback_days + .unwrap_or(DEFAULT_LOOKBACK_DAYS) + .clamp(1, 90); + let baseline_buckets = otel + .query_metrics(MetricQuery { + project_id, + metric_name: Some(metric_name.to_string()), + start_time: Some(end - Duration::days(lookback_days as i64)), + end_time: Some(end), + bucket_interval: Some(interval.clone()), + limit: None, + aggregation, + ..Default::default() + }) + .await?; + let baseline: Vec<(DateTime, f64)> = baseline_buckets + .iter() + .map(|b| (b.bucket, value_for_rule(b, aggregation))) + .collect(); + let band = BandModel::from_baseline(&baseline, params.seasonality, MIN_BASELINE_SAMPLES); + + // 2. Display series over [start, end], scored against the band. + let display = otel + .query_metrics(MetricQuery { + project_id, + metric_name: Some(metric_name.to_string()), + start_time: Some(start), + end_time: Some(end), + bucket_interval: Some(interval), + limit: None, + aggregation, + ..Default::default() + }) + .await?; + + let mut points = Vec::with_capacity(display.len()); + let mut breach_count = 0i64; + for b in &display { + let value = value_for_rule(b, aggregation); + let (lower, upper, breaching) = match band.band_for(b.bucket) { + Some((center, scale)) => { + let breaching = band + .breaches(b.bucket, value, params.deviations, params.direction) + .unwrap_or(false); + ( + center - params.deviations * scale, + center + params.deviations * scale, + breaching, + ) + } + // No usable band at this point — draw nothing, never breach. + None => (value, value, false), + }; + if breaching { + breach_count += 1; + } + points.push(PreviewPoint { + bucket: b.bucket, + value, + lower, + upper, + breaching, + }); + } + + Ok(AnomalyPreview { + points, + breach_count, + baseline_samples: band.samples as i64, + sufficient: band.samples >= MIN_BASELINE_SAMPLES, + }) +} diff --git a/crates/temps-otel/src/services/metric_alert_evaluator.rs b/crates/temps-otel/src/services/metric_alert_evaluator.rs index fc34c5b1d..a03ba2ea2 100644 --- a/crates/temps-otel/src/services/metric_alert_evaluator.rs +++ b/crates/temps-otel/src/services/metric_alert_evaluator.rs @@ -29,8 +29,8 @@ use tracing::{debug, error, info, warn}; use temps_monitoring::{AlarmService, AlarmSeverity, AlarmType, FireAlarmRequest}; use crate::detectors::{ - anomaly_breaches, robust_band, season_cell, AnomalyParams, DetectionConfig, StaticParams, - MIN_BAND_SCALE, + AnomalyParams, BandModel, DetectionConfig, StaticParams, DEFAULT_LOOKBACK_DAYS, MIN_BAND_SCALE, + MIN_BASELINE_SAMPLES, }; use crate::services::metric_alert_service::MetricAlertService; use crate::services::OtelService; @@ -45,14 +45,6 @@ const EVAL_INTERVAL_SECS: u64 = 30; /// against the cached baseline instead of re-querying the full lookback window. const BASELINE_REFRESH_SECS: u64 = 3600; -/// Default anomaly baseline lookback when a rule doesn't set one. -const DEFAULT_LOOKBACK_DAYS: i32 = 14; - -/// Minimum baseline samples required to compute a trustworthy band. Below this — -/// even after the seasonal→global fallback — the rule preserves state rather -/// than firing off a thin baseline (cold-start safety). -const MIN_BASELINE_SAMPLES: usize = 8; - /// The state transition the evaluator should apply for a single rule this cycle. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AlertTransition { @@ -102,7 +94,7 @@ pub fn evaluate_transition( /// from the explicit bucket layout (`histogram_summary`) — the server's scalar /// `value` for a percentile is the quantile of the synthetic per-point means, /// not the true distribution. All other cases use the server-computed `value`. -fn value_for_rule(latest: &MetricBucket, aggregation: MetricAggregation) -> f64 { +pub(crate) fn value_for_rule(latest: &MetricBucket, aggregation: MetricAggregation) -> f64 { if let MetricAggregation::Quantile(q) = aggregation { if let Some(hs) = &latest.histogram_summary { if !hs.bounds.is_empty() { @@ -453,53 +445,44 @@ impl MetricAlertEvaluator { p: &AnomalyParams, now: DateTime, ) -> Result, crate::error::OtelError> { - let baseline = self.baseline_values(rule, p, now).await?; - let cell = season_cell(scored_ts, p.seasonality); - - // Same seasonal cell as the scored point, and strictly before it (so the - // current — possibly anomalous — window can't contaminate its own band). - let mut samples: Vec = baseline - .iter() - .filter(|(ts, _)| *ts < scored_ts && season_cell(*ts, p.seasonality) == cell) - .map(|(_, v)| *v) + // Baseline strictly before the scored bucket so the current (possibly + // anomalous) window can't contaminate its own band. + let baseline: Vec<(DateTime, f64)> = self + .baseline_values(rule, p, now) + .await? + .into_iter() + .filter(|(ts, _)| *ts < scored_ts) .collect(); - // Cold-start fallback: too few seasonal samples → use the global band. - if samples.len() < MIN_BASELINE_SAMPLES { - samples = baseline - .iter() - .filter(|(ts, _)| *ts < scored_ts) - .map(|(_, v)| *v) - .collect(); - } - if samples.len() < MIN_BASELINE_SAMPLES { + // The SAME BandModel the preview uses, so production and preview agree. + let band = BandModel::from_baseline(&baseline, p.seasonality, MIN_BASELINE_SAMPLES); + if band.samples < MIN_BASELINE_SAMPLES { debug!( rule_id = rule.id, - samples = samples.len(), + samples = band.samples, "Anomaly: insufficient baseline, preserving state" ); return Ok(None); } - - let Some((center, scale)) = robust_band(&samples) else { - return Ok(None); - }; - if scale < MIN_BAND_SCALE { - // A flat baseline gives no meaningful band; preserve rather than fire - // on every wobble (the reviewer's divide-by-zero / always-firing case). - debug!( - rule_id = rule.id, - "Anomaly: degenerate (flat) baseline, preserving state" - ); - return Ok(None); + match band.breaches(scored_ts, value, p.deviations, p.direction) { + Some(breaching) => { + let (center, scale) = band.band_for(scored_ts).unwrap_or((value, 0.0)); + Ok(Some(AnomalyEval { + breaching, + center, + scale, + })) + } + None => { + // Flat baseline → no meaningful band; preserve rather than fire on + // every wobble (the divide-by-zero / always-firing case). + debug!( + rule_id = rule.id, + "Anomaly: degenerate (flat) baseline, preserving state" + ); + Ok(None) + } } - - let breaching = anomaly_breaches(value, center, scale, p.deviations, p.direction); - Ok(Some(AnomalyEval { - breaching, - center, - scale, - })) } /// Fetch (and cache) the per-rule baseline values over its lookback window. diff --git a/crates/temps-otel/src/services/mod.rs b/crates/temps-otel/src/services/mod.rs index 3ad9af35d..43e434dd2 100644 --- a/crates/temps-otel/src/services/mod.rs +++ b/crates/temps-otel/src/services/mod.rs @@ -1,5 +1,6 @@ //! Core OTel services. +pub mod anomaly_preview; pub mod dashboard_service; pub mod health_service; pub mod metric_alert_evaluator; diff --git a/web/src/api/client/@tanstack/react-query.gen.ts b/web/src/api/client/@tanstack/react-query.gen.ts index 64812a63b..99ea4ff07 100644 --- a/web/src/api/client/@tanstack/react-query.gen.ts +++ b/web/src/api/client/@tanstack/react-query.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, getAdminGate, patchAdminGate, listOidcProviders, createOidcProvider, deleteOidcProvider, updateOidcProvider, listOidcRoleMappings, createOidcRoleMapping, testOidcProvider, listOidcProviderUsers, deleteOidcRoleMapping, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, oidcCallback, startOidcLoginBySlug, listPublicProviders, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listServiceSchedules, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, listScheduleServices, attachScheduleServices, detachScheduleService, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, deploymentMetricsGetRange, deploymentMetricsToggle, deploymentMetricsGetLatest, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, getOnDemandCertStatus, listOnDemandCerts, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, updateEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, externalServiceMetricsGetRange, externalServiceMetricsGetAlertRules, externalServiceMetricsCreateAlertRule, externalServiceMetricsDeleteAlertRule, externalServiceMetricsUpdateAlertRule, externalServiceMetricsByDatabase, externalServiceMetricsToggle, externalServiceMetricsGetLatest, externalServiceMetricsStatus, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, nodeMetricsGetRange, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider2, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, listAlerts, createAlert, deleteAlert, getAlert, updateAlert, listDashboards, createDashboard, deleteDashboard, getDashboard, updateDashboard, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, listDeploymentContainerLogs, getDeploymentContainerLogContent, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, listKnownAiAgents, getProxyLogByRequestId, getAiAgentPages, getAiAgentBreakdown, getAiAgentTimeline, getAiPageBreakdown, getAiStatusBreakdown, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, getDiskStatus, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; +import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, getAdminGate, patchAdminGate, listOidcProviders, createOidcProvider, deleteOidcProvider, updateOidcProvider, listOidcRoleMappings, createOidcRoleMapping, testOidcProvider, listOidcProviderUsers, deleteOidcRoleMapping, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, oidcCallback, startOidcLoginBySlug, listPublicProviders, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listServiceSchedules, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, listScheduleServices, attachScheduleServices, detachScheduleService, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, deploymentMetricsGetRange, deploymentMetricsToggle, deploymentMetricsGetLatest, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, getOnDemandCertStatus, listOnDemandCerts, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, updateEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, externalServiceMetricsGetRange, externalServiceMetricsGetAlertRules, externalServiceMetricsCreateAlertRule, externalServiceMetricsDeleteAlertRule, externalServiceMetricsUpdateAlertRule, externalServiceMetricsByDatabase, externalServiceMetricsToggle, externalServiceMetricsGetLatest, externalServiceMetricsStatus, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, nodeMetricsGetRange, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider2, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, listAlerts, createAlert, previewAlert, deleteAlert, getAlert, updateAlert, listDashboards, createDashboard, deleteDashboard, getDashboard, updateDashboard, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, listDeploymentContainerLogs, getDeploymentContainerLogContent, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, listKnownAiAgents, getProxyLogByRequestId, getAiAgentPages, getAiAgentBreakdown, getAiAgentTimeline, getAiPageBreakdown, getAiStatusBreakdown, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, getDiskStatus, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; import { queryOptions, type UseMutationOptions, type DefaultError, infiniteQueryOptions, type InfiniteData } from '@tanstack/react-query'; -import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, GetAdminGateData, PatchAdminGateData, PatchAdminGateResponse, ListOidcProvidersData, CreateOidcProviderData, CreateOidcProviderResponse, DeleteOidcProviderData, DeleteOidcProviderResponse, UpdateOidcProviderData, UpdateOidcProviderResponse, ListOidcRoleMappingsData, CreateOidcRoleMappingData, CreateOidcRoleMappingResponse, TestOidcProviderData, TestOidcProviderResponse, ListOidcProviderUsersData, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageRecentError, GetUsageRecentResponse, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, OidcCallbackData, StartOidcLoginBySlugData, ListPublicProvidersData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListServiceSchedulesData, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, ListScheduleServicesData, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesResponse2 as AttachScheduleServicesResponse, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, DeploymentMetricsGetRangeData, DeploymentMetricsToggleData, DeploymentMetricsGetLatestData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, GetOnDemandCertStatusData, ListOnDemandCertsData, ListOnDemandCertsResponse2 as ListOnDemandCertsResponse, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, UpdateEmailProviderData, UpdateEmailProviderResponse, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponse, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponse, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponse, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsToggleData, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsStatusData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, NodeMetricsGetRangeData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProvider2Data, UpdateEmailProvider2Response, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, ListAlertsData, ListAlertsError, ListAlertsResponse, CreateAlertData, CreateAlertError, CreateAlertResponse, DeleteAlertData, DeleteAlertError, DeleteAlertResponse, GetAlertData, UpdateAlertData, UpdateAlertError, UpdateAlertResponse, ListDashboardsData, ListDashboardsError, ListDashboardsResponse, CreateDashboardData, CreateDashboardError, CreateDashboardResponse, DeleteDashboardData, DeleteDashboardError, DeleteDashboardResponse, GetDashboardData, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, ListDeploymentContainerLogsData, GetDeploymentContainerLogContentData, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, ListKnownAiAgentsData, GetProxyLogByRequestIdData, GetAiAgentPagesData, GetAiAgentBreakdownData, GetAiAgentTimelineData, GetAiPageBreakdownData, GetAiStatusBreakdownData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, GetDiskStatusData, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; +import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, GetAdminGateData, PatchAdminGateData, PatchAdminGateResponse, ListOidcProvidersData, CreateOidcProviderData, CreateOidcProviderResponse, DeleteOidcProviderData, DeleteOidcProviderResponse, UpdateOidcProviderData, UpdateOidcProviderResponse, ListOidcRoleMappingsData, CreateOidcRoleMappingData, CreateOidcRoleMappingResponse, TestOidcProviderData, TestOidcProviderResponse, ListOidcProviderUsersData, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageRecentError, GetUsageRecentResponse, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, OidcCallbackData, StartOidcLoginBySlugData, ListPublicProvidersData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListServiceSchedulesData, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, ListScheduleServicesData, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesResponse2 as AttachScheduleServicesResponse, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, DeploymentMetricsGetRangeData, DeploymentMetricsToggleData, DeploymentMetricsGetLatestData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, GetOnDemandCertStatusData, ListOnDemandCertsData, ListOnDemandCertsResponse2 as ListOnDemandCertsResponse, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, UpdateEmailProviderData, UpdateEmailProviderResponse, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponse, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponse, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponse, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsToggleData, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsStatusData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, NodeMetricsGetRangeData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProvider2Data, UpdateEmailProvider2Response, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, ListAlertsData, ListAlertsError, ListAlertsResponse, CreateAlertData, CreateAlertError, CreateAlertResponse, PreviewAlertData, PreviewAlertError, PreviewAlertResponse, DeleteAlertData, DeleteAlertError, DeleteAlertResponse, GetAlertData, UpdateAlertData, UpdateAlertError, UpdateAlertResponse, ListDashboardsData, ListDashboardsError, ListDashboardsResponse, CreateDashboardData, CreateDashboardError, CreateDashboardResponse, DeleteDashboardData, DeleteDashboardError, DeleteDashboardResponse, GetDashboardData, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, ListDeploymentContainerLogsData, GetDeploymentContainerLogContentData, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, ListKnownAiAgentsData, GetProxyLogByRequestIdData, GetAiAgentPagesData, GetAiAgentBreakdownData, GetAiAgentTimelineData, GetAiPageBreakdownData, GetAiStatusBreakdownData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, GetDiskStatusData, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; import { client } from '../client.gen'; export type QueryKey = [ @@ -7432,6 +7432,26 @@ export const createAlertMutation = (options?: Partial>) return mutationOptions; }; +/** + * Backtest an anomaly detector over a time range without saving a rule. + * Replays the metric against the same band the evaluator would use, returning + * the per-bucket band + which points would have fired. Powers the form's + * "would this have fired?" preview and the explorer band overlay. Read-only. + */ +export const previewAlertMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await previewAlert({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + /** * Delete an alert rule. */ diff --git a/web/src/api/client/sdk.gen.ts b/web/src/api/client/sdk.gen.ts index 8efe650ce..e047d24c6 100644 --- a/web/src/api/client/sdk.gen.ts +++ b/web/src/api/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { type Options as ClientOptions, type Client, type TDataShape, formDataBodySerializer } from './client'; -import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, GetAdminGateData, GetAdminGateResponses, GetAdminGateErrors, PatchAdminGateData, PatchAdminGateResponses, PatchAdminGateErrors, ListOidcProvidersData, ListOidcProvidersResponses, CreateOidcProviderData, CreateOidcProviderResponses, CreateOidcProviderErrors, DeleteOidcProviderData, DeleteOidcProviderResponses, UpdateOidcProviderData, UpdateOidcProviderResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingResponses, TestOidcProviderData, TestOidcProviderResponses, ListOidcProviderUsersData, ListOidcProviderUsersResponses, ListOidcProviderUsersErrors, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponses, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, OidcCallbackData, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, ListPublicProvidersData, ListPublicProvidersResponses, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListServiceSchedulesData, ListServiceSchedulesResponses, ListServiceSchedulesErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, ListScheduleServicesData, ListScheduleServicesResponses, ListScheduleServicesErrors, AttachScheduleServicesData, AttachScheduleServicesResponses, AttachScheduleServicesErrors, DetachScheduleServiceData, DetachScheduleServiceResponses, DetachScheduleServiceErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeResponses, DeploymentMetricsGetRangeErrors, DeploymentMetricsToggleData, DeploymentMetricsToggleResponses, DeploymentMetricsToggleErrors, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetLatestErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, GetOnDemandCertStatusData, GetOnDemandCertStatusResponses, GetOnDemandCertStatusErrors, ListOnDemandCertsData, ListOnDemandCertsResponses, ListOnDemandCertsErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsStatusErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, NodeMetricsGetRangeData, NodeMetricsGetRangeResponses, NodeMetricsGetRangeErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProvider2Data, UpdateEmailProvider2Responses, UpdateEmailProvider2Errors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, ListAlertsData, ListAlertsResponses, ListAlertsErrors, CreateAlertData, CreateAlertResponses, CreateAlertErrors, DeleteAlertData, DeleteAlertResponses, DeleteAlertErrors, GetAlertData, GetAlertResponses, GetAlertErrors, UpdateAlertData, UpdateAlertResponses, UpdateAlertErrors, ListDashboardsData, ListDashboardsResponses, ListDashboardsErrors, CreateDashboardData, CreateDashboardResponses, CreateDashboardErrors, DeleteDashboardData, DeleteDashboardResponses, DeleteDashboardErrors, GetDashboardData, GetDashboardResponses, GetDashboardErrors, UpdateDashboardData, UpdateDashboardResponses, UpdateDashboardErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, ListDeploymentContainerLogsData, ListDeploymentContainerLogsResponses, ListDeploymentContainerLogsErrors, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentResponses, GetDeploymentContainerLogContentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, ListKnownAiAgentsData, ListKnownAiAgentsResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetAiAgentPagesData, GetAiAgentPagesResponses, GetAiAgentPagesErrors, GetAiAgentBreakdownData, GetAiAgentBreakdownResponses, GetAiAgentBreakdownErrors, GetAiAgentTimelineData, GetAiAgentTimelineResponses, GetAiAgentTimelineErrors, GetAiPageBreakdownData, GetAiPageBreakdownResponses, GetAiPageBreakdownErrors, GetAiStatusBreakdownData, GetAiStatusBreakdownResponses, GetAiStatusBreakdownErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, GetDiskStatusData, GetDiskStatusResponses, GetDiskStatusErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; +import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, GetAdminGateData, GetAdminGateResponses, GetAdminGateErrors, PatchAdminGateData, PatchAdminGateResponses, PatchAdminGateErrors, ListOidcProvidersData, ListOidcProvidersResponses, CreateOidcProviderData, CreateOidcProviderResponses, CreateOidcProviderErrors, DeleteOidcProviderData, DeleteOidcProviderResponses, UpdateOidcProviderData, UpdateOidcProviderResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingResponses, TestOidcProviderData, TestOidcProviderResponses, ListOidcProviderUsersData, ListOidcProviderUsersResponses, ListOidcProviderUsersErrors, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponses, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, OidcCallbackData, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, ListPublicProvidersData, ListPublicProvidersResponses, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListServiceSchedulesData, ListServiceSchedulesResponses, ListServiceSchedulesErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, ListScheduleServicesData, ListScheduleServicesResponses, ListScheduleServicesErrors, AttachScheduleServicesData, AttachScheduleServicesResponses, AttachScheduleServicesErrors, DetachScheduleServiceData, DetachScheduleServiceResponses, DetachScheduleServiceErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeResponses, DeploymentMetricsGetRangeErrors, DeploymentMetricsToggleData, DeploymentMetricsToggleResponses, DeploymentMetricsToggleErrors, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetLatestErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, GetOnDemandCertStatusData, GetOnDemandCertStatusResponses, GetOnDemandCertStatusErrors, ListOnDemandCertsData, ListOnDemandCertsResponses, ListOnDemandCertsErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsStatusErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, NodeMetricsGetRangeData, NodeMetricsGetRangeResponses, NodeMetricsGetRangeErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProvider2Data, UpdateEmailProvider2Responses, UpdateEmailProvider2Errors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, ListAlertsData, ListAlertsResponses, ListAlertsErrors, CreateAlertData, CreateAlertResponses, CreateAlertErrors, PreviewAlertData, PreviewAlertResponses, PreviewAlertErrors, DeleteAlertData, DeleteAlertResponses, DeleteAlertErrors, GetAlertData, GetAlertResponses, GetAlertErrors, UpdateAlertData, UpdateAlertResponses, UpdateAlertErrors, ListDashboardsData, ListDashboardsResponses, ListDashboardsErrors, CreateDashboardData, CreateDashboardResponses, CreateDashboardErrors, DeleteDashboardData, DeleteDashboardResponses, DeleteDashboardErrors, GetDashboardData, GetDashboardResponses, GetDashboardErrors, UpdateDashboardData, UpdateDashboardResponses, UpdateDashboardErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, ListDeploymentContainerLogsData, ListDeploymentContainerLogsResponses, ListDeploymentContainerLogsErrors, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentResponses, GetDeploymentContainerLogContentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, ListKnownAiAgentsData, ListKnownAiAgentsResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetAiAgentPagesData, GetAiAgentPagesResponses, GetAiAgentPagesErrors, GetAiAgentBreakdownData, GetAiAgentBreakdownResponses, GetAiAgentBreakdownErrors, GetAiAgentTimelineData, GetAiAgentTimelineResponses, GetAiAgentTimelineErrors, GetAiPageBreakdownData, GetAiPageBreakdownResponses, GetAiPageBreakdownErrors, GetAiStatusBreakdownData, GetAiStatusBreakdownResponses, GetAiStatusBreakdownErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, GetDiskStatusData, GetDiskStatusResponses, GetDiskStatusErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; import { client } from './client.gen'; export type Options = ClientOptions & { @@ -5793,6 +5793,29 @@ export const createAlert = (options: Optio }); }; +/** + * Backtest an anomaly detector over a time range without saving a rule. + * Replays the metric against the same band the evaluator would use, returning + * the per-bucket band + which points would have fired. Powers the form's + * "would this have fired?" preview and the explorer band overlay. Read-only. + */ +export const previewAlert = (options: Options) => { + return (options.client ?? client).post({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/otel/alerts/preview', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + /** * Delete an alert rule. */ diff --git a/web/src/api/client/types.gen.ts b/web/src/api/client/types.gen.ts index a3c7731c1..4ae6ada6f 100644 --- a/web/src/api/client/types.gen.ts +++ b/web/src/api/client/types.gen.ts @@ -787,6 +787,58 @@ export type AnomalyParams = { seasonality?: Seasonality; }; +export type AnomalyPreviewPointResponse = { + breaching: boolean; + bucket: string; + /** + * Lower edge of the expected band at this point. + */ + lower: number; + /** + * Upper edge of the expected band at this point. + */ + upper: number; + value: number; +}; + +export type AnomalyPreviewRequest = { + /** + * One of `avg|sum|min|max|count|rate|p50|p90|p95|p99`. + */ + aggregation: string; + /** + * Must be an `anomaly` detector — the band to backtest. + */ + detection_config: DetectionConfig; + /** + * RFC 3339; defaults to now. + */ + end_time?: string | null; + metric_name: string; + project_id: number; + /** + * RFC 3339; defaults to 7 days before `end_time`. + */ + start_time?: string | null; + window_secs: number; +}; + +export type AnomalyPreviewResponse = { + /** + * Baseline sample count (drives the `sufficient` flag). + */ + baseline_samples: number; + /** + * How many points in the range would have fired. + */ + breach_count: number; + points: Array; + /** + * Whether the baseline had enough history for a trustworthy band. + */ + sufficient: boolean; +}; + export type ApiKeyListResponse = { api_keys: Array; total: number; @@ -29118,6 +29170,43 @@ export type CreateAlertResponses = { export type CreateAlertResponse = CreateAlertResponses[keyof CreateAlertResponses]; +export type PreviewAlertData = { + body: AnomalyPreviewRequest; + path?: never; + query?: never; + url: '/otel/alerts/preview'; +}; + +export type PreviewAlertErrors = { + /** + * Not an anomaly detector / bad input + */ + 400: ProblemDetails; + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type PreviewAlertError = PreviewAlertErrors[keyof PreviewAlertErrors]; + +export type PreviewAlertResponses = { + /** + * Per-bucket band + breach points + */ + 200: AnomalyPreviewResponse; +}; + +export type PreviewAlertResponse = PreviewAlertResponses[keyof PreviewAlertResponses]; + export type DeleteAlertData = { body?: never; path: { diff --git a/web/src/components/metrics/AnomalyBacktest.tsx b/web/src/components/metrics/AnomalyBacktest.tsx new file mode 100644 index 000000000..98da50c54 --- /dev/null +++ b/web/src/components/metrics/AnomalyBacktest.tsx @@ -0,0 +1,208 @@ +// "Would this have fired?" backtest for an anomaly alert rule. +// +// Replays the rule's band over the last 7 days via the /otel/alerts/preview +// endpoint (the SAME band the evaluator uses) and shows how often it would have +// fired, plus a chart of the value against its expected band with breach markers. + +import { previewAlertMutation } from '@/api/client/@tanstack/react-query.gen' +import type { AnomalyPreviewRequest } from '@/api/client' +import { Skeleton } from '@/components/ui/skeleton' +import { formatBucketLabel } from '@/components/metrics/metric-format' +import { useMutation } from '@tanstack/react-query' +import { AlertTriangle, Loader2 } from 'lucide-react' +import { useEffect, useMemo } from 'react' +import { + Area, + CartesianGrid, + ComposedChart, + Line, + ResponsiveContainer, + Scatter, + Tooltip, + XAxis, + YAxis, +} from 'recharts' + +interface AnomalyBacktestProps { + projectId: number + metricName: string + aggregation: string + windowSecs: number + /** The anomaly detector to backtest. */ + detectionConfig: AnomalyPreviewRequest['detection_config'] +} + +function fmtNum(v: number): string { + if (!Number.isFinite(v)) return '—' + const abs = Math.abs(v) + if (abs >= 1000) return v.toLocaleString(undefined, { maximumFractionDigits: 0 }) + if (abs >= 1) return v.toFixed(1) + return v.toFixed(3) +} + +export function AnomalyBacktest({ + projectId, + metricName, + aggregation, + windowSecs, + detectionConfig, +}: AnomalyBacktestProps) { + const preview = useMutation({ + ...previewAlertMutation(), + meta: { errorTitle: 'Backtest failed' }, + }) + + // Re-run (debounced) whenever the rule's inputs change. The body is stable + // JSON so the debounce key only changes on a real edit. + const body = useMemo( + () => ({ + project_id: projectId, + metric_name: metricName, + aggregation, + window_secs: windowSecs, + detection_config: detectionConfig, + }), + [projectId, metricName, aggregation, windowSecs, detectionConfig], + ) + const bodyKey = JSON.stringify(body) + const { mutate } = preview + useEffect(() => { + if (!metricName || !Number.isFinite(windowSecs) || windowSecs <= 0) return + const t = setTimeout(() => mutate({ body }), 500) + return () => clearTimeout(t) + // bodyKey captures every meaningful input; `body`/`mutate` are stable refs. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bodyKey]) + + const chartData = useMemo(() => { + const points = preview.data?.points ?? [] + return points.map((p) => ({ + bucket: p.bucket, + value: p.value, + lower: p.lower, + band: Math.max(0, p.upper - p.lower), + breach: p.breaching ? p.value : null, + })) + }, [preview.data]) + + const header = ( +
+ Backtest — last 7 days + {preview.isPending && ( + + )} +
+ ) + + if (preview.isError) { + return ( +
+ {header} +

+ + Couldn't run the backtest for this metric. +

+
+ ) + } + + if (!preview.data) { + return ( +
+ {header} + +
+ ) + } + + const { breach_count, sufficient } = preview.data + + return ( +
+ {header} + {!sufficient ? ( +

+ + Not enough history to backtest yet — the band needs more data. +

+ ) : ( +

+ {breach_count === 0 ? ( + <> + Would not{' '} + have fired — no points left the band. + + ) : ( + <> + Would have fired{' '} + + {breach_count}× + {' '} + ({chartData.length} points evaluated). + + )} +

+ )} + + {chartData.length > 0 && ( +
+ + + + + + {/* Expected band: invisible base at `lower`, shaded `band` on top. */} + + + + {/* Breach markers. */} + + formatBucketLabel(String(l))} + formatter={(v: number, name: string) => + name === 'band' ? null : [fmtNum(v), name] + } + /> + + +
+ )} +
+ ) +} diff --git a/web/src/pages/MetricAlertForm.tsx b/web/src/pages/MetricAlertForm.tsx index 9d463a08d..cbfcd61b9 100644 --- a/web/src/pages/MetricAlertForm.tsx +++ b/web/src/pages/MetricAlertForm.tsx @@ -45,6 +45,7 @@ import { SearchableSelect } from '@/components/ui/searchable-select' import { Skeleton } from '@/components/ui/skeleton' import { Switch } from '@/components/ui/switch' import { AGGREGATIONS } from '@/components/metrics/metric-format' +import { AnomalyBacktest } from '@/components/metrics/AnomalyBacktest' import { AlertStateBadge, ANOMALY_ALGORITHMS, @@ -662,6 +663,23 @@ function AlertFormBody({ project, isEditing, id, existing }: AlertFormBodyProps) />
+ + {/* "Would this have fired?" — only meaningful with a metric. */} + {watchedMetric && ( + + )} ) : ( /* Static: comparator + threshold */ From 15ad1c34a3e207c15c3e8200ed5bd5ba59b0d0fc Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 17:19:42 +0200 Subject: [PATCH 15/56] feat(web): shade the anomaly band on the metrics explorer For an enabled anomaly rule on the charted metric, backtest its band over the visible range (the same preview endpoint the form uses) and shade the expected [lower, upper] region behind the line via a recharts ReferenceArea. Only shown when the explorer's aggregation matches the rule's (so the band sits on the same scale as the line) and the baseline is sufficient. ThresholdLineChart gains an additive `bands` prop; existing threshold lines are unchanged. --- .../charts/threshold-line-chart.tsx | 39 ++++++++- web/src/pages/MetricsExplorer.tsx | 79 +++++++++++++++++-- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/web/src/components/charts/threshold-line-chart.tsx b/web/src/components/charts/threshold-line-chart.tsx index f9f86f726..1ec8cbdb2 100644 --- a/web/src/components/charts/threshold-line-chart.tsx +++ b/web/src/components/charts/threshold-line-chart.tsx @@ -3,6 +3,7 @@ import { CartesianGrid, Line, LineChart, + ReferenceArea, ReferenceLine, XAxis, YAxis, @@ -34,12 +35,22 @@ export type ThresholdBand = { label?: string } +/** A shaded horizontal band (e.g. an anomaly rule's expected `[lower, upper]`). */ +export type ThresholdBandArea = { + lower: number + upper: number + tone: MetricTone + label?: string +} + interface ThresholdLineChartProps { data: any[] xKey: string series: ThresholdLineSeries /** Horizontal reference lines drawn across the chart for pass/fail bands. */ thresholds?: ThresholdBand[] + /** Shaded horizontal bands (e.g. an anomaly rule's expected range). */ + bands?: ThresholdBandArea[] /** Height of the chart in px. Defaults to 300. */ height?: number /** Format the Y-axis ticks (e.g. "2.5s"). */ @@ -83,6 +94,7 @@ export function ThresholdLineChart({ xKey, series, thresholds = [], + bands = [], height = 300, yTickFormatter, tooltipValueFormatter, @@ -142,8 +154,11 @@ export function ThresholdLineChart({ (m, t) => Math.max(m, t.value), dataMax, ) - const yMax = thresholdMax * 1.1 - const yMin = Math.min(0, dataMin) + // Keep shaded band edges inside the visible Y range too. + const bandMax = bands.reduce((m, b) => Math.max(m, b.upper), thresholdMax) + const bandMin = bands.reduce((m, b) => Math.min(m, b.lower), dataMin) + const yMax = bandMax * 1.1 + const yMin = Math.min(0, bandMin) return ( } /> + {bands.map((b, idx) => ( + + ))} {thresholds.map((t, idx) => ( + (alertsQuery.data?.data ?? []).find( + (r) => + r.enabled && + r.metric_name === metricName && + r.detection_config.kind === 'anomaly', + ) ?? null, + [alertsQuery.data, metricName], + ) + const bandPreview = useMutation({ ...previewAlertMutation() }) + const { mutate: previewBand, reset: resetBand } = bandPreview + const bandKey = anomalyRule + ? `${anomalyRule.id}|${anomalyRule.aggregation}|${anomalyRule.window_secs}|${fromIso}|${toIso}|${JSON.stringify(anomalyRule.detection_config)}` + : '' + useEffect(() => { + if (!anomalyRule) { + resetBand() + return + } + previewBand({ + body: { + project_id: project.id, + metric_name: metricName, + aggregation: anomalyRule.aggregation, + window_secs: anomalyRule.window_secs, + detection_config: anomalyRule.detection_config, + start_time: fromIso, + end_time: toIso, + }, + }) + // bandKey captures every input; mutate/reset are stable refs. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bandKey]) + + const bands = useMemo(() => { + const pts = bandPreview.data?.points ?? [] + // Only overlay when the chart's aggregation matches the rule's — otherwise + // the band (e.g. on the rule's p95) sits on a different scale than the line. + if ( + !anomalyRule || + anomalyRule.aggregation !== aggregation || + pts.length === 0 || + !bandPreview.data?.sufficient + ) + return [] + // The band varies per seasonal bucket; shade a representative (median) band. + const median = (xs: number[]) => + [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)] + return [ + { + lower: median(pts.map((p) => p.lower)), + upper: median(pts.map((p) => p.upper)), + tone: anomalyRule.severity === 'critical' ? 'poor' : 'warn', + label: 'anomaly band', + }, + ] + }, [bandPreview.data, anomalyRule, aggregation]) + const buckets = metricsQuery.data?.data ?? [] // Map the `MetricBucket` rows into the chart point shape. `value` carries the @@ -536,6 +599,7 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { } chartData={chartData} thresholds={thresholds} + bands={bands} />
{latestHist && ( @@ -824,6 +888,7 @@ function MetricChart({ errorMessage, chartData, thresholds, + bands, }: { metricName: string aggLabel: string @@ -832,6 +897,7 @@ function MetricChart({ errorMessage: string chartData: ChartPoint[] thresholds?: ThresholdBand[] + bands?: ThresholdBandArea[] }) { if (!metricName) { return ( @@ -888,6 +954,7 @@ function MetricChart({ xKey="label" series={{ dataKey: 'value', label: aggLabel, tone: 'primary' }} thresholds={thresholds} + bands={bands} height={320} tooltipValueFormatter={(v) => formatMetricValue(v)} yTickFormatter={(v) => formatMetricValue(v)} From 6b068cec1ade36f8b4f190253a68e22ec9279fdb Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 18:41:04 +0200 Subject: [PATCH 16/56] feat(web): Datadog-style "what's wrong" surface for OTEL metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turn the metrics pages from a neutral data browser into a problem surface: the system finds what's wrong and the user reads the answer. - Health header pinned above the tabs (Metrics.tsx): triaged status — firing alerts + active anomalies worst-first, with Alert/Warn/No-data/OK status dots and a firing count on the Alerts tab. Honest coverage states: "all systems healthy" vs "nothing is being watched yet" vs "couldn't load" — never false-green when nothing is monitored. - Status dots + toned line on overview cards (MetricsExplorer) and dashboard tiles (MetricTile): a firing metric reds out of a wall of green instead of looking healthy. Join keyed on (metric_name, aggregation) so a tile only reds for a rule that targets the series it shows. Redundant encoding (dot + tone + chip), never hue alone. - Severity sort: overview grid and the alerts list float worst-first (alert → warn → no-data → ok), so the 24-tile cap and the alert list stop hiding the broken thing. - Shared alert-status model (one cached listAlerts fetch) reused by all three. Fixes two real token bugs found in the review: - Added --success/--warning theme tokens (+ @theme mapping); badge.tsx used bg-success/bg-warning with no token, so the "OK"/healthy badge rendered with no background. - AnomalyBacktest used hsl(var(--primary)) — but the tokens are oklch(), so the band/line/breach dots painted transparent. Use bare var(--chart-1)/ var(--destructive) like the working chart. Frontend-only; typechecks clean. --- .../components/metrics/AnomalyBacktest.tsx | 6 +- web/src/components/metrics/MetricTile.tsx | 29 ++++- .../metrics/MetricsHealthHeader.tsx | 123 ++++++++++++++++++ web/src/components/metrics/alert-format.tsx | 43 ++++++ web/src/components/metrics/alert-status.ts | Bin 0 -> 5313 bytes web/src/globals.css | 16 +++ web/src/pages/MetricAlerts.tsx | 20 ++- web/src/pages/Metrics.tsx | 18 ++- web/src/pages/MetricsExplorer.tsx | 46 ++++++- 9 files changed, 283 insertions(+), 18 deletions(-) create mode 100644 web/src/components/metrics/MetricsHealthHeader.tsx create mode 100644 web/src/components/metrics/alert-status.ts diff --git a/web/src/components/metrics/AnomalyBacktest.tsx b/web/src/components/metrics/AnomalyBacktest.tsx index 98da50c54..6b51e0aae 100644 --- a/web/src/components/metrics/AnomalyBacktest.tsx +++ b/web/src/components/metrics/AnomalyBacktest.tsx @@ -179,19 +179,19 @@ export function AnomalyBacktest({ dataKey="band" stackId="band" stroke="none" - fill="hsl(var(--primary))" + fill="var(--chart-1)" fillOpacity={0.12} isAnimationActive={false} /> {/* Breach markers. */} - + formatBucketLabel(String(l))} diff --git a/web/src/components/metrics/MetricTile.tsx b/web/src/components/metrics/MetricTile.tsx index b2d094e02..f96023f60 100644 --- a/web/src/components/metrics/MetricTile.tsx +++ b/web/src/components/metrics/MetricTile.tsx @@ -12,6 +12,8 @@ import { histogramQuantile, percentileFromAgg, } from './metric-format' +import { StatusDot } from './alert-format' +import { STATUS_META, useAlertStatus } from './alert-status' interface MetricTileProps { project: ProjectResponse @@ -53,6 +55,12 @@ export function MetricTile({ }: MetricTileProps) { const isPercentile = aggregation.startsWith('p') + // Datadog-style: a status dot + a toned line when a rule on this exact + // (metric, aggregation) is firing. Cached `listAlerts` — no extra fetch. + const status = useAlertStatus(project.id).statusFor(metricName, aggregation) + const lineTone = + status === 'alert' ? 'poor' : status === 'warn' ? 'warn' : 'primary' + const q = useQuery({ ...queryMetricsOptions({ query: { @@ -95,11 +103,20 @@ export function MetricTile({ return (
- - {displayTitle} + + {status && ( + + )} + + {displayTitle} +
{isHistogram && ( @@ -136,7 +153,7 @@ export function MetricTile({ series={{ dataKey: 'value', label: aggregationLabel(aggregation), - tone: 'primary', + tone: lineTone, }} height={height} tooltipValueFormatter={(v) => formatMetricValue(v)} diff --git a/web/src/components/metrics/MetricsHealthHeader.tsx b/web/src/components/metrics/MetricsHealthHeader.tsx new file mode 100644 index 000000000..47647e6f8 --- /dev/null +++ b/web/src/components/metrics/MetricsHealthHeader.tsx @@ -0,0 +1,123 @@ +// Datadog-style triaged status surface, pinned above the metrics tabs. +// +// Inverts the page: the system surfaces what's wrong (firing alerts + anomalies, +// worst-first) before any raw chart. Honest about coverage — distinguishes +// "all healthy" from "nothing is being watched yet" so a quiet project never +// shows false-green reassurance. + +import { ProjectResponse } from '@/api/client' +import { Skeleton } from '@/components/ui/skeleton' +import { + ruleStatus, + STATUS_META, + useAlertStatus, + type AlertStatusLevel, +} from '@/components/metrics/alert-status' +import { alertSummary, StatusDot } from '@/components/metrics/alert-format' +import { BellOff, CheckCircle2, ChevronRight } from 'lucide-react' +import { Link, useLocation } from 'react-router-dom' + +const COUNT_ORDER: AlertStatusLevel[] = ['alert', 'warn', 'nodata', 'ok'] + +export function MetricsHealthHeader({ project }: { project: ProjectResponse }) { + const status = useAlertStatus(project.id) + + // The hub mounts at `…/metrics/*`; derive that base for absolute alert links. + const { pathname } = useLocation() + const i = pathname.indexOf('/metrics') + const base = i === -1 ? pathname : pathname.slice(0, i + '/metrics'.length) + + if (status.isLoading) { + // Single fixed-height band — never N rows — so it can't shift layout. + return + } + + // Never imply health when we couldn't read the status. + if (status.isError) { + return ( +
+ Couldn't load alert status. +
+ ) + } + + // No rules → honest "not watching anything" coverage state, not false-green. + if (!status.hasRules) { + return ( +
+ + + Nothing is being watched yet.{' '} + + Create an alert + {' '} + to get notified when a metric goes wrong. + +
+ ) + } + + const { counts, firing, gathering } = status + const allClear = firing.length === 0 + + return ( +
+ {/* Summary bar */} +
+
+ {allClear ? ( + + + All systems healthy + + ) : ( + Needs attention + )} +
+ {COUNT_ORDER.map((lvl) => + counts[lvl] > 0 ? ( + + + {counts[lvl]}{' '} + {STATUS_META[lvl].label} + + ) : null, + )} +
+
+ {allClear && gathering.length > 0 && ( + + {gathering.length} anomaly rule{gathering.length === 1 ? '' : 's'}{' '} + gathering baseline + + )} +
+ + {/* Firing rows — worst first. Only shown when something is wrong. */} + {firing.length > 0 && ( +
    + {firing.map((rule) => ( +
  • + + + + {rule.name} + + {alertSummary(rule)} + + + + +
  • + ))} +
+ )} +
+ ) +} diff --git a/web/src/components/metrics/alert-format.tsx b/web/src/components/metrics/alert-format.tsx index 7b8effb84..32fb02839 100644 --- a/web/src/components/metrics/alert-format.tsx +++ b/web/src/components/metrics/alert-format.tsx @@ -5,7 +5,9 @@ import type { OtelMetricAlertRuleResponse } from '@/api/client' import { Badge } from '@/components/ui/badge' +import { cn } from '@/lib/utils' import { aggregationLabel } from './metric-format' +import { STATUS_META, type AlertStatusLevel } from './alert-status' /** Comparator tokens the backend accepts. */ export const COMPARATORS: { value: string; label: string; symbol: string }[] = [ @@ -98,6 +100,47 @@ export function alertSummary(rule: OtelMetricAlertRuleResponse): string { return `${agg} ${rule.metric_name} · ${cfg.kind} · ${rule.severity}` } +/** + * A small colored status dot (Datadog's status language). The dot is decorative + * — callers must pair it with text/badge so meaning isn't conveyed by hue alone. + * `pulse` adds a quiet ping for live `alert` states. + */ +export function StatusDot({ + level, + pulse = false, + className, + title, +}: { + level: AlertStatusLevel + pulse?: boolean + className?: string + title?: string +}) { + const meta = STATUS_META[level] + return ( + + {pulse && level === 'alert' && ( + + )} + + + ) +} + /** Firing-state badge derived from `rule.last_state` (ok|firing|unknown). */ export function AlertStateBadge({ state }: { state: string }) { if (state === 'firing') { diff --git a/web/src/components/metrics/alert-status.ts b/web/src/components/metrics/alert-status.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ba530b84a11f36655ccf7236df4f20458a8f078 GIT binary patch literal 5313 zcma)A-EteZ74Eg3;yBX@cWr8!Yt@pXahz$!apXv9bD_~l;1VRaTrOAuYndbQwhxd; z?34662f%VE(W*5zxj(@9Ki~Pl(TFbR%IXQJT3dhUyz;H1rJ3lOrpA(=Yg%e=tJ0Bc z?NpW86$hgc{?U1(>8`GvKjXdbj;7j|^C9`xHjr2~-jJf7lvfiogXndwX-9#dE6*BO z*j%gH&-rVj?J(*jU)9TG89O!>+*IZTCZ>;u--YeJkInbxyF4z41xS;C-7-@>Uid1YtXJBhr5 z$J$Zl$<56@0)J2~SM2@+ZR38vVr7mtG___)gC9p~RgKEJ(v2Ve=v9N5my40rP;vP0 zR@-$k)6F1>^6Qm`ymPPXkCJGK+XaGoq1_6C^Ywnx^syc&NUZeERQy z{-m*XzM$8m1N}t;E76b$TuLkaimI#WmQ|f)AfibFp70m0Q(n&26EiinCgcbv+~r zD|rMN)hiYsPK-xu)COHNR+E{|`~L0N`0@SK*%4jn(%8w{1A1oc5YHa)pGdSFL4!q~ z$P-q1MBA*(Lyr8Fs_Od+4LhDuSu5u{97(>I5OJ0ZTi@}T9noU-Kb5UiGRDRyTdM}+ z*b;&W zTdyaFKqfu20Kmz9?c11l&YA^ury?1ge;8m0NPP@u#z0Ij&m2(~^Arnoo9gs}L%0b| zaD@RB3&%`>m<~|^o(gaZ-c`U#{aMwmN*%80O^4+D+Ml)!stAb9n=hqVGQK=|3H^22fz6aN_brSaC6J~S7ADE$TIsGQy&@1S<~YMdiAijHWUB5=vW7n45* z06CF(yx-%U)kZK+eX-aRv^i zg|zjtJ=CxJ)u;(-1>PA`Yt=w@<=$IsZ2u8A2;lhzdT@P%os9fvC_z~+6b<#+=UAEG zu*_VpaJdMuC6{Ia$e8Pf#%p3C6^0hPMO^gc+w{L{tp(Sp*G%iU3xY9G^BaNWUFIhV zAqvaCP03m8xag?#-MFo44rQX5}3Wejwebajhq~j@jqQxr>t0?Sb|Q`3Ghtrkw?HR}{sw(?s?_vYM4d6K*Tm z(3u0)y~#rqqY((U*j2=PIz1&2H=Wb>-_xsC-A>-L$q(=up$z6QkAK_q{3OdH2f@H# zeJI}JU{}E`_*6bwk6;kVi7*`JfIi^a@Jx2y6ntn5wLp|95Dyj)6T)8)gL!@A1}e|# zFcy>GbMM_4?;amJ0jdZUEQi3NHW=&r9S%P@T4&?_yBM-ub_Tz`wb|iN2uH_he+cnU zWV5?Xbg+BC{4CxutpMWTFqFlx^6-`@<%l02d#`(b+Wh9t8_rbJ$+QG zx8X=IeAX=|S>MO;nwxLWwioA0lJOO5Q#67fA%{@0V|~m}Si*z*nC|}iVEZZA{DmL% zpkQ+scZsiHZ9c=!cxnV=2#;|{=f@Z*L31AbO>t7zLRat`=WU0FUL37flA7A%3uyzK87uH1ED3k|;2#lMv86yjFTRxAsB z#Va2l`KPrM2M+WPogSHfESUdd-SOh3TFFr;z-C_HM9`MH59cyJPqKId2tRTp<#K=x a$DPfPu0vHax(_4LZ!5TaASHXlME8Ficf$h! literal 0 HcmV?d00001 diff --git a/web/src/globals.css b/web/src/globals.css index b994a4055..4ba3a73ed 100644 --- a/web/src/globals.css +++ b/web/src/globals.css @@ -34,6 +34,12 @@ /* Vercel red (#e5484d) */ --destructive: oklch(0.627 0.2193 23.03); --destructive-foreground: oklch(0.985 0 0); + /* Status semantics — success (green) + warning (amber), matching the chart + tones. Used by AlertStateBadge (ok/warn) and the metrics health surfaces. */ + --success: oklch(0.723 0.1844 150.31); /* green */ + --success-foreground: oklch(0.145 0 0); + --warning: oklch(0.773 0.1776 69.61); /* amber */ + --warning-foreground: oklch(0.145 0 0); /* gray-200 borders (#eaeaea) — Vercel's signature subtle border */ --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); @@ -111,6 +117,12 @@ /* Vercel red (#e5484d / error) */ --destructive: oklch(0.627 0.2193 23.03); --destructive-foreground: oklch(0.985 0 0); + /* Status semantics — same green/amber as light (chart tones are mode-stable); + dark foreground reads well on both. */ + --success: oklch(0.723 0.1844 150.31); /* green */ + --success-foreground: oklch(0.145 0 0); + --warning: oklch(0.773 0.1776 69.61); /* amber */ + --warning-foreground: oklch(0.145 0 0); /* gray-200 borders (#2e2e2e) */ --border: oklch(0.269 0 0); --input: oklch(0.269 0 0); @@ -179,6 +191,10 @@ --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); diff --git a/web/src/pages/MetricAlerts.tsx b/web/src/pages/MetricAlerts.tsx index 4c54fbcdb..c0e1f9617 100644 --- a/web/src/pages/MetricAlerts.tsx +++ b/web/src/pages/MetricAlerts.tsx @@ -42,7 +42,12 @@ import { import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' -import { AlertStateBadge, alertSummary } from '@/components/metrics/alert-format' +import { + AlertStateBadge, + alertSummary, + StatusDot, +} from '@/components/metrics/alert-format' +import { ruleStatus, statusRank } from '@/components/metrics/alert-status' interface MetricAlertsProps { project: ProjectResponse @@ -73,7 +78,12 @@ export default function MetricAlerts({ project }: MetricAlertsProps) { }, }) - const alerts = alertsQuery.data?.data ?? [] + // Worst-first: firing-critical → firing-warning → unknown → ok, then by name. + const alerts = [...(alertsQuery.data?.data ?? [])].sort( + (a, b) => + statusRank(ruleStatus(a)) - statusRank(ruleStatus(b)) || + a.name.localeCompare(b.name), + ) const goToNew = () => navigate('new') @@ -188,6 +198,12 @@ function AlertRow({ }) { return (
+
diff --git a/web/src/pages/MetricsExplorer.tsx b/web/src/pages/MetricsExplorer.tsx index d31bd9dff..5cec8d83c 100644 --- a/web/src/pages/MetricsExplorer.tsx +++ b/web/src/pages/MetricsExplorer.tsx @@ -24,7 +24,13 @@ import { import { Skeleton } from '@/components/ui/skeleton' import { EmptyState } from '@/components/ui/empty-state' import { ThresholdLineChart } from '@/components/charts/threshold-line-chart' -import { comparatorSymbol } from '@/components/metrics/alert-format' +import { comparatorSymbol, StatusDot } from '@/components/metrics/alert-format' +import { + STATUS_META, + statusRank, + useAlertStatus, + type AlertStatusLevel, +} from '@/components/metrics/alert-status' import { usePageTitle } from '@/hooks/usePageTitle' import { useMutation, useQuery } from '@tanstack/react-query' import { format } from 'date-fns' @@ -652,6 +658,18 @@ function MetricsOverview({ isLoadingNames: boolean totalCount: number }) { + // One alert fetch for the whole grid (cached). Float metrics with a firing + // rule to the top so the 24-cap can't hide what's actually broken. + const alerts = useAlertStatus(project.id) + const sorted = useMemo(() => { + // alert/warn/nodata/ok rank 0–3; metrics with no rule rank last (4). + const rank = (n: string) => { + const s = alerts.statusFor(n, aggregation) + return s ? statusRank(s) : 4 + } + return [...names].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)) + }, [names, alerts, aggregation]) + if (isLoadingNames) { return (
@@ -678,7 +696,7 @@ function MetricsOverview({ ) } - const shown = names.slice(0, OVERVIEW_LIMIT) + const shown = sorted.slice(0, OVERVIEW_LIMIT) return (
@@ -691,6 +709,7 @@ function MetricsOverview({ toIso={toIso} bucketInterval={bucketInterval} aggregation={aggregation} + status={alerts.statusFor(n, aggregation)} onSelect={onSelect} /> ))} @@ -712,6 +731,7 @@ function MetricCard({ toIso, bucketInterval, aggregation, + status, onSelect, }: { project: ProjectResponse @@ -720,8 +740,12 @@ function MetricCard({ toIso: string bucketInterval: string aggregation: AggKind + status: AlertStatusLevel | null onSelect: (name: string) => void }) { + // Tone the line only for active problems; OK/no-data/unwatched stay neutral. + const lineTone = + status === 'alert' ? 'poor' : status === 'warn' ? 'warn' : 'primary' const isPercentile = aggregation.startsWith('p') const q = useQuery({ ...queryMetricsOptions({ @@ -757,8 +781,20 @@ function MetricCard({ className="group flex flex-col gap-2 rounded-lg border border-border bg-card p-3 text-left transition hover:border-primary/40 hover:shadow-sm" >
- - {metricName} + + {status && ( + + )} + + {metricName} + {isHistogram && ( @@ -776,7 +812,7 @@ function MetricCard({ formatMetricValue(v)} yTickFormatter={(v) => formatMetricValue(v)} From 850d94f7b1305a6f61d68c4d8545afae8c1082b8 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 19:48:29 +0200 Subject: [PATCH 17/56] feat(web): deploy markers on the metrics explorer chart (Tier 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Did a deploy cause this?" is the first triage question, and Temps owns the deploy pipeline — a structural edge over Datadog. Overlay deploy events that fall inside the chart's visible window as distinct (purple, dashed) vertical markers, snapped to the nearest bucket (the categorical x-axis can't take a raw timestamp), labelled with the short commit hash. Scoped to the selected environment; timestamps normalised (sec or ms). ThresholdLineChart gains an additive `markers` prop (vertical ReferenceLine); existing lines unchanged. --- .../charts/threshold-line-chart.tsx | 32 +++++++++++ web/src/pages/MetricsExplorer.tsx | 53 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/web/src/components/charts/threshold-line-chart.tsx b/web/src/components/charts/threshold-line-chart.tsx index 1ec8cbdb2..38088b64b 100644 --- a/web/src/components/charts/threshold-line-chart.tsx +++ b/web/src/components/charts/threshold-line-chart.tsx @@ -43,6 +43,16 @@ export type ThresholdBandArea = { label?: string } +/** A vertical event marker (e.g. a deployment) at a categorical x value. */ +export type ThresholdMarker = { + /** Must match a data point's `xKey` value (the categorical x axis). */ + x: string + /** Short label drawn at the top of the line (e.g. a commit hash). */ + label?: string + /** Tooltip text (e.g. the commit message + time). */ + title?: string +} + interface ThresholdLineChartProps { data: any[] xKey: string @@ -51,6 +61,8 @@ interface ThresholdLineChartProps { thresholds?: ThresholdBand[] /** Shaded horizontal bands (e.g. an anomaly rule's expected range). */ bands?: ThresholdBandArea[] + /** Vertical event markers (e.g. deployments) at categorical x values. */ + markers?: ThresholdMarker[] /** Height of the chart in px. Defaults to 300. */ height?: number /** Format the Y-axis ticks (e.g. "2.5s"). */ @@ -95,6 +107,7 @@ export function ThresholdLineChart({ series, thresholds = [], bands = [], + markers = [], height = 300, yTickFormatter, tooltipValueFormatter, @@ -257,6 +270,25 @@ export function ThresholdLineChart({ } /> ))} + {markers.map((m, idx) => ( + + ))} 0, + }) + const deployMarkers = useMemo(() => { + const deploys = deploysQuery.data?.deployments ?? [] + if (chartData.length === 0 || deploys.length === 0) return [] + const fromMs = fromDate.getTime() + const toMs = toDate.getTime() + // Timestamps come back in seconds; normalise to ms. + const toMs10 = (n: number) => (n < 1e12 ? n * 1000 : n) + const bucketMs = chartData.map((p) => new Date(p.bucket).getTime()) + const markers: ThresholdMarker[] = [] + for (const d of deploys) { + const at = d.finished_at ?? d.started_at ?? d.created_at + const ts = toMs10(at) + if (ts < fromMs || ts > toMs) continue + // Snap to the nearest bucket (the categorical x axis can't take a raw ts). + let best = 0 + let bestDiff = Infinity + for (let i = 0; i < bucketMs.length; i++) { + const diff = Math.abs(bucketMs[i] - ts) + if (diff < bestDiff) { + bestDiff = diff + best = i + } + } + markers.push({ + x: formatBucketLabel(chartData[best].bucket), + label: d.commit_hash ? d.commit_hash.slice(0, 7) : 'deploy', + title: d.commit_message ?? undefined, + }) + } + return markers + }, [deploysQuery.data, chartData, fromDate, toDate]) + // Most recent histogram snapshot in range (for the distribution panel). Using // the latest bucket avoids re-summing cumulative snapshots across time. const latestHist = useMemo(() => { @@ -606,6 +655,7 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { chartData={chartData} thresholds={thresholds} bands={bands} + markers={deployMarkers} />
{latestHist && ( @@ -925,6 +975,7 @@ function MetricChart({ chartData, thresholds, bands, + markers, }: { metricName: string aggLabel: string @@ -934,6 +985,7 @@ function MetricChart({ chartData: ChartPoint[] thresholds?: ThresholdBand[] bands?: ThresholdBandArea[] + markers?: ThresholdMarker[] }) { if (!metricName) { return ( @@ -991,6 +1043,7 @@ function MetricChart({ series={{ dataKey: 'value', label: aggLabel, tone: 'primary' }} thresholds={thresholds} bands={bands} + markers={markers} height={320} tooltipValueFormatter={(v) => formatMetricValue(v)} yTickFormatter={(v) => formatMetricValue(v)} From 1d196188d5be07c7347bab533bf9409bb5805ad6 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 20:17:46 +0200 Subject: [PATCH 18/56] feat(web): cross-signal "what changed" strip on the metrics drill-in (Tier 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a metric looks wrong the next question is "what else moved in this same window?" — and Temps owns metrics, deploys, traces, and errors, so the answer shouldn't require re-pivoting four tools by hand. Under the detail chart, a "related signals" strip: - Frames itself by live state: "This metric is firing — see what else changed" when a rule on this (metric, aggregation) is firing, else a neutral "What changed in this window". Reuses the cached listAlerts via useAlertStatus — no extra fetch. - Leads with the deploy answer ("1 deploy landed here — marked on the chart above", or "No deploys in this window — rules out a release"), the literal thing the chart's deploy markers visualise. - Deep-links to Traces and Errors pre-scoped to the SAME window: Traces gets range+env (which it already honours); Errors learns to read `?range=` so the jump actually lands on that window (it widens the metrics-only 6h to 24h rather than ignore the intent). Plus a "Live view" jump to /observe. Honesty: every link carries params the target page genuinely applies, and the strip only states what it knows (deploys) — it never bluffs a trace/error count it didn't fetch. Verified live: firing CPU-anomaly drill-in shows the firing header + "1 deploy", and /errors?range=1h lands on "the last hour". All curated-lucide icons (Network/Bug/Eye/Rocket/ArrowUpRight) confirmed in the runtime bundle — the subset excludes ListTree/Telescope. --- .../components/metrics/MetricCorrelations.tsx | 127 ++++++++++++++++++ web/src/components/projects/ErrorTracking.tsx | 11 +- web/src/pages/MetricsExplorer.tsx | 9 ++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 web/src/components/metrics/MetricCorrelations.tsx diff --git a/web/src/components/metrics/MetricCorrelations.tsx b/web/src/components/metrics/MetricCorrelations.tsx new file mode 100644 index 000000000..6019f41e9 --- /dev/null +++ b/web/src/components/metrics/MetricCorrelations.tsx @@ -0,0 +1,127 @@ +// Cross-signal "what changed" strip for a drilled-in metric. +// +// The correlation moat: Temps owns metrics, deploys, traces, and errors, so when +// a metric looks wrong the operator shouldn't have to re-pivot every other tool +// to the same window by hand. This strip leads with the deploy answer ("did a +// deploy land here?") — which the chart already marks — and offers one-click +// jumps to Traces and Errors pre-scoped to the SAME time window, so the next +// triage step is a click, not a re-filter. +// +// Honesty rules baked in: +// - The deep-links carry `range` (and `env` for traces) that those pages +// actually honour, so "Traces in this window" really is this window. +// - We never invent a count we didn't fetch: the strip states what changed +// (deploys, which we know) and links out for the rest rather than bluffing a +// correlation we haven't computed. + +import type { ProjectResponse } from '@/api/client' +import { Button } from '@/components/ui/button' +import { useAlertStatus } from './alert-status' +import { Link } from 'react-router-dom' +import { ArrowUpRight, Bug, Eye, Network, Rocket } from 'lucide-react' + +/** The relative ranges the explorer offers; a subset is shared with Traces. */ +type TimeRange = '1h' | '6h' | '24h' | '7d' | '30d' + +interface MetricCorrelationsProps { + project: ProjectResponse + metricName: string + /** Aggregation token, so we only flag "firing" for a rule on THIS series. */ + aggregation: string + /** The window the chart is showing — handed straight to the linked pages. */ + timeRange: TimeRange + /** Selected environment id, or null for "all environments". */ + environmentId: number | null + /** Deploy events that fall inside the visible window (already computed). */ + deployCount: number +} + +/** + * A horizontal "related signals" strip rendered under a metric's detail chart. + * Self-resolves whether a rule on this series is firing (to frame the strip as + * an investigation vs. a passive cross-link) — reuses the cached `listAlerts`, + * no extra fetch. + */ +export function MetricCorrelations({ + project, + metricName, + aggregation, + timeRange, + environmentId, + deployCount, +}: MetricCorrelationsProps) { + const status = useAlertStatus(project.id).statusFor(metricName, aggregation) + const firing = status === 'alert' || status === 'warn' + + const base = `/projects/${project.slug}` + const env = environmentId ?? 'all' + // Traces honour both; the errors page honours `range` (and widens 6h→24h + // itself), so pass the metric's range through untouched. + const tracesHref = `${base}/traces?range=${timeRange}&env=${env}` + const errorsHref = `${base}/errors?range=${timeRange}` + const observeHref = `${base}/observe` + + return ( +
+
+
+ + {firing + ? 'This metric is firing — see what else changed' + : 'What changed in this window'} + + + + {deployCount > 0 ? ( + + + {deployCount} deploy{deployCount === 1 ? '' : 's'} + {' '} + landed here — marked on the chart above. + + ) : ( + No deploys in this window — rules out a release. + )} + +
+ +
+ }> + Traces + + }> + Errors + + }> + Live view + +
+
+
+ ) +} + +function CorrelationLink({ + to, + icon, + children, +}: { + to: string + icon: React.ReactNode + children: React.ReactNode +}) { + return ( + + ) +} diff --git a/web/src/components/projects/ErrorTracking.tsx b/web/src/components/projects/ErrorTracking.tsx index f992be71b..45920a3de 100644 --- a/web/src/components/projects/ErrorTracking.tsx +++ b/web/src/components/projects/ErrorTracking.tsx @@ -69,9 +69,18 @@ export function ErrorTracking({ project }: ErrorTrackingProps) { const navigate = useNavigate() const queryClient = useQueryClient() const [searchParams, setSearchParams] = useSearchParams() + // Honour a `?range=` deep-link (e.g. from a firing metric's "what changed" + // strip) so arriving here lands on the same window the operator was looking + // at. The metrics surface also offers `6h`, which error tracking doesn't + // bucket — widen it to 24h rather than silently ignore the intent. const [selectedTimeRange, setSelectedTimeRange] = useState< '1h' | '24h' | '7d' | '30d' - >('24h') + >(() => { + const r = searchParams.get('range') + if (r === '1h' || r === '24h' || r === '7d' || r === '30d') return r + if (r === '6h') return '24h' + return '24h' + }) const [isDsnConfigOpen, setIsDsnConfigOpen] = useState(false) const [statusFilter, setStatusFilter] = useState< 'unresolved' | 'resolved' | 'all' diff --git a/web/src/pages/MetricsExplorer.tsx b/web/src/pages/MetricsExplorer.tsx index edd803e83..e35bfda42 100644 --- a/web/src/pages/MetricsExplorer.tsx +++ b/web/src/pages/MetricsExplorer.tsx @@ -28,6 +28,7 @@ import { Skeleton } from '@/components/ui/skeleton' import { EmptyState } from '@/components/ui/empty-state' import { ThresholdLineChart } from '@/components/charts/threshold-line-chart' import { comparatorSymbol, StatusDot } from '@/components/metrics/alert-format' +import { MetricCorrelations } from '@/components/metrics/MetricCorrelations' import { STATUS_META, statusRank, @@ -658,6 +659,14 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { markers={deployMarkers} />
+ {latestHist && (
From 1e31aa7e46c7804753d6ec46391ea3b041de95be Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 20:59:42 +0200 Subject: [PATCH 19/56] fix(web): keep the project header on one line for long titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A long project name in the breadcrumb switcher (and long crumbs on deep paths like errors/) wrapped the header to two lines because shadcn's BreadcrumbList is flex-wrap + break-words. Force the list to flex-nowrap with a min-w-0 ancestor chain, truncate + responsively cap every crumb (switcher label, intermediate links, and the current-page crumb — not just the switcher, or removing the wrap escape-hatch would overflow the terminal crumb under the action cluster), and clip at the breadcrumb boundary (overflow-hidden) with the right-hand action cluster pinned shrink-0. Verified by measurement: with every crumb forced to a 393px label, the breadcrumb stays 1 line and the header 64px at both 1280px and 375px, each crumb ellipsizes, and the breadcrumb never overlaps the action cluster. --- web/src/components/dashboard/Header.tsx | 35 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/web/src/components/dashboard/Header.tsx b/web/src/components/dashboard/Header.tsx index 49f38cbd5..00acb26a6 100644 --- a/web/src/components/dashboard/Header.tsx +++ b/web/src/components/dashboard/Header.tsx @@ -114,10 +114,12 @@ function ProjectSwitcher({ -
-
- - - - +
+
+ + + + {breadcrumbs.map((item, index) => { const isLast = index === breadcrumbs.length - 1 const isProjectCrumb = @@ -219,7 +221,7 @@ export function Header() { item.href === `/projects/${projectSlug}`) return ( - + {isProjectCrumb ? ( ) : !isLast ? ( - {item.label} + + {item.label} + ) : ( - {item.label} + + {item.label} + )} {!isLast && } @@ -240,7 +249,7 @@ export function Header() {
-
+
{headerActions?.map((action) => ( {action.element} ))} From 8f2dcea3193fe37e0ccaac37a4fe22f51da030d9 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 20:59:56 +0200 Subject: [PATCH 20/56] feat(web): Datadog-style firing status on metric dashboards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make "is anything on fire here?" answerable at a glance. A dashboard's status is the worst alert-rule status across the metrics its tiles plot — derived from the same cached listAlerts the per-tile dots already use, so no extra fetch and the signals can't disagree. The dashboards list shows a pulsing status dot + "N firing" per row; the dashboard view shows a firing badge in the header and a per-section count; "All clear" appears only when tiles are actually watched, nothing when no rule covers the dashboard (no vanity green). Hardened per adversarial review: - rollupStatus counts DISTINCT firing rules (Set of rule id), not tiles, so three tiles plotting one firing metric read "1 firing", not "3" (the name-fallback would otherwise map all three to the same rule). - ruleStatus + the firing/gathering lists now treat a disabled rule as not-firing: the backend freezes a disabled rule's last_state, so without this a monitor switched off mid-firing flashed a false red alarm. Fixed at the source, so the alerts/health surfaces benefit too. - the section "N firing" carries a severity title (no color-only meaning). Verified live: toggling the rule's `enabled` flips the dashboard between "1 firing" and "All clear" while last_state stays frozen-firing. --- web/src/components/metrics/alert-status.ts | Bin 5313 -> 8453 bytes .../components/metrics/dashboard-status.tsx | 105 ++++++++++++++++++ web/src/pages/DashboardView.tsx | 36 +++++- web/src/pages/Dashboards.tsx | 41 ++++++- 4 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 web/src/components/metrics/dashboard-status.tsx diff --git a/web/src/components/metrics/alert-status.ts b/web/src/components/metrics/alert-status.ts index 3ba530b84a11f36655ccf7236df4f20458a8f078..9a0ea01e8243c5e46cce1a8dba2770c45c0b1846 100644 GIT binary patch delta 2647 zcmZ8j&u<$=7!_2atw}{mBB3IXzJ{u{Q`;%(q2RWuN&_?kQmIKrrK+mOyR-Hx>m6%m zHm;jSh67hF3le8;+>mnR!XLqj3-AYU=E(bIX6>|*s(N>4z8~*<@6G(){O*%qyQgOx z8+3=_)QEm2V;ZSEwMtVY@?2T!OG;9m=7SFH4JGwOv@i3RP03)jsSzQR z%zYePWYV)4|XUlVH6r_ zWWc21Dwnw$rMZ|f$#_z00(D236nd@7;C9P+p5mxh0~$42C&p)ic(36xZ;==bv>XVV zs=V2A=`g!QqMHcA0HO{GgNmv#fSzC+$K6tmMlU@dE$a;*=)SO`W&i%c6+dH45DVJal zC8fZQsRvNPbTjGu=P5@h4Ti$eXf^~Q5LW{n!Bn_wPTTaJBj`ZDFQ+Hj36EF^Hz}Y# zu#MOWit(Fr90)f z=igs#)tcQ`jiG$;*2g!TEW31-caRh3{2^B{JZJu>_QTI3ECm+M17`HXgL%vShc6fb z8;yi};GLY%uCzD3z&9IBEMTQJ{3>U5uFa-1s+T^xBLQYuv7});UZV^_i)@FLI;*5w z1@Pv}={rMV8oara#?2$jog!iF*)XrPOF!(2z&VOE$kIj=SSMX-W zuN<|o!%6>s((uo>Z&Lw2BqihPGy?G2-rJY6)iDt zDbM5`lRlSpbo|HJr>EM^>$%WywxDj%YGUR#BdW6noGzD^-kU*ZAUU5{BA+@Etdq>3 zKR!u=83b*FQ;BsCi{)%__;)NNnv4HGC|tbY1GvS@Xx delta 62 zcmV-E0Kxx-LcuApItR0f2%iG8N)lTGv-uV_1Cw_e@RQ9O(vv(LACr;@7?Xe['sections'][number] + +/** Flatten a dashboard's sections to the (metric, aggregation) pairs it plots. */ +export function dashboardTiles( + sections: Section[] | undefined, +): { metricName: string; aggregation?: string }[] { + return (sections ?? []).flatMap((s) => + (s.tiles ?? []).map((t) => ({ + metricName: t.metric_name, + aggregation: t.aggregation, + })), + ) +} + +/** + * Dashboard status pill for the view header. Shows a firing count when a monitor + * on this dashboard is firing, a quiet "All clear" only when tiles are actually + * watched, and nothing when no rule covers the dashboard (honest — no vanity + * green on a dashboard nobody is alerting on). + */ +export function DashboardStatusBadge({ + rollup, + className, +}: { + rollup: StatusRollup + className?: string +}) { + if (rollup.firing > 0 && rollup.level) { + return ( + + {rollup.firing} firing + + ) + } + if (rollup.watched > 0) { + return ( + + + All clear + + ) + } + return null +} + +/** + * Compact inline "N firing" count for a section header / list row summary. + * Renders nothing unless something is firing, so it only ever draws the eye to + * trouble. + */ +export function FiringCount({ + rollup, + className, +}: { + rollup: StatusRollup + className?: string +}) { + if (rollup.firing === 0 || !rollup.level) return null + const tone: Record, string> = { + alert: 'text-destructive', + warn: 'text-warning', + } + return ( + + {rollup.firing} firing + + ) +} diff --git a/web/src/pages/DashboardView.tsx b/web/src/pages/DashboardView.tsx index cddd99a8f..f0a988c64 100644 --- a/web/src/pages/DashboardView.tsx +++ b/web/src/pages/DashboardView.tsx @@ -5,6 +5,15 @@ import { ProjectResponse } from '@/api/client' // always use the generated SDK, never hand-roll fetch). import { getDashboardOptions } from '@/api/client/@tanstack/react-query.gen' import { MetricTile } from '@/components/metrics/MetricTile' +import { + rollupStatus, + useAlertStatus, +} from '@/components/metrics/alert-status' +import { + dashboardTiles, + DashboardStatusBadge, + FiringCount, +} from '@/components/metrics/dashboard-status' import { RANGE_BUCKET, TIME_RANGES, @@ -63,6 +72,13 @@ export default function DashboardView({ project }: DashboardViewProps) { const sections = dashboard?.layout?.sections ?? [] + // Datadog-style firing status for this dashboard, derived from the same + // cached alert-rule fetch the tiles use (no extra round-trip). Rolled up at + // the dashboard level for the header summary and per section for the section + // headers, so "what's on fire" reads at a glance without scanning every tile. + const statusModel = useAlertStatus(project.id) + const rollup = rollupStatus(dashboardTiles(sections), statusModel.rulesFor) + return (
{/* Header */} @@ -77,15 +93,16 @@ export default function DashboardView({ project }: DashboardViewProps) { All dashboards
-
- -

+
+ +

{dashboardQuery.isPending ? ( ) : ( (dashboard?.name ?? 'Dashboard') )}

+ {!dashboardQuery.isPending && }
- - - - - All environments - {activeEnvironments.map((env) => ( - - {env.name} - - ))} - - - )} - -
- {(['1h', '6h', '24h'] as const).map((range) => ( - - ))} -
-
-

- - {envsLoading ? ( -
-
- - -
- -
- ) : activeEnvironments.length === 0 ? ( - - - -

- No active environments. Deploy your project to see metrics. -

-
-
- ) : ( -
- {/* Requests + Response Time */} - - - {/* CPU/Memory/Network metrics are in Environments → Containers */} -
- )} -
- ) -} diff --git a/web/src/pages/ProjectDetail.tsx b/web/src/pages/ProjectDetail.tsx index 407747640..e19d2dd3b 100644 --- a/web/src/pages/ProjectDetail.tsx +++ b/web/src/pages/ProjectDetail.tsx @@ -20,7 +20,6 @@ import { GitSettings, ChangeRepositoryPage } from '@/components/project/settings import { ProjectSpeedInsights } from '@/components/project/ProjectSpeedInsights' import { ProjectStorage } from '@/components/project/ProjectStorage' import { ProjectMonitors } from '@/components/project/ProjectMonitors' -import { ProjectMonitoring } from '@/components/project/ProjectMonitoring' import { MonitorDetail } from '@/components/project/MonitorDetail' import { ErrorTracking } from '@/components/projects/ErrorTracking' import { ErrorTrackingSetup } from '@/components/project/setup/ErrorTrackingSetup' @@ -410,10 +409,6 @@ export function ProjectDetail() { path="monitors/:monitorId" element={} /> - } - /> } From 8ef2bf31ac36f079e11a85a35c013a6c65360c98 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 22:13:45 +0200 Subject: [PATCH 22/56] docs(changelog): add Unreleased entry for OpenTelemetry metrics (#158) The PR's Changelog Check requires CHANGELOG.md to carry an [Unreleased] entry. Document the OTel metrics feature set: explorer, dashboards, alert rules with anomaly detection + backtest, deploy markers, cross-signal links, Datadog-style firing status, the OpenTelemetry/Monitoring nav grouping, the one-line header fix, the disabled-rule firing fix, and the legacy Metrics page removal. --- CHANGELOG.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24f73a885..ee518de21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- +- **OpenTelemetry metrics.** A full OTLP metrics pipeline backed by ClickHouse, + with a Metrics explorer, saved dashboards, and first-class metric alert rules. + Alerts support **anomaly detection** (a robust, seasonal median/MAD baseline) + alongside static thresholds, with sensitivity presets and a **backtest + preview** ("would have fired N times") while you author the rule. Charts + overlay **deploy markers** ("did a deploy cause this?") and a firing metric + links straight across to its traces and errors in the same window + ("what changed"). Datadog-style firing status — triaged dots, severity sort, + and a needs-attention header — surfaces on the metrics overview, the + dashboards list, and the dashboard view (#158). ### Changed -- +- **Project navigation grouped into OpenTelemetry + Monitoring.** The OTel + signals (Observe, Traces, AI Traces, Metrics, Error Tracking) are now grouped + under an **OpenTelemetry** section; operational tools (Uptime, Request Logs, + AI Crawlers) under **Monitoring** (#158). +- **Project header stays on one line.** A long project name in the breadcrumb + switcher (and long crumbs on deep paths) now truncates with an ellipsis + instead of wrapping the header onto two lines (#158). + +### Removed +- **Legacy project "Metrics" (resource monitoring) page.** Superseded by the + OpenTelemetry Metrics surface; its sidebar entry, route, and page component + were removed (#158). ### Fixed -- +- **Disabled alert rules no longer report as "firing".** A rule disabled while + firing kept a frozen `firing` state (the evaluator only scans enabled rules); + the status model now treats a disabled monitor as not-firing everywhere, so + dashboards and the alerts list don't flash a false red alarm (#158). ## [0.1.0-beta.39] - 2026-06-25 From f20f79a187aabbc98c81cfcfa6111b42f5424531 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 22:53:39 +0200 Subject: [PATCH 23/56] fix(otel): qualify service_name in trace WHERE to dodge alias-shadow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filtering traces by service threw ClickHouse Code 184 ILLEGAL_AGGREGATION: the trace-summary SELECT aliases `argMax(service_name, …) AS service_name`, which shadows the raw column, so an unqualified `service_name = ?` in WHERE resolved to the aggregate alias. Qualify it as `spans.service_name` so it binds the per-span column. Fixes both query_trace_summaries (Traces) and query_genai_trace_summaries (AI Traces); the count mirrors are qualified too to keep their filter SQL byte-identical. Not the space in the value — verified against live ClickHouse with "Observability Starter". --- .../temps-otel/src/storage/clickhouse/mod.rs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/crates/temps-otel/src/storage/clickhouse/mod.rs b/crates/temps-otel/src/storage/clickhouse/mod.rs index add07ff79..5c520162a 100644 --- a/crates/temps-otel/src/storage/clickhouse/mod.rs +++ b/crates/temps-otel/src/storage/clickhouse/mod.rs @@ -1183,7 +1183,12 @@ impl OtelStorage for ClickHouseOtelStorage { binds.push(Bv::Str(tid.clone())); } if let Some(ref svc) = query.service_name { - where_parts.push("service_name = ?".to_owned()); + // Qualify with the table: the trace-summary SELECTs alias + // `argMax(service_name) AS service_name`, which shadows the raw + // column, so an unqualified `service_name` in WHERE binds to the + // aggregate (ClickHouse Code 184 ILLEGAL_AGGREGATION). The count + // mirrors qualify too so the filter SQL stays byte-identical. + where_parts.push("spans.service_name = ?".to_owned()); binds.push(Bv::Str(svc.clone())); } if let Some(min_dur) = query.min_duration_ms { @@ -1344,7 +1349,12 @@ impl OtelStorage for ClickHouseOtelStorage { binds.push(Bv::Str(tid.clone())); } if let Some(ref svc) = query.service_name { - where_parts.push("service_name = ?".to_owned()); + // Qualify with the table: the trace-summary SELECTs alias + // `argMax(service_name) AS service_name`, which shadows the raw + // column, so an unqualified `service_name` in WHERE binds to the + // aggregate (ClickHouse Code 184 ILLEGAL_AGGREGATION). The count + // mirrors qualify too so the filter SQL stays byte-identical. + where_parts.push("spans.service_name = ?".to_owned()); binds.push(Bv::Str(svc.clone())); } if let Some(min_dur) = query.min_duration_ms { @@ -1555,7 +1565,12 @@ impl OtelStorage for ClickHouseOtelStorage { let mut binds: Vec = vec![Bv::I32(query.project_id)]; if let Some(ref svc) = query.service_name { - where_parts.push("service_name = ?".to_owned()); + // Qualify with the table: the trace-summary SELECTs alias + // `argMax(service_name) AS service_name`, which shadows the raw + // column, so an unqualified `service_name` in WHERE binds to the + // aggregate (ClickHouse Code 184 ILLEGAL_AGGREGATION). The count + // mirrors qualify too so the filter SQL stays byte-identical. + where_parts.push("spans.service_name = ?".to_owned()); binds.push(Bv::Str(svc.clone())); } if let Some(start) = query.start_time { @@ -1805,7 +1820,12 @@ impl OtelStorage for ClickHouseOtelStorage { let mut binds: Vec = vec![Bv::I32(query.project_id)]; if let Some(ref svc) = query.service_name { - where_parts.push("service_name = ?".to_owned()); + // Qualify with the table: the trace-summary SELECTs alias + // `argMax(service_name) AS service_name`, which shadows the raw + // column, so an unqualified `service_name` in WHERE binds to the + // aggregate (ClickHouse Code 184 ILLEGAL_AGGREGATION). The count + // mirrors qualify too so the filter SQL stays byte-identical. + where_parts.push("spans.service_name = ?".to_owned()); binds.push(Bv::Str(svc.clone())); } if let Some(start) = query.start_time { From 44d6520bc589f57f5195e143a3a829da4dd4b747 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 26 Jun 2026 23:21:02 +0200 Subject: [PATCH 24/56] feat(web): show the anomaly band + breach on the metric chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An orange line told you a metric was anomalous but the chart didn't show why. Datadog-style: overlay the detector's time-varying expected-range band and mark the points that left it. - ThresholdLineChart gains an optional `bandSeries` (LineChart → ComposedChart): two stacked Areas draw the [lower, upper] band behind the line; a stroke-less Line with a custom dot marks only the breaching points in red (recharts' Scatter plots null points, so it can't mark a sparse subset). Existing usages (tiles, web vitals) are unchanged. - Drill-in: backtest the rule's detector with the DISPLAYED aggregation (not the rule's) so the band always tracks the visible line and shows even when you're viewing a different aggregation than the rule alerts on. Per-bucket band values are merged onto the chart points (nearest-timestamp), replacing the old flat, aggregation-gated median band that almost never showed. Verified live: anomtest.cpu drill-in shows the expected band + a red breach dot at the spike; non-anomaly metrics and the web-vitals charts render unchanged. --- .../charts/threshold-line-chart.tsx | 110 +++++++++++++++++- web/src/pages/MetricsExplorer.tsx | 84 ++++++++----- 2 files changed, 161 insertions(+), 33 deletions(-) diff --git a/web/src/components/charts/threshold-line-chart.tsx b/web/src/components/charts/threshold-line-chart.tsx index 38088b64b..a4732e5b5 100644 --- a/web/src/components/charts/threshold-line-chart.tsx +++ b/web/src/components/charts/threshold-line-chart.tsx @@ -1,8 +1,9 @@ import { ReactNode } from 'react' import { + Area, CartesianGrid, + ComposedChart, Line, - LineChart, ReferenceArea, ReferenceLine, XAxis, @@ -53,6 +54,20 @@ export type ThresholdMarker = { title?: string } +/** + * A time-varying "expected range" band (e.g. an anomaly rule's seasonal band), + * read from per-point data keys. `lowerKey` is the band floor and `spanKey` the + * band height (upper − lower), so the chart shades [lower, lower + span] + * (recharts draws a band as a transparent base area + a filled span on top). + * `breachKey`, when set, marks the points that left the band — the anomaly. + */ +export type ThresholdBandSeries = { + lowerKey: string + spanKey: string + breachKey?: string + tone: MetricTone +} + interface ThresholdLineChartProps { data: any[] xKey: string @@ -63,6 +78,8 @@ interface ThresholdLineChartProps { bands?: ThresholdBandArea[] /** Vertical event markers (e.g. deployments) at categorical x values. */ markers?: ThresholdMarker[] + /** Time-varying expected-range band drawn behind the line (anomaly band). */ + bandSeries?: ThresholdBandSeries /** Height of the chart in px. Defaults to 300. */ height?: number /** Format the Y-axis ticks (e.g. "2.5s"). */ @@ -108,6 +125,7 @@ export function ThresholdLineChart({ thresholds = [], bands = [], markers = [], + bandSeries, height = 300, yTickFormatter, tooltipValueFormatter, @@ -170,8 +188,22 @@ export function ThresholdLineChart({ // Keep shaded band edges inside the visible Y range too. const bandMax = bands.reduce((m, b) => Math.max(m, b.upper), thresholdMax) const bandMin = bands.reduce((m, b) => Math.min(m, b.lower), dataMin) - const yMax = bandMax * 1.1 - const yMin = Math.min(0, bandMin) + // A time-varying band (anomaly) can dip below / rise above the line — widen + // the domain to its envelope so the whole band stays on-chart. + let envMax = bandMax + let envMin = bandMin + if (bandSeries) { + for (const p of data) { + const lo = p?.[bandSeries.lowerKey] + const sp = p?.[bandSeries.spanKey] + if (typeof lo === 'number' && typeof sp === 'number') { + envMin = Math.min(envMin, lo) + envMax = Math.max(envMax, lo + sp) + } + } + } + const yMax = envMax * 1.1 + const yMin = Math.min(0, envMin) return ( - @@ -189,6 +221,42 @@ export function ThresholdLineChart({ className="stroke-border" strokeOpacity={0.6} /> + {/* Anomaly "expected range" band: a transparent base at `lower`, with the + filled span (upper − lower) stacked on top — drawn first so it sits + behind the line. Kept as two sibling s (NOT wrapped in a + fragment — recharts only detects cartesian children at the top level). + Tooltip excludes both via tooltipType="none". */} + {bandSeries && ( + + )} + {bandSeries && ( + + )} - + {/* Breach markers: dots at the points that left the band — the anomaly + itself. A stroke-less Line over the VALUE series with a custom dot + that renders only where `breachKey` is set (recharts' Scatter plots + null points at the top, so it can't be used to mark a sparse subset). */} + {bandSeries?.breachKey && ( + { + const breaching = + props?.payload?.[bandSeries.breachKey as string] != null + return breaching ? ( + + ) : ( + + ) + }} + /> + )} + ) } diff --git a/web/src/pages/MetricsExplorer.tsx b/web/src/pages/MetricsExplorer.tsx index e35bfda42..cf6bf614a 100644 --- a/web/src/pages/MetricsExplorer.tsx +++ b/web/src/pages/MetricsExplorer.tsx @@ -11,7 +11,7 @@ import { } from '@/api/client/@tanstack/react-query.gen' import type { ThresholdBand, - ThresholdBandArea, + ThresholdBandSeries, ThresholdMarker, } from '@/components/charts/threshold-line-chart' import { Badge } from '@/components/ui/badge' @@ -326,8 +326,12 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { ) const bandPreview = useMutation({ ...previewAlertMutation() }) const { mutate: previewBand, reset: resetBand } = bandPreview + // Backtest with the DISPLAYED aggregation (not the rule's), so the band always + // tracks the line on screen and shows even when you're viewing a different + // aggregation than the rule alerts on — it's the same detector params, just + // applied to the series you're looking at. const bandKey = anomalyRule - ? `${anomalyRule.id}|${anomalyRule.aggregation}|${anomalyRule.window_secs}|${fromIso}|${toIso}|${JSON.stringify(anomalyRule.detection_config)}` + ? `${anomalyRule.id}|${aggregation}|${anomalyRule.window_secs}|${fromIso}|${toIso}|${JSON.stringify(anomalyRule.detection_config)}` : '' useEffect(() => { if (!anomalyRule) { @@ -338,7 +342,7 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { body: { project_id: project.id, metric_name: metricName, - aggregation: anomalyRule.aggregation, + aggregation, window_secs: anomalyRule.window_secs, detection_config: anomalyRule.detection_config, start_time: fromIso, @@ -349,29 +353,24 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [bandKey]) - const bands = useMemo(() => { - const pts = bandPreview.data?.points ?? [] - // Only overlay when the chart's aggregation matches the rule's — otherwise - // the band (e.g. on the rule's p95) sits on a different scale than the line. + // Datadog-style anomaly overlay: a time-varying expected-range band with the + // breaching points marked. Present only when an anomaly rule covers this + // metric and the backtest had enough history. The per-bucket values are merged + // into the chart data below (chartDataWithBand). + const bandSeries = useMemo(() => { if ( !anomalyRule || - anomalyRule.aggregation !== aggregation || - pts.length === 0 || - !bandPreview.data?.sufficient + !bandPreview.data?.sufficient || + (bandPreview.data?.points?.length ?? 0) === 0 ) - return [] - // The band varies per seasonal bucket; shade a representative (median) band. - const median = (xs: number[]) => - [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)] - return [ - { - lower: median(pts.map((p) => p.lower)), - upper: median(pts.map((p) => p.upper)), - tone: anomalyRule.severity === 'critical' ? 'poor' : 'warn', - label: 'anomaly band', - }, - ] - }, [bandPreview.data, anomalyRule, aggregation]) + return undefined + return { + lowerKey: 'bandLower', + spanKey: 'bandSpan', + breachKey: 'bandBreach', + tone: anomalyRule.severity === 'critical' ? 'poor' : 'warn', + } + }, [anomalyRule, bandPreview.data]) const buckets = metricsQuery.data?.data ?? [] @@ -405,6 +404,35 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { [buckets, isPercentile, aggregation], ) + // Merge the anomaly band onto each chart bucket. The backtest buckets by the + // rule's window, which may not line up 1:1 with the chart's interval, so align + // each chart point to the NEAREST band point by timestamp. `bandBreach` carries + // the value only where the point left the band, so the chart can mark it. + const chartDataWithBand = useMemo(() => { + const pts = bandPreview.data?.points ?? [] + if (!bandSeries || pts.length === 0) return chartData + const bandTs = pts.map((p) => new Date(p.bucket).getTime()) + return chartData.map((d) => { + const t = new Date(d.bucket).getTime() + let best = 0 + let bestDiff = Infinity + for (let i = 0; i < bandTs.length; i++) { + const diff = Math.abs(bandTs[i] - t) + if (diff < bestDiff) { + bestDiff = diff + best = i + } + } + const p = pts[best] + return { + ...d, + bandLower: p.lower, + bandSpan: Math.max(0, p.upper - p.lower), + bandBreach: p.breaching ? d.value : null, + } + }) + }, [chartData, bandPreview.data, bandSeries]) + // ── Deploy markers ── // Overlay deploy events that fall inside the visible window as vertical lines, // snapped to the nearest chart bucket. "Did a deploy cause this?" is the first @@ -653,9 +681,9 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { ? metricsQuery.error.message : 'Failed to load metric series.' } - chartData={chartData} + chartData={chartDataWithBand} thresholds={thresholds} - bands={bands} + bandSeries={bandSeries} markers={deployMarkers} />
@@ -983,7 +1011,7 @@ function MetricChart({ errorMessage, chartData, thresholds, - bands, + bandSeries, markers, }: { metricName: string @@ -993,7 +1021,7 @@ function MetricChart({ errorMessage: string chartData: ChartPoint[] thresholds?: ThresholdBand[] - bands?: ThresholdBandArea[] + bandSeries?: ThresholdBandSeries markers?: ThresholdMarker[] }) { if (!metricName) { @@ -1051,7 +1079,7 @@ function MetricChart({ xKey="label" series={{ dataKey: 'value', label: aggLabel, tone: 'primary' }} thresholds={thresholds} - bands={bands} + bandSeries={bandSeries} markers={markers} height={320} tooltipValueFormatter={(v) => formatMetricValue(v)} From e136056713f6e797e20157cff2303cdd685b62e0 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 08:45:37 +0200 Subject: [PATCH 25/56] feat(web): show the anomaly band on standalone metric tiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the drill-in's anomaly overlay into a reusable useAnomalyBand hook (find the enabled anomaly rule for a metric, backtest its detector over the visible window at the DISPLAYED aggregation, return a ThresholdLineChart bandSeries + a mergeBand that stamps per-bucket band/breach onto chart points). Wire it into MetricTile, so a metric shown standalone — dashboard tiles — gets the same expected-range band + breach markers as the explorer drill-in. Only the metrics that actually have an anomaly rule draw a band; everything else is unchanged (mergeBand is a no-op without a band). --- web/src/components/metrics/MetricTile.tsx | 42 ++++-- .../components/metrics/use-anomaly-band.ts | 138 ++++++++++++++++++ 2 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 web/src/components/metrics/use-anomaly-band.ts diff --git a/web/src/components/metrics/MetricTile.tsx b/web/src/components/metrics/MetricTile.tsx index f96023f60..11049e971 100644 --- a/web/src/components/metrics/MetricTile.tsx +++ b/web/src/components/metrics/MetricTile.tsx @@ -14,6 +14,7 @@ import { } from './metric-format' import { StatusDot } from './alert-format' import { STATUS_META, useAlertStatus } from './alert-status' +import { useAnomalyBand } from './use-anomaly-band' interface MetricTileProps { project: ProjectResponse @@ -61,6 +62,18 @@ export function MetricTile({ const lineTone = status === 'alert' ? 'poor' : status === 'warn' ? 'warn' : 'primary' + // Datadog-style anomaly overlay: the expected-range band + breach markers, + // identical to the explorer drill-in. Only renders when an anomaly rule covers + // this metric and the backtest had enough history. + const { bandSeries, mergeBand } = useAnomalyBand({ + project, + metricName, + aggregation, + fromIso, + toIso, + enabled: metricName.length > 0, + }) + const q = useQuery({ ...queryMetricsOptions({ query: { @@ -82,19 +95,21 @@ export function MetricTile({ const data = useMemo( () => - buckets.map((b) => { - const hs = b.histogram_summary - const value = - isPercentile && hs && hs.bounds.length > 0 - ? histogramQuantile( - hs.bounds, - hs.bucket_counts, - percentileFromAgg(aggregation), - ) - : (b.value ?? b.avg_value) - return { label: formatBucketLabel(b.bucket), value } - }), - [buckets, isPercentile, aggregation], + mergeBand( + buckets.map((b) => { + const hs = b.histogram_summary + const value = + isPercentile && hs && hs.bounds.length > 0 + ? histogramQuantile( + hs.bounds, + hs.bucket_counts, + percentileFromAgg(aggregation), + ) + : (b.value ?? b.avg_value) + return { bucket: b.bucket, label: formatBucketLabel(b.bucket), value } + }), + ), + [buckets, isPercentile, aggregation, mergeBand], ) const latest = data.length ? data[data.length - 1].value : null @@ -155,6 +170,7 @@ export function MetricTile({ label: aggregationLabel(aggregation), tone: lineTone, }} + bandSeries={bandSeries} height={height} tooltipValueFormatter={(v) => formatMetricValue(v)} yTickFormatter={(v) => formatMetricValue(v)} diff --git a/web/src/components/metrics/use-anomaly-band.ts b/web/src/components/metrics/use-anomaly-band.ts new file mode 100644 index 000000000..3ec53cb9e --- /dev/null +++ b/web/src/components/metrics/use-anomaly-band.ts @@ -0,0 +1,138 @@ +// Shared anomaly-band data for any metric chart. +// +// Given a metric + the aggregation/time-window currently on screen, this finds +// the enabled anomaly rule covering the metric and backtests its detector over +// the visible range (the SAME /otel/alerts/preview the evaluator uses), then +// hands back a `bandSeries` descriptor for ThresholdLineChart plus a `mergeBand` +// that stamps the per-bucket band onto the chart points. Used by the explorer +// drill-in, dashboard tiles, and anywhere a single metric is shown — so the +// expected-range band + breach markers look identical everywhere. +// +// The backtest uses the DISPLAYED aggregation (not the rule's) so the band +// always tracks the line you're looking at, even when you view a different +// aggregation than the rule alerts on. + +import { previewAlert, type ProjectResponse } from '@/api/client' +import type { ThresholdBandSeries } from '@/components/charts/threshold-line-chart' +import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' +import { useAlertStatus } from './alert-status' + +interface UseAnomalyBandArgs { + project: ProjectResponse + metricName: string + /** The aggregation token currently displayed (avg|sum|max|pNN|…). */ + aggregation: string + /** Visible window — ISO strings, memoized upstream to keep the query stable. */ + fromIso: string + toIso: string + /** Skip the work entirely (e.g. metric not selected yet). */ + enabled?: boolean +} + +export interface AnomalyBand { + /** Pass to ThresholdLineChart; undefined when there's no band to draw. */ + bandSeries: ThresholdBandSeries | undefined + /** + * Stamp the band onto chart points (keyed on each point's `bucket`). A no-op + * that returns the input unchanged when there's no band. Aligns each chart + * bucket to the NEAREST backtest bucket, since the rule's window may not match + * the chart's interval. + */ + mergeBand: (data: T[]) => T[] +} + +export function useAnomalyBand({ + project, + metricName, + aggregation, + fromIso, + toIso, + enabled = true, +}: UseAnomalyBandArgs): AnomalyBand { + // Reuses the cached listAlerts the status dots already fetch — no extra call. + const { rules } = useAlertStatus(project.id, { enabled }) + const rule = useMemo( + () => + rules.find( + (r) => + r.enabled && + r.metric_name === metricName && + r.detection_config.kind === 'anomaly', + ) ?? null, + [rules, metricName], + ) + + const query = useQuery({ + queryKey: [ + 'anomaly-band', + project.id, + metricName, + aggregation, + fromIso, + toIso, + rule?.id, + rule?.window_secs, + JSON.stringify(rule?.detection_config), + ], + enabled: enabled && !!rule && metricName.length > 0, + queryFn: async () => { + const res = await previewAlert({ + body: { + project_id: project.id, + metric_name: metricName, + aggregation, + window_secs: rule!.window_secs, + detection_config: rule!.detection_config, + start_time: fromIso, + end_time: toIso, + }, + throwOnError: true, + }) + return res.data + }, + }) + + const points = useMemo(() => query.data?.points ?? [], [query.data]) + const sufficient = query.data?.sufficient ?? false + + const bandSeries: ThresholdBandSeries | undefined = + rule && sufficient && points.length > 0 + ? { + lowerKey: 'bandLower', + spanKey: 'bandSpan', + breachKey: 'bandBreach', + tone: rule.severity === 'critical' ? 'poor' : 'warn', + } + : undefined + + const mergeBand = useMemo(() => { + const bandTs = points.map((p) => new Date(p.bucket).getTime()) + return (data: T[]): T[] => { + if (!bandSeries || points.length === 0) return data + return data.map((d) => { + const t = new Date(d.bucket).getTime() + let best = 0 + let bestDiff = Infinity + for (let i = 0; i < bandTs.length; i++) { + const diff = Math.abs(bandTs[i] - t) + if (diff < bestDiff) { + bestDiff = diff + best = i + } + } + const p = points[best] + return { + ...d, + bandLower: p.lower, + bandSpan: Math.max(0, p.upper - p.lower), + bandBreach: p.breaching ? d.value : null, + } + }) + } + // bandSeries is derived from points; points identity is stable per fetch. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [points, bandSeries]) + + return { bandSeries, mergeBand } +} From 4117d8a5035d4b5e3e2641fcdf50555be04c7876 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 08:45:37 +0200 Subject: [PATCH 26/56] feat(web): custom date-range filter in the metrics explorer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "Custom range…" option to the time selector that reveals a date-range picker; the absolute window is driven by ?start/?end in the URL alongside the relative ?range presets (the backend metric query already takes arbitrary start/end). A custom span derives its ClickHouse bucket interval from its width (bucketForSpan), mirroring the preset thresholds. Picking a preset clears the custom range and vice-versa. The anomaly band + deploy markers track the custom window too. --- web/src/pages/MetricsExplorer.tsx | 106 ++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 21 deletions(-) diff --git a/web/src/pages/MetricsExplorer.tsx b/web/src/pages/MetricsExplorer.tsx index cf6bf614a..884de2d00 100644 --- a/web/src/pages/MetricsExplorer.tsx +++ b/web/src/pages/MetricsExplorer.tsx @@ -24,6 +24,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { DateRangePicker } from '@/components/ui/date-range-picker' import { Skeleton } from '@/components/ui/skeleton' import { EmptyState } from '@/components/ui/empty-state' import { ThresholdLineChart } from '@/components/charts/threshold-line-chart' @@ -95,6 +96,20 @@ function timeRangeToFrom(range: TimeRange): Date { return new Date(now - map[range]) } +/** + * Pick a ClickHouse bucket interval for an arbitrary (custom-range) span, + * mirroring the thresholds in `RANGE_BUCKET` so a custom 24h looks like the + * 24h preset. Keep the labels in the same allowlist the backend parses. + */ +function bucketForSpan(spanMs: number): string { + const hours = spanMs / 3_600_000 + if (hours <= 2) return '1 minute' + if (hours <= 12) return '5 minutes' + if (hours <= 48) return '15 minutes' + if (hours <= 24 * 14) return '1 hour' + return '6 hours' +} + // ── Aggregation selector ───────────────────────────────────────────────────── // // These mirror the FROZEN store-neutral contract from Phase C @@ -205,6 +220,17 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { () => parseLabelFilters(searchParams.get('labels')), [searchParams], ) + // Custom absolute date range — overrides the relative preset when both bounds + // are present and valid (`?start`/`?end`, ISO). The backend metric query takes + // arbitrary start/end, so this is purely a UI concern. + const customStartStr = searchParams.get('start') + const customEndStr = searchParams.get('end') + const isCustom = (() => { + if (!customStartStr || !customEndStr) return false + const s = new Date(customStartStr).getTime() + const e = new Date(customEndStr).getTime() + return Number.isFinite(s) && Number.isFinite(e) && s < e + })() const [nameSearch, setNameSearch] = useState('') @@ -219,7 +245,21 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { const setServiceName = (v: string) => patchParams((p) => (v ? p.set('service', v) : p.delete('service'))) const setTimeRange = (v: TimeRange) => - patchParams((p) => (v === '24h' ? p.delete('range') : p.set('range', v))) + patchParams((p) => { + // Selecting a preset always exits custom mode. + p.delete('start') + p.delete('end') + if (v === '24h') p.delete('range') + else p.set('range', v) + }) + // Apply an absolute range (from the date picker, or freezing the current + // window). Clears the relative preset. + const setCustomRange = (from: Date, to: Date) => + patchParams((p) => { + p.set('start', from.toISOString()) + p.set('end', to.toISOString()) + p.delete('range') + }) const setEnvironmentId = (v: number | null) => patchParams((p) => v == null ? p.delete('environment_id') : p.set('environment_id', String(v)), @@ -255,14 +295,22 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { // Both bounds are memoized on `timeRange` so they stay STABLE across renders. // Using `new Date()` inline would change the query key every render and spin // React Query into an infinite refetch loop. - const fromDate = useMemo(() => timeRangeToFrom(timeRange), [timeRange]) + const fromDate = useMemo( + () => (isCustom ? new Date(customStartStr!) : timeRangeToFrom(timeRange)), + // eslint-disable-next-line react-hooks/exhaustive-deps + [isCustom, customStartStr, timeRange], + ) const toDate = useMemo( - () => new Date(), + () => (isCustom ? new Date(customEndStr!) : new Date()), // eslint-disable-next-line react-hooks/exhaustive-deps - [timeRange], + [isCustom, customEndStr, timeRange], ) const fromIso = fromDate.toISOString() const toIso = toDate.toISOString() + // Preset spans use their tuned bucket; a custom span derives one from its width. + const bucketInterval = isCustom + ? bucketForSpan(toDate.getTime() - fromDate.getTime()) + : RANGE_BUCKET[timeRange] const selectedEnv = environments?.find((e) => e.id === environmentId) ?? null const metricsQuery = useQuery({ @@ -274,7 +322,7 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { environment: selectedEnv?.name || undefined, start_time: fromIso, end_time: toIso, - bucket_interval: RANGE_BUCKET[timeRange], + bucket_interval: bucketInterval, // The AggKind strings (avg/sum/min/max/count/rate/pNN) map directly to // the backend's `MetricAggregation::parse`. aggregation, @@ -623,21 +671,37 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { - +
+ + {isCustom && ( + { + if (range?.from && range?.to) + setCustomRange(range.from, range.to) + }} + /> + )} +
@@ -709,7 +773,7 @@ export default function MetricsExplorer({ project }: MetricsExplorerProps) { names={filteredNames} fromIso={fromIso} toIso={toIso} - bucketInterval={RANGE_BUCKET[timeRange]} + bucketInterval={bucketInterval} aggregation={aggregation} onSelect={setMetricName} isLoadingNames={namesQuery.isPending} From 3da38a68eef5e3cdd0c8a4ca348b02c25f8e3ab6 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 09:47:40 +0200 Subject: [PATCH 27/56] feat(otel): embed a Datadog-style chart in metric-alert emails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alert emails were text-only. Now a fired metric alert leads with a rendered chart of the metric's recent series — the line, the breach threshold (dashed, breach side shaded + labelled e.g. "< 5.0") or the anomaly's expected band (amber), the firing value marked with a red line + label ("last 5m max: …"), the evaluation moment marked, and y-axis labels. Matches Datadog's alert chart. No change to the shared NotificationData struct: - The evaluator renders a self-contained inline SVG (no deps) and carries it on the fire's existing `metadata` under a reserved `_chart_svg` key, with the metric/value/threshold detail fields. - AlarmService now propagates FireAlarmRequest.metadata into the notification (it was dropped before) — so the email also gains a Details table of the context. - The email renders `_chart_svg` raw + the other fields as rows; Slack, webhook, and the persisted row skip `_`-prefixed keys so the SVG never leaks there. Verified live: a fired alert's Mailpit email embeds the chart; existing alarm/disk emails unaffected. --- crates/temps-monitoring/src/alarm_service.rs | 14 +- crates/temps-notifications/src/services.rs | 49 +++- .../src/services/metric_alert_evaluator.rs | 265 +++++++++++++++++- 3 files changed, 320 insertions(+), 8 deletions(-) diff --git a/crates/temps-monitoring/src/alarm_service.rs b/crates/temps-monitoring/src/alarm_service.rs index 4591a8570..cb76f6f6d 100644 --- a/crates/temps-monitoring/src/alarm_service.rs +++ b/crates/temps-monitoring/src/alarm_service.rs @@ -739,13 +739,25 @@ impl AlarmService { /// Send notification for a fired alarm (failure is logged, not propagated) async fn send_alarm_notification(&self, request: &FireAlarmRequest, alarm_id: i32) { - let metadata = self + let mut metadata = self .build_notification_metadata( request.project_id, request.service_id, request.environment_id, ) .await; + // Merge caller-supplied detail (e.g. the OTel metric-alert's metric/value + // fields and the reserved `_chart_svg` chart) so the channels can render + // it — the email surfaces the chart, Slack/webhook skip `_`-prefixed keys. + if let Some(serde_json::Value::Object(extra)) = &request.metadata { + for (k, v) in extra { + let s = match v { + serde_json::Value::String(s) => s.clone(), + other => other.to_string(), + }; + metadata.insert(k.clone(), s); + } + } let notification = NotificationData { id: uuid::Uuid::new_v4().to_string(), diff --git a/crates/temps-notifications/src/services.rs b/crates/temps-notifications/src/services.rs index 1cacd2a37..f8ad8fd80 100644 --- a/crates/temps-notifications/src/services.rs +++ b/crates/temps-notifications/src/services.rs @@ -395,11 +395,29 @@ impl EmailProvider { NotificationPriority::Critical => ("#dc2626", "#fef2f2", "🚨", "Critical"), }; - let metadata_html = if notification.metadata.is_empty() { + // Inline chart (e.g. an OTel metric-alert's recent series) carried as a + // reserved `_chart_svg` key and rendered raw. `_`-prefixed keys are + // channel payloads — never shown as plain detail rows. + let chart_html = notification + .metadata + .get("_chart_svg") + .map(|svg| { + format!( + r#"{}"#, + svg + ) + }) + .unwrap_or_default(); + + let visible_metadata: Vec<(&String, &String)> = notification + .metadata + .iter() + .filter(|(k, _)| !k.starts_with('_')) + .collect(); + let metadata_html = if visible_metadata.is_empty() { String::new() } else { - let rows: String = notification - .metadata + let rows: String = visible_metadata .iter() .map(|(k, v)| { // Format key: replace underscores with spaces and title-case @@ -496,6 +514,7 @@ impl EmailProvider { {message} + {chart} {metadata} @@ -521,6 +540,7 @@ impl EmailProvider { icon = icon, label = label, message = message_html, + chart = chart_html, metadata = metadata_html, priority = notification.priority, ) @@ -751,6 +771,9 @@ impl NotificationProvider for SlackProvider { let metadata_fields = notification .metadata .iter() + // `_`-prefixed keys are channel payloads (e.g. the email's `_chart_svg`), + // not human-facing fields — skip them here. + .filter(|(k, _)| !k.starts_with('_')) .map(|(k, v)| { serde_json::json!({ "title": k, @@ -824,7 +847,14 @@ impl NotificationProvider for WebhookProvider { .timeout(std::time::Duration::from_secs(self.timeout_secs)) .build()?; - // Build the payload with all notification data + // Build the payload with all notification data. `_`-prefixed keys are + // channel-specific payloads (e.g. the email's `_chart_svg`) — drop them + // so they don't bloat the webhook body. + let metadata: std::collections::HashMap<&String, &String> = notification + .metadata + .iter() + .filter(|(k, _)| !k.starts_with('_')) + .collect(); let payload = serde_json::json!({ "id": notification.id, "title": notification.title, @@ -833,7 +863,7 @@ impl NotificationProvider for WebhookProvider { "priority": notification.priority.to_string(), "severity": notification.effective_severity().to_string(), "timestamp": notification.timestamp.to_rfc3339(), - "metadata": notification.metadata, + "metadata": metadata, }); // Build the request with configured method @@ -1072,7 +1102,14 @@ impl NotificationService { // Pass the previous record's occurrence_count so the gap doubles per // ongoing-incident attempt instead of staying at the base delay forever. let previous_attempts = existing.as_ref().map(|e| e.occurrence_count).unwrap_or(0); - let metadata_json = serde_json::to_string(¬ification.metadata)?; + // Persist only human-facing metadata; `_`-prefixed channel payloads + // (e.g. the email's `_chart_svg`) would bloat the row needlessly. + let persisted_metadata: std::collections::HashMap<&String, &String> = notification + .metadata + .iter() + .filter(|(k, _)| !k.starts_with('_')) + .collect(); + let metadata_json = serde_json::to_string(&persisted_metadata)?; let next_allowed = Self::get_next_allowed_time(¬ification.priority, previous_attempts); // Create new notification record diff --git a/crates/temps-otel/src/services/metric_alert_evaluator.rs b/crates/temps-otel/src/services/metric_alert_evaluator.rs index a03ba2ea2..b3ec43ec3 100644 --- a/crates/temps-otel/src/services/metric_alert_evaluator.rs +++ b/crates/temps-otel/src/services/metric_alert_evaluator.rs @@ -132,6 +132,178 @@ fn histogram_quantile(bounds: &[f64], counts: &[u64], q: f64) -> f64 { bounds.last().copied().unwrap_or(0.0) } +/// A number formatted compactly for chart labels (100000 -> "100.0k"). +fn fmt_compact(v: f64) -> String { + let a = v.abs(); + if a >= 1e9 { + format!("{:.1}B", v / 1e9) + } else if a >= 1e6 { + format!("{:.1}M", v / 1e6) + } else if a >= 1e3 { + format!("{:.1}k", v / 1e3) + } else if a >= 10.0 { + format!("{:.0}", v) + } else { + format!("{:.2}", v) + } +} + +/// Escape text for inline SVG `` content. +fn svg_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + +/// Inputs for the alert email chart. +struct AlertChart<'a> { + values: &'a [f64], + /// Anomaly expected band (lower, upper) — shaded amber, labelled. + band: Option<(f64, f64)>, + /// Static threshold + whether a breach is ABOVE it: dashed line, breach side + /// lightly shaded red, with `threshold_label`. + threshold: Option<(f64, bool)>, + threshold_label: String, + /// The firing value — drawn as a red marker line with `value_label`. + value: f64, + value_label: String, +} + +/// Render a Datadog-style inline-SVG chart for an alert email: the recent series +/// (blue), the breach threshold / expected band shaded and labelled, the firing +/// value marked with a red line + label, the evaluation moment marked, and y-axis +/// labels. Light theme, self-contained (inline attributes only) so it renders in +/// mail clients that allow inline SVG (Mailpit, Apple Mail); clients that strip +/// it just drop the image and keep the email's text summary + detail table. +fn render_alert_chart_svg(chart: &AlertChart) -> String { + let values = chart.values; + const W: f64 = 560.0; + const H: f64 = 180.0; + const ML: f64 = 46.0; // left margin (y labels) + const MR: f64 = 12.0; + const MT: f64 = 14.0; + const MB: f64 = 20.0; // bottom margin + let xr = W - MR; // right edge of plot + let yb = H - MB; // bottom edge of plot + + let mut lo = values.iter().copied().fold(f64::INFINITY, f64::min); + let mut hi = values.iter().copied().fold(f64::NEG_INFINITY, f64::max); + if let Some((bl, bu)) = chart.band { + lo = lo.min(bl); + hi = hi.max(bu); + } + if let Some((t, _)) = chart.threshold { + lo = lo.min(t); + hi = hi.max(t); + } + lo = lo.min(chart.value); + hi = hi.max(chart.value); + if !lo.is_finite() || !hi.is_finite() { + return String::new(); + } + if (hi - lo).abs() < f64::EPSILON { + hi = lo + 1.0; + lo -= 1.0; + } + let span = hi - lo; + lo -= span * 0.08; + hi += span * 0.08; + + let pw = W - ML - MR; + let ph = H - MT - MB; + let n = values.len(); + let x_at = |i: usize| -> f64 { + if n <= 1 { + ML + } else { + ML + pw * (i as f64) / ((n - 1) as f64) + } + }; + let y_at = |v: f64| -> f64 { MT + ph * (1.0 - (v - lo) / (hi - lo)) }; + + let mut s = format!( + r##""## + ); + + // Gridlines + y labels at lo / mid / hi. + let tx = ML - 6.0; + for frac in [0.0_f64, 0.5, 1.0] { + let v = lo + span * frac + span * 0.08 * (1.0 - 2.0 * frac); + let y = y_at(v); + let yt = y + 3.0; + let label = fmt_compact(v); + s.push_str(&format!( + r##"{label}"## + )); + } + + // Anomaly expected band (amber) + label. + if let Some((bl, bu)) = chart.band { + let yt = y_at(bu); + let bh = (y_at(bl) - yt).max(0.0); + let lx = ML + 4.0; + let ly = yt + 11.0; + let lbl = format!("expected {}–{}", fmt_compact(bl), fmt_compact(bu)); + s.push_str(&format!( + r##"{lbl}"## + )); + } + + // Static breach zone (red) + threshold dashed line + label. + if let Some((t, breach_above)) = chart.threshold { + let yt = y_at(t); + let zy = if breach_above { MT } else { yt }; + let zh = if breach_above { + (yt - MT).max(0.0) + } else { + (yb - yt).max(0.0) + }; + let lx = ML + 4.0; + let ly = yt - 4.0; + let lbl = svg_escape(&chart.threshold_label); + s.push_str(&format!( + r##"{lbl}"## + )); + } + + // Firing-value marker line (red) + label. + let vy = y_at(chart.value); + let vly = vy - 4.0; + let vlbl = format!( + "{}: {}", + svg_escape(&chart.value_label), + fmt_compact(chart.value) + ); + s.push_str(&format!( + r##"{vlbl}"## + )); + + // Evaluation marker (vertical, at the latest point). + let ex = x_at(n - 1); + s.push_str(&format!( + r##""## + )); + + // Metric line + latest point. + let pts: String = values + .iter() + .enumerate() + .map(|(i, &v)| format!("{:.1},{:.1}", x_at(i), y_at(v))) + .collect::>() + .join(" "); + s.push_str(&format!( + r##""## + )); + if let Some(&last) = values.last() { + let cy = y_at(last); + s.push_str(&format!( + r##""## + )); + } + s.push_str(""); + s +} + /// Map a rule severity string to an `AlarmSeverity`. fn map_severity(severity: &str) -> AlarmSeverity { match severity { @@ -539,6 +711,15 @@ impl MetricAlertEvaluator { /// Fire an alarm via the reused alarm system and remember the alarm id. async fn fire(&self, rule: &AlertRule, details: FireDetails) { + // Best-effort: render a compact chart of the recent series for the email + // (Datadog-style). Carried as a reserved `_chart_svg` metadata key that + // only the email renderer reads; never blocks or fails the fire. + let mut metadata = details.metadata; + if let Some(svg) = self.chart_svg_for(rule, &metadata).await { + if let serde_json::Value::Object(map) = &mut metadata { + map.insert("_chart_svg".to_string(), serde_json::Value::String(svg)); + } + } let request = FireAlarmRequest { project_id: rule.project_id, environment_id: None, @@ -549,7 +730,7 @@ impl MetricAlertEvaluator { severity: map_severity(&rule.severity), title: details.title, message: details.message, - metadata: Some(details.metadata), + metadata: Some(metadata), }; match self.alarm_service.fire_alarm(request).await { Ok(Some(alarm_id)) => { @@ -569,6 +750,88 @@ impl MetricAlertEvaluator { } } + /// Render a compact chart SVG of the metric's recent series for the alert + /// email, reading the expected band / threshold from the fire metadata. + /// Returns None on any hiccup (too little data, query error) — the email + /// simply omits the chart. + async fn chart_svg_for( + &self, + rule: &AlertRule, + metadata: &serde_json::Value, + ) -> Option { + // Show ~60 windows of context around the breach. + let window = rule.window_secs.max(60); + let now = Utc::now(); + let query = MetricQuery { + project_id: rule.project_id, + metric_name: Some(rule.metric_name.clone()), + start_time: Some(now - chrono::Duration::seconds(window as i64 * 60)), + end_time: Some(now), + bucket_interval: Some(format!("{}s", window)), + limit: Some(120), + aggregation: MetricAggregation::parse(&rule.aggregation), + ..Default::default() + }; + let buckets = self.otel_service.query_metrics(query).await.ok()?; + if buckets.len() < 2 { + return None; + } + let agg = MetricAggregation::parse(&rule.aggregation); + let values: Vec = buckets.iter().map(|b| value_for_rule(b, agg)).collect(); + + // Expected band: anomaly rules carry center/scale/deviations; static + // rules carry a single threshold line. + let band = match ( + metadata.get("baseline_center").and_then(|v| v.as_f64()), + metadata.get("baseline_scale").and_then(|v| v.as_f64()), + metadata.get("deviations").and_then(|v| v.as_f64()), + ) { + (Some(center), Some(scale), Some(dev)) => { + Some((center - dev * scale, center + dev * scale)) + } + _ => None, + }; + let threshold = metadata.get("threshold").and_then(|v| v.as_f64()).map(|t| { + // ">"/"≥" → breach above the line; "<"/"≤" → breach below. + let breach_above = metadata + .get("comparator") + .and_then(|v| v.as_str()) + .map(|c| c.contains('>')) + .unwrap_or(true); + (t, breach_above) + }); + let threshold_label = match ( + threshold, + metadata.get("comparator").and_then(|v| v.as_str()), + ) { + (Some((t, _)), Some(cmp)) => format!("{} {}", cmp, fmt_compact(t)), + _ => String::new(), + }; + let value = metadata + .get("value") + .and_then(|v| v.as_f64()) + .or_else(|| values.last().copied()) + .unwrap_or(0.0); + let w = rule.window_secs.max(1); + let window_label = if w % 3600 == 0 { + format!("{}h", w / 3600) + } else if w % 60 == 0 { + format!("{}m", w / 60) + } else { + format!("{}s", w) + }; + let value_label = format!("last {} {}", window_label, rule.aggregation); + + Some(render_alert_chart_svg(&AlertChart { + values: &values, + band, + threshold, + threshold_label, + value, + value_label, + })) + } + /// Resolve the alarm previously fired for this rule, if any. async fn resolve(&self, rule: &AlertRule) { let alarm_id = self.firing.write().await.remove(&rule.id); From 8568e2e74cbcf5a5400bb7775e89cfcc6b85065d Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 11:57:22 +0200 Subject: [PATCH 28/56] feat(otel): add x-axis time ticks to alert email chart The anomaly alert email chart had no x-axis, so the time window was ambiguous. Thread bucket timestamps into AlertChart and render three ticks: a dated start (e.g. 'Jun 27 08:56'), a bare HH:MM midpoint, and a UTC-marked end at the evaluation moment. Bottom margin widened to fit. --- .../src/services/metric_alert_evaluator.rs | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/crates/temps-otel/src/services/metric_alert_evaluator.rs b/crates/temps-otel/src/services/metric_alert_evaluator.rs index b3ec43ec3..d3164b50b 100644 --- a/crates/temps-otel/src/services/metric_alert_evaluator.rs +++ b/crates/temps-otel/src/services/metric_alert_evaluator.rs @@ -155,9 +155,22 @@ fn svg_escape(s: &str) -> String { .replace('>', ">") } +/// Format a UTC timestamp for an x-axis tick. `with_date` prepends the calendar +/// day (used on the left tick) so a chart read days later is still unambiguous; +/// otherwise just `HH:MM`. Zero-padded specifiers only, for cross-platform output. +fn fmt_axis_time(t: DateTime, with_date: bool) -> String { + if with_date { + t.format("%b %d %H:%M").to_string() + } else { + t.format("%H:%M").to_string() + } +} + /// Inputs for the alert email chart. struct AlertChart<'a> { values: &'a [f64], + /// Bucket timestamps (UTC) parallel to `values`; drives the x-axis time ticks. + times: &'a [DateTime], /// Anomaly expected band (lower, upper) — shaded amber, labelled. band: Option<(f64, f64)>, /// Static threshold + whether a breach is ABOVE it: dashed line, breach side @@ -182,7 +195,7 @@ fn render_alert_chart_svg(chart: &AlertChart) -> String { const ML: f64 = 46.0; // left margin (y labels) const MR: f64 = 12.0; const MT: f64 = 14.0; - const MB: f64 = 20.0; // bottom margin + const MB: f64 = 26.0; // bottom margin (room for x-axis time ticks) let xr = W - MR; // right edge of plot let yb = H - MB; // bottom edge of plot @@ -300,6 +313,32 @@ fn render_alert_chart_svg(chart: &AlertChart) -> String { r##""## )); } + + // X-axis time ticks: start (with date), middle, and end (the eval moment, + // marked UTC so the window is unambiguous). The series is index-linear, so + // each tick is placed at — and labelled with — its actual bucket timestamp. + let times = chart.times; + if times.len() == n && n >= 2 { + let ty = yb + 12.0; + let mid = n / 2; + let last = n - 1; + let lt = svg_escape(&fmt_axis_time(times[0], true)); + s.push_str(&format!( + r##"{lt}"## + )); + if mid != 0 && mid != last { + let mx = x_at(mid); + let mt = svg_escape(&fmt_axis_time(times[mid], false)); + s.push_str(&format!( + r##"{mt}"## + )); + } + let rt = svg_escape(&format!("{} UTC", fmt_axis_time(times[last], false))); + s.push_str(&format!( + r##"{rt}"## + )); + } + s.push_str(""); s } @@ -778,6 +817,7 @@ impl MetricAlertEvaluator { } let agg = MetricAggregation::parse(&rule.aggregation); let values: Vec = buckets.iter().map(|b| value_for_rule(b, agg)).collect(); + let times: Vec> = buckets.iter().map(|b| b.bucket).collect(); // Expected band: anomaly rules carry center/scale/deviations; static // rules carry a single threshold line. @@ -824,6 +864,7 @@ impl MetricAlertEvaluator { Some(render_alert_chart_svg(&AlertChart { values: &values, + times: ×, band, threshold, threshold_label, @@ -918,6 +959,28 @@ mod tests { )); } + #[test] + fn test_chart_svg_renders_x_axis_time_ticks() { + use chrono::TimeZone; + let t0 = Utc.with_ymd_and_hms(2026, 6, 27, 9, 30, 0).unwrap(); + let times: Vec> = (0..5).map(|i| t0 + chrono::Duration::minutes(i)).collect(); + let values = vec![18.0, 19.0, 90.0, 92.0, 95.0]; + let svg = render_alert_chart_svg(&AlertChart { + values: &values, + times: ×, + band: Some((16.0, 20.0)), + threshold: None, + threshold_label: String::new(), + value: 95.0, + value_label: "last 1m avg".to_string(), + }); + // Start tick carries the date; middle tick is bare HH:MM; end tick (the + // eval moment) carries the UTC marker so the window can't be misread. + assert!(svg.contains("Jun 27 09:30"), "missing dated start tick"); + assert!(svg.contains("09:32"), "missing middle tick"); + assert!(svg.contains("09:34 UTC"), "missing UTC-marked end tick"); + } + #[test] fn test_histogram_quantile() { // bounds [10,50,100], counts [2,3,4,1] (total 10, cum 2/5/9/10). From ccba7790cde725291a170a2779d3141ae71fff98 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 13:52:21 +0200 Subject: [PATCH 29/56] docs(adr): ADR-021 humanized alert notification text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deterministic plain-language humanizer as the OSS default (no AI, no latency, no key), with an optional best-effort AI enricher over the existing temps-ai-gateway that only augments — never gates or delays — the deterministic text. Draws the OSS (one-shot summary) vs EE AI SRE (deep investigation) boundary. --- docs/adr/021-humanized-alert-notifications.md | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 docs/adr/021-humanized-alert-notifications.md diff --git a/docs/adr/021-humanized-alert-notifications.md b/docs/adr/021-humanized-alert-notifications.md new file mode 100644 index 000000000..947fa7b90 --- /dev/null +++ b/docs/adr/021-humanized-alert-notifications.md @@ -0,0 +1,316 @@ +--- +title: "ADR-021: Humanized alert notification text (deterministic default, optional AI enrichment)" +status: Proposed +date: 2026-06-27 +author: David Viejo +--- + +# ADR-021: Humanized alert notification text (deterministic default, optional AI enrichment) + +**Status:** Proposed +**Date:** 2026-06-27 +**Author:** David Viejo + +## Context + +The metric alert notification (email/Slack/webhook) currently leads with a +statistician's sentence. For an anomaly it reads: + +> `guestbook.activity.level avg is 76.223 — 34.6σ from the baseline 18.467 ± 1.672 (band ±3σ)` + +and for a static threshold: + +> `guestbook.list.requests avg is 5.000 (threshold ≥ 0.000)` + +These strings are built by a single `format!` each, in `FireDetails::anomaly_breach` +(`crates/temps-otel/src/services/metric_alert_evaluator.rs:409`) and +`FireDetails::static_breach` (`:382`). They are precise and grep-able, but for an +on-call engineer glancing at a phone notification they bury the lede: *what +happened, how bad, and which direction.* "34.6σ" is not how a human reads "this +metric is ~4× its normal level." + +We want the notification to read like a human wrote it. The question raised was +whether to do this with a **configured AI/model in OSS** (the `temps-ai-gateway` +crate already exists with BYO-key providers). The honest answer is: humanization +and AI are two different levers, and conflating them puts an LLM on a path that +must never flake. + +### The reliability constraint + +An alert is the thing you depend on when something is on fire. Its baseline +quality cannot depend on an LLM being configured, reachable, fast, and within +budget. The notification text is produced **inside the alarm fire path**: the +evaluator builds `FireDetails`, then `AlarmService::fire_alarm` synchronously +fans the message out to email/Slack/webhook (the email is rendered and dispatched +as part of firing — once sent it cannot be edited). Any text that must appear in +the email therefore has to be ready *before* dispatch. Routing the **baseline** +message through a network LLM call would add that call's latency and failure risk +to every fire. + +### Rejected alternative: AI as the humanizer + +Generating the primary, always-present message via the model is rejected. It is +over-engineering a reliability-critical path: it adds per-alert latency and cost, +introduces a hard dependency (no provider key configured ⇒ no readable alert), and +risks hallucinated causes in the one place users must trust literally. Rephrasing +a number we already computed is not where an LLM earns its keep — we have the value, +baseline, scale, σ, band, and direction in hand already. + +### Where AI genuinely adds value + +An LLM beats a template at **synthesis across signals** — "errors on `/checkout` +co-spiked and a deploy landed 3 minutes earlier" — not at restating a single +number. That is also the boundary with the EE **AI SRE companion** +(`Feature::AiSre`, interactive multi-step investigation over logs/traces/errors). +A one-shot *summary* enricher is a fair OSS feature; deep *investigation* stays EE. + +## Decision + +**Humanize deterministically by default; offer an optional, best-effort AI +enricher that only ever augments — never gates or delays — the deterministic text.** + +Two tiers: + +1. **Tier 1 — deterministic humanizer (OSS default, no AI).** A pure + `humanize` module in `temps-otel` rewrites the `FireDetails.message` into plain + language from the facts already computed. Zero latency, zero cost, no key + required, works for 100% of self-hosters. Exact figures continue to live in + `metadata` and render in the email's DETAILS table for the math-minded. + +2. **Tier 2 — optional AI enricher (OSS, opt-in, off by default).** When a + provider key is configured *and* a per-project toggle is on, the evaluator + calls `temps-ai-gateway`'s `GatewayService::chat_completion` + (`crates/temps-ai-gateway/src/services/gateway_service.rs:130`) with a compact + set of **structured facts** to produce a 1–2 sentence summary. It is wrapped in + a hard timeout and falls back to the Tier-1 text on any failure, missing key, + or budget exhaustion. It enriches the message; it never blocks the fire beyond + its bounded timeout. + +### Tier 1 — the deterministic humanizer + +New module `crates/temps-otel/src/services/humanize.rs`, pure and unit-tested. It +takes the same inputs the `FireDetails` constructors already have: + +```rust +pub struct AnomalyFacts { + pub metric: String, // rule.metric_name + pub agg: String, // rule.aggregation + pub value: f64, + pub center: f64, // baseline center + pub scale: f64, // baseline scale (≈ σ) + pub z: f64, // (value - center) / scale + pub band_lo: f64, // center - deviations*scale + pub band_hi: f64, // center + deviations*scale + pub window_secs: i64, +} + +/// e.g. "Activity level (guestbook.activity.level) is unusually high — averaging +/// ~76 over the last minute, about 4× the normal ~18 (usual range 13–23), far +/// outside the expected band." +pub fn humanize_anomaly(f: &AnomalyFacts) -> String { /* … */ } +pub fn humanize_static(f: &StaticFacts) -> String { /* … */ } +``` + +Phrasing rules (deterministic, no inference of cause): + +- **Direction** from `value` vs `center`: above ⇒ "high / elevated", below ⇒ + "low / depressed". +- **Magnitude** preferring a ratio when `center` is meaningfully non-zero: + `value/center` ⇒ "about 4× the normal ~18"; otherwise fall back to an absolute + delta. Numbers rounded via the existing `fmt_compact` helper (`:136`). +- **Severity adverb** banded off `|z|` relative to the rule's `deviations` (e.g. + "just outside" / "well outside" / "far outside" the expected band). +- **Window** rendered in human units ("over the last minute") from `window_secs`, + reusing the `window_label` logic already in `chart_svg_for` (`:815`). +- Keep the canonical metric name in parentheses so it stays searchable. + +This replaces only the `message` field; `title` and `metadata` are unchanged, so +the email subject, the DETAILS table, the chart, Slack blocks, and the webhook +payload all keep working. The exact `value/center/scale/z/deviations` stay in +`metadata` (already populated at `:419-434`). + +### Tier 2 — the optional AI enricher + +A trait carried by the evaluator, `None` by default: + +```rust +// temps-otel +pub trait AlertSummarizer: Send + Sync { + /// Best-effort 1–2 sentence summary. Returns None on any failure so the + /// caller keeps the deterministic text. MUST NOT outlive its own timeout. + async fn summarize(&self, facts: &AlertFacts) -> Option; +} +``` + +`MetricAlertEvaluator` holds `Option>` (wired like the +other optional deps). The OSS implementation lives behind the existing +`temps-ai-gateway` and is registered only when a provider key exists. Flow inside +`fire`, after `FireDetails` is built with the Tier-1 message: + +```rust +let mut details = FireDetails::anomaly_breach(rule, value, params, &eval); +if let Some(sum) = &self.summarizer { + if project_ai_summaries_enabled(rule.project_id) { + if let Ok(Some(text)) = + tokio::time::timeout(AI_SUMMARY_TIMEOUT, sum.summarize(&facts)).await + { + details.message = text; // enrich + details.metadata["ai_summary"] = json!(true); + } + } +} +// any timeout/None/error path leaves the deterministic message intact +``` + +Hard rules: + +- **`AI_SUMMARY_TIMEOUT` ≈ 4s**, wrapped in `tokio::time::timeout`. Bounds the + added fire latency; only paid when the feature is enabled and a key is set. +- **Always fall back** to the Tier-1 text on timeout, error, no key, disabled, or + over-budget. The deterministic message is the floor. +- **Only on an actual fire** (not every 30s tick) — alarms are already + cooldown-throttled by `AlarmService`, so call volume is naturally low. Optionally + cache by `(rule_id, direction)` to skip repeated fires within a window. +- **Structured facts only** in the prompt — the numbers above plus an optional + one-line shape descriptor — never raw series, labels with PII, or request bodies. + The system prompt forbids inventing causes: "summarize these facts for an on-call + engineer in 1–2 sentences; do not speculate about root cause." This keeps the + output grounded; root-cause reasoning is the EE companion's job. +- **Cross-signal context (Phase 2):** when cheap to gather, pass already-computed + facts like "a deploy completed N minutes before the breach" or "error events on + this project rose K× in the same window." This is the only place the LLM does + something a template can't, and it stays factual because the facts are precomputed. + +### Configuration (per the entity-column rule) + +Following CLAUDE.md (runtime config is an entity column, not an env var) and the +`attack_mode` tri-state precedent (`crates/temps-entities/src/environments.rs:45`): + +| Layer | Column | Meaning | +|---|---|---| +| projects | `ai_alert_summaries_enabled BOOLEAN NULL` | NULL = inherit global default (off) | +| environments | `ai_alert_summaries_enabled BOOLEAN NULL` | tri-state override (`Option`) | + +The **provider key and budget** already live in `ai_provider_keys` +(migration `m20260310_000001`) and `ai_gateway_config` +(`crates/temps-entities/src/ai_gateway_config.rs`: `allowed_models`, +`max_requests_per_minute`, `max_cost_per_month_microcents`). The enricher routes +through `GatewayService`, so it inherits that rate-limit and monthly-cost +governance for free, and the summarization model is chosen from `allowed_models`. +No new secret env var is introduced. + +### OSS ↔ EE boundary + +- **OSS:** Tier 1 (always) + Tier 2 one-shot summary (opt-in, BYO key). A nicer + sentence and, later, a factual cross-signal note. +- **EE:** the AI SRE companion (`Feature::AiSre`) — interactive, multi-step + investigation across logs/traces/errors. Distinct value; not cannibalized by a + one-shot summary. + +## Consequences + +### Positive + +- Every self-hoster gets a readable alert with no AI, no latency, no key — the + ~90% readability win at zero risk. +- The fire path's reliability is unchanged: the deterministic message is always + the floor; the LLM can only add, and only within a bounded timeout. +- Reuses existing infrastructure: `temps-ai-gateway`, `ai_provider_keys`, and the + `ai_gateway_config` rate/cost governance. No new provider abstraction, no new + secret env var. +- Clean product ladder OSS → EE, with the EE AI SRE companion's value protected. +- Localized blast radius for Tier 1: one new pure module + the two `FireDetails` + message lines; `title`/`metadata`/chart/Slack/webhook untouched. + +### Negative / risks + +- **Hallucinated causes** are the headline risk for Tier 2. Mitigated by feeding + only precomputed facts and a prompt that forbids speculation; root-cause + reasoning is explicitly out of scope for the OSS summary. +- **Bounded added latency on fire** when Tier 2 is enabled (≤ `AI_SUMMARY_TIMEOUT`). + Acceptable for a 30s evaluator cadence, but it is real and only paid when enabled. +- **Cost**, governed by `ai_gateway_config` budgets; an operator who sets generous + limits and noisy rules will spend more. Throttling + fire-only generation keep it + bounded. +- **Provider downtime / rate limits** degrade to the Tier-1 text (by design), but + the operator may not notice the AI summary silently stopped — surface an + `ai_summary` success counter. +- **Translation/locale:** Tier 1 ships English only; a future locale pass or the + LLM (which can be prompted for a target language) would address non-English teams. + +### Neutral + +- No behavior change for operators who leave `ai_alert_summaries_enabled` off / + NULL and configure no provider key: the summarizer slot is `None`; the path is + the deterministic humanizer only. +- The DETAILS table keeps the precise statistics, so nothing is lost for users who + want the exact σ/baseline numbers. + +## Phased plan + +### Phase 1 — Tier 1 deterministic humanizer (ship first) + +- Add `crates/temps-otel/src/services/humanize.rs` (`humanize_anomaly`, + `humanize_static`) with unit tests for direction, ratio vs delta, rounding, and + severity banding. +- Swap the two `format!` calls in `FireDetails` (`:382`, `:409`) to call it; keep + `title`/`metadata` unchanged. +- Verify live: trigger an anomaly, confirm the email/Slack/webhook lead sentence + is humanized and the DETAILS table still carries exact figures. + +### Phase 2 — Tier 2 opt-in AI enricher + +- Add the `AlertSummarizer` trait + an OSS impl over `GatewayService::chat_completion`. +- Add `ai_alert_summaries_enabled` columns (projects + environments, tri-state) and + a global default (off). +- Wire `Option>` into the evaluator; gate on toggle + key; + `tokio::time::timeout`; fallback; fire-only; optional `(rule_id, direction)` cache. +- Structured-facts prompt; system prompt forbids cause speculation; cap output + length; record an `ai_summary` success/failure counter. + +### Phase 3 — cross-signal facts in the prompt + +- Pass precomputed context (recent deploy within N minutes; co-spiking error count + in the same window) so the summary can note correlated changes factually. +- This is the genuine AI-shaped value and the natural hand-off point to consider + surfacing "open in AI SRE" (EE) for deeper investigation. + +## Open questions + +1. **Default model for summarization?** A small/cheap model is plenty for a 1–2 + sentence summary. Pick a sensible default from `allowed_models`, or require the + operator to designate a "summary model" in `ai_gateway_config`? +2. **Global toggle home:** a global default for `ai_alert_summaries_enabled` — + server config row, or a settings table? (Per-project/env columns are settled; + the global default's home is open.) +3. **Generalize `humanize`?** Start narrow in `temps-otel`. If monitoring alarms, + deploy failures, and error-group notifications want the same treatment, promote + a generic `humanize(context)` to `temps-core` later — not up front. +4. **Slack/webhook vs email parity:** the AI summary replaces `message`, so all + channels get it. Confirm we want the same enriched text everywhere (likely yes) + vs email-only. +5. **Cache TTL / invalidation** for `(rule_id, direction)` summaries on repeated + fires — fixed short TTL, or skip caching and rely on fire-throttling alone? + +## References + +- `crates/temps-otel/src/services/metric_alert_evaluator.rs:372-437` — + `FireDetails` and the two message `format!`s (`:382` static, `:409` anomaly) this + ADR humanizes; `metadata` already carries the exact stats (`:419-434`). +- `crates/temps-otel/src/services/metric_alert_evaluator.rs:136` — `fmt_compact` + (reused for rounded numbers); `:815` — `window_label` logic (reused for "last 1m"). +- `crates/temps-ai-gateway/src/services/gateway_service.rs:130` — + `GatewayService::chat_completion(&ChatCompletionRequest, &ByokOverride)`, the + Tier-2 entrypoint (resolves system or BYO key, routes to provider). +- `crates/temps-entities/src/ai_gateway_config.rs` — `allowed_models`, + `max_requests_per_minute`, `max_cost_per_month_microcents` (rate/cost governance + the enricher inherits). +- `crates/temps-migrations/src/migration/m20260310_000001_create_ai_provider_keys.rs` + — encrypted provider keys (the "is a model configured?" gate). +- `crates/temps-entities/src/environments.rs:45` — `attack_mode: Option` + tri-state per-environment precedent for `ai_alert_summaries_enabled`. +- `temps-monitoring` `AlarmService::fire_alarm` / `send_alarm_notification` — the + synchronous fan-out that renders and dispatches the email during firing (why the + AI summary must be ready before dispatch, not after). +- AI SRE companion (`Feature::AiSre`) — the EE interactive investigator; the OSS↔EE + boundary this ADR draws (one-shot summary in OSS, deep investigation in EE). From 533a41988355a7d1edcdec31ea171318c98d3b55 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 14:08:39 +0200 Subject: [PATCH 30/56] feat(otel): humanize metric alert notification text (ADR-021 Tier 1) Replace the statistician's message with a plain-language lead built deterministically from the same figures: direction, rounded value, window in human units, magnitude vs baseline, and how far outside the expected band. Exact stats stay in the alarm metadata (email DETAILS table). Pure, no AI, no I/O; unit-tested. --- .../src/services/metric_alert_evaluator.rs | 241 +++++++++++++++++- 1 file changed, 227 insertions(+), 14 deletions(-) diff --git a/crates/temps-otel/src/services/metric_alert_evaluator.rs b/crates/temps-otel/src/services/metric_alert_evaluator.rs index d3164b50b..3114eb566 100644 --- a/crates/temps-otel/src/services/metric_alert_evaluator.rs +++ b/crates/temps-otel/src/services/metric_alert_evaluator.rs @@ -29,8 +29,8 @@ use tracing::{debug, error, info, warn}; use temps_monitoring::{AlarmService, AlarmSeverity, AlarmType, FireAlarmRequest}; use crate::detectors::{ - AnomalyParams, BandModel, DetectionConfig, StaticParams, DEFAULT_LOOKBACK_DAYS, MIN_BAND_SCALE, - MIN_BASELINE_SAMPLES, + AnomalyParams, BandModel, Comparator, DetectionConfig, StaticParams, DEFAULT_LOOKBACK_DAYS, + MIN_BAND_SCALE, MIN_BASELINE_SAMPLES, }; use crate::services::metric_alert_service::MetricAlertService; use crate::services::OtelService; @@ -166,6 +166,157 @@ fn fmt_axis_time(t: DateTime, with_date: bool) -> String { } } +// --- Plain-language notification text (ADR-021 Tier 1) ----------------------- +// +// The alert notification leads with a humanized sentence built deterministically +// from the figures the detector already computed. No AI, no I/O — pure functions, +// unit-tested. The exact statistics stay in the alarm `metadata` (and the email +// DETAILS table), so nothing is lost for the math-minded. + +/// Past-tense verb describing how the rule's aggregation reduced the window, +/// e.g. `avg` -> "averaged", so the sentence reads naturally. +fn agg_verb(aggregation: &str) -> &'static str { + match aggregation.to_ascii_lowercase().as_str() { + "avg" | "average" | "mean" => "averaged", + "max" | "maximum" => "peaked at", + "min" | "minimum" => "bottomed out at", + "sum" | "total" => "totaled", + "count" => "counted", + _ => "was", // percentiles / anything else + } +} + +/// The rule window in human units, e.g. "over the last minute" / "over the last +/// 5 minutes" / "over the last hour". +fn window_phrase(window_secs: i64) -> String { + let w = window_secs.max(1); + let (n, unit) = if w % 3600 == 0 { + (w / 3600, "hour") + } else if w % 60 == 0 { + (w / 60, "minute") + } else { + (w, "second") + }; + if n == 1 { + format!("over the last {unit}") + } else { + format!("over the last {n} {unit}s") + } +} + +/// A multiplier label that drops a redundant decimal, e.g. 4.13 -> "4×", +/// 4.25 -> "4.2×". +fn times_label(ratio: f64) -> String { + if (ratio - ratio.round()).abs() < 0.15 { + format!("{:.0}×", ratio) + } else { + format!("{:.1}×", ratio) + } +} + +/// How far past the expected band the value sits, in plain words. `ratio` is +/// `|z| / deviations` — `> 1` means breaching; bigger means further out. +fn outside_phrase(z_abs: f64, deviations: f64) -> &'static str { + let ratio = if deviations > 0.0 { + z_abs / deviations + } else { + z_abs + }; + if ratio >= 4.0 { + "far outside" + } else if ratio >= 1.5 { + "well outside" + } else { + "just outside" + } +} + +/// Magnitude relative to baseline, e.g. "about 4× the normal 18" (above) or +/// "about 28% of the normal 18" (below). Falls back to an absolute delta when +/// the baseline is too close to zero for a ratio to mean anything. +fn magnitude_phrase(value: f64, center: f64, above: bool) -> String { + if center.abs() >= 1.0 { + if above { + format!( + "about {} the normal {}", + times_label(value / center), + fmt_compact(center) + ) + } else { + let pct = (value / center * 100.0).round(); + format!("about {:.0}% of the normal {}", pct, fmt_compact(center)) + } + } else { + let delta = (value - center).abs(); + let dir = if above { "above" } else { "below" }; + format!( + "{} {} the usual ~{}", + fmt_compact(delta), + dir, + fmt_compact(center) + ) + } +} + +/// Plain-language summary of an anomaly breach. +/// +/// e.g. "guestbook.activity.level is unusually high — it averaged 76 over the +/// last minute, about 4× the normal 18. That's far outside the expected range +/// (13–23)." +#[allow(clippy::too_many_arguments)] +fn humanize_anomaly( + metric: &str, + aggregation: &str, + value: f64, + center: f64, + scale: f64, + z: f64, + deviations: f64, + window_secs: i64, +) -> String { + let above = value >= center; + let direction = if above { "high" } else { "low" }; + let band_lo = center - deviations * scale; + let band_hi = center + deviations * scale; + format!( + "{} is unusually {} — it {} {} {}, {}. That's {} the expected range ({}–{}).", + metric, + direction, + agg_verb(aggregation), + fmt_compact(value), + window_phrase(window_secs), + magnitude_phrase(value, center, above), + outside_phrase(z.abs(), deviations), + fmt_compact(band_lo), + fmt_compact(band_hi), + ) +} + +/// Plain-language summary of a static-threshold breach. +/// +/// e.g. "guestbook.list.requests averaged 5 over the last minute, above the 100 +/// threshold." +fn humanize_static( + metric: &str, + aggregation: &str, + value: f64, + comparator: Comparator, + threshold: f64, + window_secs: i64, +) -> String { + let above = matches!(comparator, Comparator::Gt | Comparator::Gte); + let side = if above { "above" } else { "below" }; + format!( + "{} {} {} {}, {} the {} threshold.", + metric, + agg_verb(aggregation), + fmt_compact(value), + window_phrase(window_secs), + side, + fmt_compact(threshold), + ) +} + /// Inputs for the alert email chart. struct AlertChart<'a> { values: &'a [f64], @@ -379,13 +530,13 @@ impl FireDetails { fn static_breach(rule: &AlertRule, value: f64, p: &StaticParams) -> Self { FireDetails { title: format!("Metric threshold breached: {}", rule.name), - message: format!( - "{} {} is {:.3} (threshold {} {:.3})", - rule.metric_name, - rule.aggregation, + message: humanize_static( + &rule.metric_name, + &rule.aggregation, value, - p.comparator.symbol(), - p.threshold + p.comparator, + p.threshold, + i64::from(rule.window_secs), ), metadata: json!({ "rule_id": rule.id, @@ -406,15 +557,15 @@ impl FireDetails { let z = (value - ev.center) / ev.scale.max(MIN_BAND_SCALE); FireDetails { title: format!("Metric anomaly: {}", rule.name), - message: format!( - "{} {} is {:.3} — {:.1}σ from the baseline {:.3} ± {:.3} (band ±{}σ)", - rule.metric_name, - rule.aggregation, + message: humanize_anomaly( + &rule.metric_name, + &rule.aggregation, value, - z.abs(), ev.center, ev.scale, - p.deviations + z, + p.deviations, + i64::from(rule.window_secs), ), metadata: json!({ "rule_id": rule.id, @@ -981,6 +1132,68 @@ mod tests { assert!(svg.contains("09:34 UTC"), "missing UTC-marked end tick"); } + #[test] + fn test_window_phrase() { + assert_eq!(window_phrase(60), "over the last minute"); + assert_eq!(window_phrase(300), "over the last 5 minutes"); + assert_eq!(window_phrase(3600), "over the last hour"); + assert_eq!(window_phrase(7200), "over the last 2 hours"); + assert_eq!(window_phrase(45), "over the last 45 seconds"); + } + + #[test] + fn test_humanize_anomaly_high() { + // The screenshot case: value 76.2, baseline 18.5 ± 1.67, band ±3σ. + let z = (76.223 - 18.467) / 1.672_f64.max(MIN_BAND_SCALE); + let msg = humanize_anomaly( + "guestbook.activity.level", + "avg", + 76.223, + 18.467, + 1.672, + z, + 3.0, + 60, + ); + assert!( + msg.contains("guestbook.activity.level is unusually high"), + "{msg}" + ); + assert!(msg.contains("averaged 76"), "{msg}"); + assert!(msg.contains("over the last minute"), "{msg}"); + assert!(msg.contains("about 4× the normal 18"), "{msg}"); + assert!(msg.contains("far outside the expected range"), "{msg}"); + // No statistician's notation in the human sentence. + assert!(!msg.contains('σ'), "should not leak sigma: {msg}"); + } + + #[test] + fn test_humanize_anomaly_low() { + let z = (5.0 - 18.467) / 1.672_f64.max(MIN_BAND_SCALE); + let msg = humanize_anomaly("svc.queue.depth", "avg", 5.0, 18.467, 1.672, z, 3.0, 300); + assert!(msg.contains("is unusually low"), "{msg}"); + assert!(msg.contains("% of the normal 18"), "{msg}"); + assert!(msg.contains("over the last 5 minutes"), "{msg}"); + } + + #[test] + fn test_humanize_static_above_and_below() { + let above = humanize_static( + "guestbook.list.requests", + "avg", + 150.0, + Comparator::Gte, + 100.0, + 60, + ); + assert!(above.contains("averaged 150"), "{above}"); + assert!(above.contains("above the 100 threshold"), "{above}"); + + let below = humanize_static("cache.hit.ratio", "min", 0.4, Comparator::Lt, 0.9, 60); + assert!(below.contains("bottomed out at"), "{below}"); + assert!(below.contains("below the"), "{below}"); + } + #[test] fn test_histogram_quantile() { // bounds [10,50,100], counts [2,3,4,1] (total 10, cum 2/5/9/10). From a605b735b5197b8e3dea2ad4388b7d2703fb2368 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 14:32:51 +0200 Subject: [PATCH 31/56] docs(adr): ADR-022 general AI foundation for typed/structured output A temps-core AiService seam + generic complete_typed::() that derives a JSON Schema (schemars) from a Rust type, asks the provider for matching JSON (response_format, already plumbed), and deserializes. Implemented over the existing ai-gateway (keys, routing, rate/cost governance). Usable from any crate; ADR-021's AlertSummarizer becomes consumer #1. --- .../022-ai-foundation-structured-output.md | 279 ++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 docs/adr/022-ai-foundation-structured-output.md diff --git a/docs/adr/022-ai-foundation-structured-output.md b/docs/adr/022-ai-foundation-structured-output.md new file mode 100644 index 000000000..5addb6375 --- /dev/null +++ b/docs/adr/022-ai-foundation-structured-output.md @@ -0,0 +1,279 @@ +--- +title: "ADR-022: A general AI foundation for typed/structured output (`AiService`)" +status: Proposed +date: 2026-06-27 +author: David Viejo +--- + +# ADR-022: A general AI foundation for typed/structured output (`AiService`) + +**Status:** Proposed +**Date:** 2026-06-27 +**Author:** David Viejo + +## Context + +ADR-021 added an AI enricher for alert notifications. While wiring it, the real +requirement surfaced: **any part of Temps should be able to ask the configured +model for structured, typed data** — not just free text, and not just for +alerts. Alert summaries are merely the first consumer. We want one governed, +provider-agnostic way to say "given these facts, return *this Rust type*," and +get back a validated value. + +Building this per-feature (a bespoke trait like ADR-021's `AlertSummarizer`, a +hand-rolled prompt, ad-hoc JSON parsing) would scatter prompt-wrangling, JSON +repair, model selection, and governance across every crate that wants AI. That +is the wrong foundation. + +The pieces already exist: + +- **`temps-ai-gateway`** routes to providers (OpenAI, Anthropic, xAI, Gemini), + decrypts BYO/system keys, and enforces per-scope rate/cost governance via + `ai_gateway_config` (`allowed_models`, `max_requests_per_minute`, + `max_cost_per_month_microcents`). Entry point: + `GatewayService::chat_completion` (`crates/temps-ai-gateway/src/services/gateway_service.rs:130`). +- **Structured output is already plumbed.** `ChatCompletionRequest.response_format` + exists (`crates/temps-ai-gateway/src/types.rs:35`), and the Gemini provider + already translates it to JSON mode + (`crates/temps-ai-gateway/src/providers/gemini.rs:266`); OpenAI-compatible + providers accept `{"type":"json_schema", ...}` natively. +- **`schemars` 0.8** is a workspace dependency (`Cargo.toml:182`), so a JSON + Schema can be *derived* from any Rust type. + +What's missing is a small, reusable seam on top of the gateway that turns those +into "call AI, get a `T`." + +### Rejected alternative: feature-specific AI traits + +Continuing the ADR-021 pattern — a new `XxxSummarizer`/`XxxAnalyzer` trait per +feature — is rejected. It duplicates model resolution, prompt scaffolding, JSON +extraction/repair, governance, and the best-effort/timeout discipline in every +consumer. The `AlertSummarizer` introduced in ADR-021 is therefore **demoted to +a thin consumer** of the foundation defined here (see §5). + +### Rejected alternative: a generic method on the trait object + +The natural API — `async fn complete(...) -> T` — is **not +object-safe**, so it can't live on an `Arc` resolved through the +plugin DI. The decision below splits it: an object-safe core method that speaks +`serde_json::Value`, plus a generic *free function* that adds the typed sugar. + +## Decision + +**Add `temps_core::ai::AiService`: an object-safe, governed, provider-agnostic AI +seam, plus a generic `complete_typed::()` helper that derives a JSON Schema +from `T`, asks the provider for matching JSON, and deserializes. Implement it in +`temps-ai-gateway` over `GatewayService`. Every AI use in Temps — alerts and +beyond — goes through this.** + +### 1. The seam (`temps-core`, no AI dependency) + +```rust +// crates/temps-core/src/ai/mod.rs +use async_trait::async_trait; + +#[derive(Debug, Clone, Default)] +pub struct AiRequest { + /// Short tag for logging / usage attribution, e.g. "alert.summary". + pub purpose: String, + /// Optional governance + usage scope (per-project budgets). + pub project_id: Option, + pub system: Option, + pub prompt: String, + /// Override the configured default model for this call. + pub model: Option, + pub max_tokens: Option, + pub temperature: Option, + /// When set, the provider is asked to return JSON matching this schema. + pub response_schema: Option, +} + +#[derive(Debug, Clone)] +pub struct AiResponse { + pub text: String, + /// Parsed JSON when a schema was requested and the reply parsed. + pub json: Option, + pub model: String, +} + +/// The governed AI capability. Object-safe: registered + resolved as +/// `Arc` via the plugin DI, like `NotificationService`. +#[async_trait] +pub trait AiService: Send + Sync { + /// Cheap gate: is a provider key + usable model actually configured? Lets a + /// caller skip building a prompt when AI is unavailable. + async fn is_available(&self) -> bool; + + /// Low-level completion. Best-effort: returns `AiError` rather than + /// panicking; callers wrap in a timeout. + async fn complete(&self, request: AiRequest) -> Result; +} +``` + +### 2. The typed sugar (generic free functions) + +Object-safety lives on the trait; ergonomics live beside it: + +```rust +// crates/temps-core/src/ai/typed.rs + +/// Plain text. `None` if AI is unavailable or the call fails. +pub async fn complete_text(ai: &dyn AiService, req: AiRequest) -> Option { + ai.complete(req).await.ok().map(|r| r.text) +} + +/// Typed structured output. Derives `T`'s JSON Schema, requests matching JSON, +/// and deserializes. `None` on any failure (unavailable, provider error, +/// non-conforming JSON). One repair retry on parse failure. +pub async fn complete_typed(ai: &dyn AiService, mut req: AiRequest) -> Option +where + T: schemars::JsonSchema + serde::de::DeserializeOwned, +{ + let schema = serde_json::to_value(schemars::schema_for!(T)).ok()?; + req.response_schema = Some(schema); + let resp = ai.complete(req).await.ok()?; + let value = resp.json.or_else(|| extract_json_block(&resp.text))?; + serde_json::from_value(value).ok() +} +``` + +Caller experience — "structured data from AI anywhere": + +```rust +#[derive(serde::Deserialize, schemars::JsonSchema)] +struct DeployRiskAssessment { risk: String, reasons: Vec } + +let assessment: Option = complete_typed( + ai.as_ref(), + AiRequest { purpose: "deploy.risk".into(), prompt, ..Default::default() }, +).await; +``` + +### 3. The implementation (`temps-ai-gateway`) + +`GatewayAiService { gateway: Arc, db: Arc }`, +registered in the AI-gateway plugin as `Arc`: + +- `is_available()` — a model resolves (from `ai_gateway_config.allowed_models`, + or a configured default) **and** a provider key exists for it. +- `complete(req)`: + 1. resolve the model (`req.model` → project-scope config → instance config); + 2. build a `ChatCompletionRequest`; when `req.response_schema` is set, attach + `response_format = {"type":"json_schema","json_schema":{"name":"temps_response","schema":,"strict":true}}` + (OpenAI-native; Gemini already maps it to JSON mode); + 3. `gateway.chat_completion(&req, &ByokOverride::default())` — inherits key + decryption, provider routing, rate/cost governance, usage tracking; + 4. extract assistant text; if a schema was requested, parse JSON (stripping + markdown fences) into `AiResponse.json`. + +Provider JSON-mode support is uneven, so the helper also extracts a JSON block +from plain text — structured output degrades to "parse what came back," never to +a hard failure. + +### 4. Governance, safety, config + +- **One throttle/budget path:** every call goes through `GatewayService`, so the + existing `ai_gateway_config` rate/cost limits and usage accounting apply to + internal AI exactly as to the public gateway. `purpose` + `project_id` attribute + the spend. +- **Best-effort everywhere:** `complete_typed`/`complete_text` return `Option`; + the trait returns `Result`. AI is never on a path that can block or fail a core + operation — callers add a timeout (e.g. alerts' `AI_SUMMARY_TIMEOUT`). +- **Availability is a capability, opt-in is a feature concern:** `is_available()` + answers "is AI configured at all." Whether a *feature* uses it (e.g. the + per-project `ai_alert_summaries_enabled` toggle from ADR-021) stays with the + feature, not the foundation. +- **No new secrets as env vars:** keys stay in `ai_provider_keys`; model/limits + stay in `ai_gateway_config` (entity rows, per CLAUDE.md). + +### 5. Alerts become consumer #1 (refactor ADR-021) + +The bespoke `temps_core::alert_summary::AlertSummarizer` + `AiGatewaySummarizer` +introduced in ADR-021 are refactored onto this foundation: + +- The metric alert evaluator depends on `Option>`. +- On fire (when the project toggle is on and `ai.is_available()`), it builds an + `AiRequest` from the alert facts and calls `complete_text` (a richer + `complete_typed::` once we want a structured headline + reason), + inside the existing timeout, falling back to the deterministic Tier-1 text. + +This deletes the alert-specific trait in favour of the general one — less code, +and the same call shape every future feature will use. + +## Consequences + +### Positive + +- One governed, provider-agnostic way to get **typed** data from AI, callable + from any crate via a `temps-core` seam — the foundation the product needs. +- Schema-derived structured output (`schemars`) means call sites declare a Rust + type and get it back; no hand-written schemas, no brittle string parsing. +- Reuses all existing gateway machinery (keys, routing, rate/cost, usage) — no + parallel AI stack, no new secret env vars. +- Best-effort + object-safe by construction; AI can never block or fail a core + path, and the capability flows through normal plugin DI. +- Collapses ADR-021's alert-specific trait into a general one (net less code). + +### Negative / risks + +- **Provider JSON-mode variance:** not every provider/model honours + `response_format` identically; the text-extraction fallback + one repair retry + mitigate but don't eliminate non-conforming replies. `complete_typed` returns + `None` rather than a wrong value. +- **Cost/latency** are real when used on hot paths; mitigated by `is_available()` + gating, per-scope budgets, and mandatory caller-side timeouts. Internal AI + should stay on cold/triggered paths (alerts, on-demand analysis), not request + hot paths. +- **Prompt-injection / data exposure:** callers must pass only the data they + intend; the foundation does not sanitize. Document that user-controlled text in + a prompt is a trust boundary. +- **Schema drift:** `schemars` output for complex types can be large; keep + request/response DTOs small and flat for reliable structured replies. + +### Neutral + +- No behaviour change when no provider key is configured: `is_available()` is + false and every helper returns `None`. + +## Phased plan + +1. **Foundation:** `temps_core::ai` (trait, `AiRequest`/`AiResponse`/`AiError`, + `complete_text`/`complete_typed`, `extract_json_block`); add `schemars` + + `async-trait` to `temps-core`. Unit-test `extract_json_block` and the typed + helper against a mock `AiService`. +2. **Impl + DI:** `GatewayAiService` in `temps-ai-gateway`; register as + `Arc` in the gateway plugin (replacing ADR-021's summarizer + registration). Model resolution + `is_available()` + `response_format` wiring. +3. **Refactor alerts onto it:** evaluator depends on `AiService`; delete + `AlertSummarizer`/`AiGatewaySummarizer`; keep the `ai_alert_summaries_enabled` + toggle and the timeout/fallback. +4. **Second consumer (proof of generality):** pick one structured use — + e.g. a `complete_typed::` over a failed build log tail, + or error-group titling — to validate the typed path end-to-end. + +## Open questions + +1. **Default internal model:** reuse `allowed_models[0]`, or add an explicit + `ai_gateway_config.default_model` for server-internal features so summaries + don't depend on the public allow-list? +2. **Repair policy:** how many JSON-repair retries (0 or 1) before giving up, and + do we log non-conforming replies for tuning? +3. **Sync vs streaming:** the foundation is request/response; streaming + (`chat_completion_stream`) stays gateway-only until a consumer needs it. +4. **Per-purpose budgets:** do we want `ai_gateway_config` scopes keyed by + `purpose` (e.g. cap "alert.summary" spend separately), or is project/instance + scope enough for v1? + +## References + +- ADR-021 (humanized alert notifications) — the first consumer; its + `AlertSummarizer` is refactored onto this foundation (§5). +- `crates/temps-ai-gateway/src/services/gateway_service.rs:130` — + `GatewayService::chat_completion`, the governed entry point this wraps. +- `crates/temps-ai-gateway/src/types.rs:35` — existing + `ChatCompletionRequest.response_format` (structured-output input). +- `crates/temps-ai-gateway/src/providers/gemini.rs:266` — provider translation of + `response_format` to JSON mode (`response_mime_type`). +- `crates/temps-entities/src/ai_gateway_config.rs` — `allowed_models` + + rate/cost governance inherited by internal AI calls. +- `Cargo.toml:182` — `schemars` 0.8, used to derive JSON Schemas from Rust types. From 20533bff4f561ed0d46f706a8ffd6e3deb5c0fdb Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 14:48:45 +0200 Subject: [PATCH 32/56] feat(ai): general AiService foundation for typed/structured output (ADR-022) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add temps-core::ai — an object-safe AiService trait (is_available + complete) plus complete_text/complete_typed:: helpers that derive a JSON Schema (schemars) from a Rust type, request matching JSON, and deserialize, with a robust extract_json_block fallback. Implement it as GatewayAiService over the existing ai-gateway (provider keys, routing, rate/cost governance), registered as Arc. Refactor ADR-021's alert enrichment onto it: the metric alert evaluator now depends on Option>, gated on the per-project ai_alert_summaries_enabled toggle and is_available, bounded by a 4s timeout, falling back to the deterministic Tier-1 text. Deletes the bespoke AlertSummarizer trait. Also anchor an over-broad '/ai' gitignore rule so source dirs named 'ai' are tracked. Adds projects.ai_alert_summaries_enabled (m20260627_000001). --- .gitignore | 2 +- Cargo.lock | 1 + .../src/services/config_service.rs | 1 + crates/temps-agents/src/services/executor.rs | 1 + crates/temps-ai-gateway/src/plugin.rs | 9 + .../src/services/ai_service.rs | 183 +++++++++++++++++ crates/temps-ai-gateway/src/services/mod.rs | 2 + crates/temps-core/Cargo.toml | 1 + crates/temps-core/src/ai/mod.rs | 93 +++++++++ crates/temps-core/src/ai/typed.rs | 191 ++++++++++++++++++ crates/temps-core/src/lib.rs | 1 + crates/temps-entities/src/projects.rs | 4 + ...m20260627_000001_add_ai_alert_summaries.rs | 53 +++++ crates/temps-migrations/src/migration/mod.rs | 2 + .../src/vulnerability_notifications.rs | 1 + crates/temps-otel/src/plugin.rs | 5 + .../src/services/metric_alert_evaluator.rs | 119 ++++++++++- 17 files changed, 667 insertions(+), 2 deletions(-) create mode 100644 crates/temps-ai-gateway/src/services/ai_service.rs create mode 100644 crates/temps-core/src/ai/mod.rs create mode 100644 crates/temps-core/src/ai/typed.rs create mode 100644 crates/temps-migrations/src/migration/m20260627_000001_add_ai_alert_summaries.rs diff --git a/.gitignore b/.gitignore index cccff09be..8a4fa3d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ dist/ .settings/ target target2 -ai +/ai .cargo/config.toml *.crt diff --git a/Cargo.lock b/Cargo.lock index b311f4b55..2212902fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11149,6 +11149,7 @@ dependencies = [ "log", "once_cell", "rand 0.8.6", + "schemars 0.8.22", "serde", "serde_json", "serde_yaml", diff --git a/crates/temps-agents/src/services/config_service.rs b/crates/temps-agents/src/services/config_service.rs index b859aeb49..f62e4b311 100644 --- a/crates/temps-agents/src/services/config_service.rs +++ b/crates/temps-agents/src/services/config_service.rs @@ -1027,6 +1027,7 @@ mod tests { gitlab_webhook_id: None, gitlab_webhook_signing_token: None, attack_mode: false, + ai_alert_summaries_enabled: None, enable_preview_environments: false, preview_envs_on_demand: false, preview_envs_idle_timeout_seconds: 300, diff --git a/crates/temps-agents/src/services/executor.rs b/crates/temps-agents/src/services/executor.rs index 613a8c33b..a6cb623b4 100644 --- a/crates/temps-agents/src/services/executor.rs +++ b/crates/temps-agents/src/services/executor.rs @@ -4075,6 +4075,7 @@ mod tests { gitlab_webhook_id: None, gitlab_webhook_signing_token: None, attack_mode: false, + ai_alert_summaries_enabled: None, enable_preview_environments: true, preview_envs_on_demand: false, preview_envs_idle_timeout_seconds: 300, diff --git a/crates/temps-ai-gateway/src/plugin.rs b/crates/temps-ai-gateway/src/plugin.rs index ff1824bfa..327ec66f6 100644 --- a/crates/temps-ai-gateway/src/plugin.rs +++ b/crates/temps-ai-gateway/src/plugin.rs @@ -48,6 +48,15 @@ impl TempsPlugin for AiGatewayPlugin { let gateway_service = Arc::new(GatewayService::new(provider_key_service.clone())); context.register_service(gateway_service.clone()); + // ADR-022: register the general AI foundation so any feature can get + // text or typed/structured output from the configured model through + // one governed seam. Best-effort + self-gating, safe to always register. + let ai_service = Arc::new(crate::services::GatewayAiService::new( + gateway_service.clone(), + db.clone(), + )); + context.register_service(ai_service as Arc); + let usage_service = Arc::new(UsageService::new(db)); context.register_service(usage_service.clone()); diff --git a/crates/temps-ai-gateway/src/services/ai_service.rs b/crates/temps-ai-gateway/src/services/ai_service.rs new file mode 100644 index 000000000..c6a11c769 --- /dev/null +++ b/crates/temps-ai-gateway/src/services/ai_service.rs @@ -0,0 +1,183 @@ +//! ADR-022: the gateway-backed implementation of the general [`AiService`] +//! foundation. +//! +//! Wraps [`GatewayService`] so every internal AI call inherits provider-key +//! resolution, model routing, and per-scope rate/cost governance. Structured +//! output rides the gateway's existing `response_format` plumbing. Best-effort: +//! returns [`AiError`] rather than panicking; callers add the timeout. + +use std::sync::Arc; + +use async_trait::async_trait; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; +use tracing::debug; + +use temps_core::ai::{AiError, AiRequest, AiResponse, AiService}; + +use crate::services::{ByokOverride, GatewayService}; +use crate::types::{ChatCompletionRequest, ChatCompletionResponse, MessageContent}; + +/// Gateway-backed [`AiService`] (ADR-022). +pub struct GatewayAiService { + gateway: Arc, + db: Arc, +} + +impl GatewayAiService { + pub fn new(gateway: Arc, db: Arc) -> Self { + Self { gateway, db } + } + + /// Resolve the model to use: an explicit per-call `model`, else the first + /// entry of `allowed_models` for a `project:{id}` config if present, else the + /// instance-scope config. `None` when nothing names a concrete model. + async fn resolve_model( + &self, + project_id: Option, + explicit: Option<&str>, + ) -> Option { + if let Some(m) = explicit { + if !m.is_empty() { + return Some(m.to_string()); + } + } + let mut scopes: Vec = Vec::new(); + if let Some(pid) = project_id { + scopes.push(format!("project:{pid}")); + } + scopes.push("instance".to_string()); + + let rows = temps_entities::ai_gateway_config::Entity::find() + .filter(temps_entities::ai_gateway_config::Column::Scope.is_in(scopes.clone())) + .all(self.db.as_ref()) + .await + .ok()?; + for scope in scopes { + if let Some(row) = rows.iter().find(|r| r.scope == scope) { + if let Some(model) = first_model(row.allowed_models.as_ref()) { + return Some(model); + } + } + } + None + } +} + +/// First model id from an `allowed_models` JSON array (`["a","b"]` -> "a"). +/// `None`/non-array/empty -> `None` (NULL means "all allowed", which names no +/// specific model to use for internal calls). +fn first_model(allowed_models: Option<&serde_json::Value>) -> Option { + allowed_models? + .as_array()? + .iter() + .find_map(|v| v.as_str()) + .map(str::to_string) +} + +/// Build the OpenAI-style `response_format` for a requested JSON Schema. Gemini +/// maps this to JSON mode; OpenAI-compatible providers honour `json_schema`. +fn response_format_for(schema: &serde_json::Value) -> serde_json::Value { + serde_json::json!({ + "type": "json_schema", + "json_schema": { "name": "temps_response", "schema": schema, "strict": true } + }) +} + +/// Pull the assistant text out of the first choice. +fn first_text(resp: &ChatCompletionResponse) -> Option { + let choice = resp.choices.first()?; + match choice.message.content.as_ref()? { + MessageContent::Text(s) => Some(s.clone()), + MessageContent::Parts(_) => None, + } +} + +#[async_trait] +impl AiService for GatewayAiService { + async fn is_available(&self) -> bool { + self.resolve_model(None, None).await.is_some() + } + + async fn complete(&self, request: AiRequest) -> Result { + let model = self + .resolve_model(request.project_id, request.model.as_deref()) + .await + .ok_or_else(|| AiError::NoModel { + purpose: request.purpose.clone(), + })?; + + let mut messages = Vec::new(); + if let Some(system) = &request.system { + messages.push(serde_json::json!({"role": "system", "content": system})); + } + messages.push(serde_json::json!({"role": "user", "content": request.prompt})); + + let mut body = serde_json::json!({ + "model": model, + "messages": messages, + }); + if let Some(mt) = request.max_tokens { + body["max_tokens"] = serde_json::json!(mt); + } + if let Some(t) = request.temperature { + body["temperature"] = serde_json::json!(t); + } + let wants_json = request.response_schema.is_some(); + if let Some(schema) = &request.response_schema { + body["response_format"] = response_format_for(schema); + } + + let chat_req: ChatCompletionRequest = + serde_json::from_value(body).map_err(|e| AiError::Provider { + purpose: request.purpose.clone(), + reason: format!("malformed request: {e}"), + })?; + + let (resp, _cred) = self + .gateway + .chat_completion(&chat_req, &ByokOverride::default()) + .await + .map_err(|e| { + debug!(error = %e, purpose = request.purpose, model, "AI completion failed"); + AiError::Provider { + purpose: request.purpose.clone(), + reason: e.to_string(), + } + })?; + + let text = first_text(&resp).unwrap_or_default(); + let json = if wants_json { + temps_core::ai::extract_json_block(&text) + } else { + None + }; + Ok(AiResponse { + text: text.trim().to_string(), + json, + model, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_first_model() { + assert_eq!( + first_model(Some(&serde_json::json!(["gpt-4o-mini", "x"]))), + Some("gpt-4o-mini".to_string()) + ); + assert_eq!(first_model(Some(&serde_json::json!([]))), None); + assert_eq!(first_model(None), None); + } + + #[test] + fn test_response_format_for() { + let rf = response_format_for(&serde_json::json!({"type": "object"})); + assert_eq!(rf["type"], "json_schema"); + assert_eq!(rf["json_schema"]["schema"]["type"], "object"); + assert_eq!(rf["json_schema"]["strict"], true); + } +} diff --git a/crates/temps-ai-gateway/src/services/mod.rs b/crates/temps-ai-gateway/src/services/mod.rs index fab765059..1c7af9c16 100644 --- a/crates/temps-ai-gateway/src/services/mod.rs +++ b/crates/temps-ai-gateway/src/services/mod.rs @@ -1,7 +1,9 @@ +pub mod ai_service; pub mod gateway_service; pub mod provider_key_service; pub mod usage_service; +pub use ai_service::GatewayAiService; pub use gateway_service::{ByokOverride, CredentialType, GatewayService}; pub use provider_key_service::ProviderKeyService; pub use usage_service::{AiRequestContext, UsageFilter, UsageService}; diff --git a/crates/temps-core/Cargo.toml b/crates/temps-core/Cargo.toml index 0788eb427..ea4420001 100644 --- a/crates/temps-core/Cargo.toml +++ b/crates/temps-core/Cargo.toml @@ -17,6 +17,7 @@ serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } thiserror = { workspace = true } +schemars = { workspace = true } anyhow = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } diff --git a/crates/temps-core/src/ai/mod.rs b/crates/temps-core/src/ai/mod.rs new file mode 100644 index 000000000..9733dc0f9 --- /dev/null +++ b/crates/temps-core/src/ai/mod.rs @@ -0,0 +1,93 @@ +//! The Temps AI foundation (ADR-022). +//! +//! A single, governed, provider-agnostic way for any crate to ask the configured +//! model for either free text or **typed, structured data**. The object-safe +//! [`AiService`] trait is registered + resolved through the plugin DI as +//! `Arc` (like [`crate::notifications::NotificationService`]); the +//! generic [`complete_typed`]/[`complete_text`] helpers in [`typed`] add the +//! ergonomic, schema-derived API on top. +//! +//! Everything here is best-effort: the trait returns [`Result`], the helpers +//! return [`Option`], and AI must never sit on a path that can block or fail a +//! core operation — callers wrap calls in a timeout. + +pub mod typed; + +pub use typed::{complete_text, complete_typed, extract_json_block}; + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +/// A single AI completion request. Construct with `..Default::default()` and set +/// only what you need: +/// +/// ```ignore +/// let req = AiRequest { purpose: "alert.summary".into(), prompt, ..Default::default() }; +/// ``` +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct AiRequest { + /// Short tag for logging, usage attribution, and per-purpose budgets, + /// e.g. `"alert.summary"` or `"deploy.risk"`. + pub purpose: String, + /// Optional governance + usage scope (per-project budgets / allow-lists). + pub project_id: Option, + /// Optional system instruction. + pub system: Option, + /// The user prompt. + pub prompt: String, + /// Override the configured default model for this call. + pub model: Option, + /// Cap on response tokens (provider default when `None`). + pub max_tokens: Option, + /// Sampling temperature (provider default when `None`). + pub temperature: Option, + /// When set, the provider is asked to return JSON matching this JSON Schema. + /// Usually populated by [`complete_typed`] from a Rust type rather than by + /// hand. + pub response_schema: Option, +} + +/// The result of a completion. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AiResponse { + /// The assistant's text reply. + pub text: String, + /// Parsed JSON, when a schema was requested and the reply parsed as JSON. + pub json: Option, + /// The model that actually served the request. + pub model: String, +} + +/// Why an AI call could not be completed. All variants are non-fatal — callers +/// fall back to non-AI behaviour. +#[derive(Debug, thiserror::Error)] +pub enum AiError { + /// No provider key / usable model is configured. Check [`AiService::is_available`] + /// first to avoid building a prompt that can't be served. + #[error("AI is not configured (no provider key or usable model)")] + NotAvailable, + /// No model could be resolved for this request. + #[error("no model configured for AI request '{purpose}'")] + NoModel { purpose: String }, + /// The provider/gateway returned an error. + #[error("AI provider error for '{purpose}': {reason}")] + Provider { purpose: String, reason: String }, +} + +/// The governed AI capability. Object-safe so it can be registered and resolved +/// as `Arc` through the plugin DI. +/// +/// Implementations route through the AI gateway, inheriting provider-key +/// resolution, model routing, and per-scope rate/cost governance. They are +/// best-effort: never panic, and never block beyond the work itself (the caller +/// adds the timeout). +#[async_trait] +pub trait AiService: Send + Sync { + /// Cheap gate: is a provider key + usable model actually configured? Lets a + /// caller skip prompt construction when AI is unavailable. + async fn is_available(&self) -> bool; + + /// Low-level completion. Prefer the [`complete_text`] / [`complete_typed`] + /// helpers for everyday use. + async fn complete(&self, request: AiRequest) -> Result; +} diff --git a/crates/temps-core/src/ai/typed.rs b/crates/temps-core/src/ai/typed.rs new file mode 100644 index 000000000..13c725c99 --- /dev/null +++ b/crates/temps-core/src/ai/typed.rs @@ -0,0 +1,191 @@ +//! Ergonomic, schema-derived helpers over the object-safe [`AiService`](super::AiService). +//! +//! Object-safety forbids generic methods on the trait object, so the typed API +//! lives here as free functions: [`complete_typed`] derives a JSON Schema from a +//! Rust type, asks the provider to return matching JSON, and deserializes. + +use serde::de::DeserializeOwned; + +use super::{AiRequest, AiService}; + +/// Plain-text completion. `None` if AI is unavailable or the call fails. +pub async fn complete_text(ai: &dyn AiService, request: AiRequest) -> Option { + ai.complete(request).await.ok().map(|r| r.text) +} + +/// Typed, structured completion — the foundation's headline capability. +/// +/// Derives `T`'s JSON Schema (via `schemars`), asks the provider to return +/// matching JSON, and deserializes into `T`. Returns `None` on any failure +/// (AI unavailable, provider error, or a reply that doesn't deserialize into +/// `T`) — never a wrong value. +/// +/// ```ignore +/// #[derive(serde::Deserialize, schemars::JsonSchema)] +/// struct Risk { level: String, reasons: Vec } +/// +/// let risk: Option = complete_typed(ai.as_ref(), +/// AiRequest { purpose: "deploy.risk".into(), prompt, ..Default::default() }).await; +/// ``` +pub async fn complete_typed(ai: &dyn AiService, mut request: AiRequest) -> Option +where + T: schemars::JsonSchema + DeserializeOwned, +{ + if request.response_schema.is_none() { + request.response_schema = serde_json::to_value(schemars::schema_for!(T)).ok(); + } + let resp = ai.complete(request).await.ok()?; + let value = resp.json.or_else(|| extract_json_block(&resp.text))?; + serde_json::from_value(value).ok() +} + +/// Best-effort extraction of a JSON value from a model's text reply. +/// +/// Handles the common ways providers wrap JSON when not in a strict JSON mode: +/// a ```` ```json ```` fenced block, a bare fenced block, or the first balanced +/// `{...}` / `[...]` span in the text. Returns `None` if nothing parses. +pub fn extract_json_block(text: &str) -> Option { + let trimmed = text.trim(); + + // 1. The whole reply is JSON. + if let Ok(v) = serde_json::from_str::(trimmed) { + return Some(v); + } + + // 2. A fenced code block (```json ... ``` or ``` ... ```). + if let Some(after) = trimmed.split("```").nth(1) { + let body = after.strip_prefix("json").unwrap_or(after); + if let Ok(v) = serde_json::from_str::(body.trim()) { + return Some(v); + } + } + + // 3. The first balanced object/array span anywhere in the text. + for (open, close) in [('{', '}'), ('[', ']')] { + if let Some(span) = balanced_span(trimmed, open, close) { + if let Ok(v) = serde_json::from_str::(span) { + return Some(v); + } + } + } + + None +} + +/// The first balanced `open..close` span in `s`, respecting string literals so a +/// brace inside a JSON string doesn't throw off the depth count. +fn balanced_span(s: &str, open: char, close: char) -> Option<&str> { + let bytes = s.as_bytes(); + let start = s.find(open)?; + let mut depth = 0i32; + let mut in_str = false; + let mut escaped = false; + for i in start..bytes.len() { + let c = bytes[i] as char; + if in_str { + if escaped { + escaped = false; + } else if c == '\\' { + escaped = true; + } else if c == '"' { + in_str = false; + } + continue; + } + match c { + '"' => in_str = true, + ch if ch == open => depth += 1, + ch if ch == close => { + depth -= 1; + if depth == 0 { + return Some(&s[start..=i]); + } + } + _ => {} + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ai::{AiError, AiResponse}; + use async_trait::async_trait; + + /// A mock that returns a canned text reply. + struct CannedAi(String); + + #[async_trait] + impl AiService for CannedAi { + async fn is_available(&self) -> bool { + true + } + async fn complete(&self, _request: AiRequest) -> Result { + Ok(AiResponse { + text: self.0.clone(), + json: None, + model: "mock".into(), + }) + } + } + + #[derive(serde::Deserialize, schemars::JsonSchema, PartialEq, Debug)] + struct Note { + headline: String, + score: i32, + } + + #[test] + fn test_extract_plain_json() { + let v = extract_json_block(r#"{"a": 1}"#).unwrap(); + assert_eq!(v["a"], 1); + } + + #[test] + fn test_extract_fenced_json() { + let v = extract_json_block("Sure:\n```json\n{\"a\": 2}\n```\nDone").unwrap(); + assert_eq!(v["a"], 2); + } + + #[test] + fn test_extract_embedded_object_with_braces_in_string() { + let v = extract_json_block(r#"prefix {"msg": "a } b", "n": 3} suffix"#).unwrap(); + assert_eq!(v["n"], 3); + assert_eq!(v["msg"], "a } b"); + } + + #[test] + fn test_extract_none_when_no_json() { + assert!(extract_json_block("no json here").is_none()); + } + + #[tokio::test] + async fn test_complete_typed_parses_fenced_reply() { + let ai = CannedAi("```json\n{\"headline\": \"hot\", \"score\": 5}\n```".into()); + let note: Option = complete_typed(&ai, AiRequest::default()).await; + assert_eq!( + note, + Some(Note { + headline: "hot".into(), + score: 5 + }) + ); + } + + #[tokio::test] + async fn test_complete_typed_none_on_garbage() { + let ai = CannedAi("I cannot help with that.".into()); + let note: Option = complete_typed(&ai, AiRequest::default()).await; + assert_eq!(note, None); + } + + #[tokio::test] + async fn test_complete_text() { + let ai = CannedAi("hello".into()); + assert_eq!( + complete_text(&ai, AiRequest::default()).await, + Some("hello".to_string()) + ); + } +} diff --git a/crates/temps-core/src/lib.rs b/crates/temps-core/src/lib.rs index 69ca4045c..0a839caee 100644 --- a/crates/temps-core/src/lib.rs +++ b/crates/temps-core/src/lib.rs @@ -1,6 +1,7 @@ //! Core utilities and types shared across all Temps crates pub mod admin_gate; +pub mod ai; pub mod audit; pub mod config; pub mod deployment; diff --git a/crates/temps-entities/src/projects.rs b/crates/temps-entities/src/projects.rs index 21700c1b6..a3afc4c72 100644 --- a/crates/temps-entities/src/projects.rs +++ b/crates/temps-entities/src/projects.rs @@ -40,6 +40,10 @@ pub struct Model { pub git_provider_connection_id: Option, /// Attack mode - when enabled, requires CAPTCHA verification for all visitors pub attack_mode: bool, + /// ADR-021 Tier 2: opt-in AI summarization of metric alert notifications for + /// this project. NULL/false = off (the deterministic Tier-1 text is used); + /// true = enrich with the configured AI provider when one is available. + pub ai_alert_summaries_enabled: Option, /// Enable automatic preview environment creation for each branch pub enable_preview_environments: bool, /// When true, preview environments auto-created for branches are diff --git a/crates/temps-migrations/src/migration/m20260627_000001_add_ai_alert_summaries.rs b/crates/temps-migrations/src/migration/m20260627_000001_add_ai_alert_summaries.rs new file mode 100644 index 000000000..804b2f651 --- /dev/null +++ b/crates/temps-migrations/src/migration/m20260627_000001_add_ai_alert_summaries.rs @@ -0,0 +1,53 @@ +//! Migration to add `ai_alert_summaries_enabled` to the `projects` table +//! (ADR-021 Tier 2). +//! +//! Per-project opt-in for AI summarization of metric alert notifications. The +//! column is NULLABLE with no default — existing rows get NULL on upgrade, which +//! means "off": the deterministic Tier-1 humanized text is used. Set to `true` +//! to enrich this project's alerts with the configured AI provider (best-effort; +//! falls back to the deterministic text when no provider is configured). +//! Additive-only and backward-compatible. + +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Projects::Table) + .add_column( + ColumnDef::new(Projects::AiAlertSummariesEnabled) + .boolean() + .null(), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Projects::Table) + .drop_column(Projects::AiAlertSummariesEnabled) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum Projects { + Table, + AiAlertSummariesEnabled, +} diff --git a/crates/temps-migrations/src/migration/mod.rs b/crates/temps-migrations/src/migration/mod.rs index 67b0662c2..21cf69785 100644 --- a/crates/temps-migrations/src/migration/mod.rs +++ b/crates/temps-migrations/src/migration/mod.rs @@ -124,6 +124,7 @@ mod m20260619_000001_add_settings_change_trigger; mod m20260621_000001_create_telemetry_milestones; mod m20260626_000001_create_metric_dashboards; mod m20260626_000002_create_metric_alert_rules; +mod m20260627_000001_add_ai_alert_summaries; pub struct Migrator; @@ -251,6 +252,7 @@ impl MigratorTrait for Migrator { Box::new(m20260621_000001_create_telemetry_milestones::Migration), Box::new(m20260626_000001_create_metric_dashboards::Migration), Box::new(m20260626_000002_create_metric_alert_rules::Migration), + Box::new(m20260627_000001_add_ai_alert_summaries::Migration), ] } } diff --git a/crates/temps-notifications/src/vulnerability_notifications.rs b/crates/temps-notifications/src/vulnerability_notifications.rs index 3321be3e4..fe7ae7d61 100644 --- a/crates/temps-notifications/src/vulnerability_notifications.rs +++ b/crates/temps-notifications/src/vulnerability_notifications.rs @@ -396,6 +396,7 @@ mod tests { gitlab_webhook_id: None, gitlab_webhook_signing_token: None, attack_mode: false, + ai_alert_summaries_enabled: None, source_type: temps_entities::source_type::SourceType::Git, }; diff --git a/crates/temps-otel/src/plugin.rs b/crates/temps-otel/src/plugin.rs index 2fdbe27e1..95af758d8 100644 --- a/crates/temps-otel/src/plugin.rs +++ b/crates/temps-otel/src/plugin.rs @@ -529,10 +529,15 @@ impl TempsPlugin for OtelPlugin { notification_service, job_queue, )); + // ADR-022: optional general AI foundation, registered by the AI + // gateway plugin when present. Absent -> deterministic Tier-1 text. + let ai = context.get_service::(); let evaluator = Arc::new(crate::services::MetricAlertEvaluator::new( metric_alert_service.clone(), otel_service.clone(), alarm_service, + db.clone(), + ai, )); tokio::spawn(async move { evaluator.run().await; diff --git a/crates/temps-otel/src/services/metric_alert_evaluator.rs b/crates/temps-otel/src/services/metric_alert_evaluator.rs index 3114eb566..c1babf197 100644 --- a/crates/temps-otel/src/services/metric_alert_evaluator.rs +++ b/crates/temps-otel/src/services/metric_alert_evaluator.rs @@ -45,6 +45,11 @@ const EVAL_INTERVAL_SECS: u64 = 30; /// against the cached baseline instead of re-querying the full lookback window. const BASELINE_REFRESH_SECS: u64 = 3600; +/// Hard cap on the optional AI summary call (ADR-021 Tier 2). Bounds the extra +/// latency added to a fire when the feature is enabled; on timeout the +/// deterministic Tier-1 message is kept. +const AI_SUMMARY_TIMEOUT: Duration = Duration::from_secs(4); + /// The state transition the evaluator should apply for a single rule this cycle. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AlertTransition { @@ -317,6 +322,71 @@ fn humanize_static( ) } +/// System prompt for the AI alert summary — constrain to one grounded sentence +/// and forbid the two failure modes that would make a summary worse than the +/// template: invented causes and invented numbers. +const ALERT_SUMMARY_SYSTEM: &str = "You are an observability assistant writing a one-line alert summary for an \ +on-call engineer. Rewrite the given metric-alert facts as a single clear, plain-English sentence of at most \ +30 words. Convey what the metric is doing, the magnitude, and that it is outside its expected range. Do NOT \ +speculate about causes, do NOT give recommendations, and do NOT invent numbers beyond those given. Reply with \ +only the sentence."; + +/// Build the best-effort AI summary request (ADR-022) from the rule, the fire +/// metadata (which already carries the computed figures), and the deterministic +/// message. Feeds only structured facts — no raw series — and grounds the model +/// with the deterministic sentence. Returns `None` when the essentials (a +/// `value`) are absent. +fn alert_summary_request( + rule: &AlertRule, + metadata: &serde_json::Value, + deterministic_summary: &str, +) -> Option { + let num = |k: &str| metadata.get(k).and_then(serde_json::Value::as_f64); + let value = num("value")?; + let mut facts = String::new(); + facts.push_str(&format!("Metric: {}\n", rule.metric_name)); + facts.push_str(&format!("Aggregation: {}\n", rule.aggregation)); + facts.push_str(&format!( + "Detection: {}\n", + metadata + .get("detection_kind") + .and_then(|v| v.as_str()) + .unwrap_or("static") + )); + facts.push_str(&format!("Current value: {value:.3}\n")); + if let Some(c) = num("baseline_center") { + facts.push_str(&format!("Baseline (normal) value: {c:.3}\n")); + if let (Some(s), Some(d)) = (num("baseline_scale"), num("deviations")) { + facts.push_str(&format!( + "Expected range: {:.3} to {:.3}\n", + c - d * s, + c + d * s + )); + } + } + if let (Some(t), Some(cmp)) = ( + num("threshold"), + metadata.get("comparator").and_then(|v| v.as_str()), + ) { + facts.push_str(&format!("Threshold: {cmp} {t:.3}\n")); + } + facts.push_str(&format!("Window seconds: {}\n", rule.window_secs)); + facts.push_str(&format!("Severity: {}\n", rule.severity)); + facts.push_str(&format!( + "Deterministic summary for reference: {deterministic_summary}" + )); + + Some(temps_core::ai::AiRequest { + purpose: "alert.summary".to_string(), + project_id: Some(rule.project_id), + system: Some(ALERT_SUMMARY_SYSTEM.to_string()), + prompt: facts, + max_tokens: Some(160), + temperature: Some(0.2), + ..Default::default() + }) +} + /// Inputs for the alert email chart. struct AlertChart<'a> { values: &'a [f64], @@ -599,6 +669,13 @@ pub struct MetricAlertEvaluator { firing: Arc>>, /// rule_id -> cached anomaly baseline (refreshed every `BASELINE_REFRESH_SECS`). baseline_cache: Arc>>, + /// DB handle, used only to read the per-project AI-summary opt-in toggle. + db: Arc, + /// ADR-022: optional general AI foundation. `None` (default) keeps the + /// deterministic Tier-1 text; when present (and the project opts in) it is + /// called inside a timeout on each fire and may replace the lead sentence, + /// never block or fail the alert. + ai: Option>, } impl MetricAlertEvaluator { @@ -606,6 +683,8 @@ impl MetricAlertEvaluator { alert_service: Arc, otel_service: Arc, alarm_service: Arc, + db: Arc, + ai: Option>, ) -> Self { Self { alert_service, @@ -614,9 +693,24 @@ impl MetricAlertEvaluator { breach_start: Arc::new(RwLock::new(HashMap::new())), firing: Arc::new(RwLock::new(HashMap::new())), baseline_cache: Arc::new(RwLock::new(HashMap::new())), + db, + ai, } } + /// Whether AI summarization is opted in for this project + /// (`projects.ai_alert_summaries_enabled = true`). Feature-level gate; the + /// foundation's `is_available` is the separate capability gate. + async fn ai_summaries_enabled(&self, project_id: i32) -> bool { + use sea_orm::EntityTrait; + matches!( + temps_entities::projects::Entity::find_by_id(project_id) + .one(self.db.as_ref()) + .await, + Ok(Some(p)) if p.ai_alert_summaries_enabled == Some(true) + ) + } + /// Run the evaluator loop forever. Skips the immediate first tick. pub async fn run(&self) { info!("Starting OTel metric alert evaluator"); @@ -910,6 +1004,29 @@ impl MetricAlertEvaluator { map.insert("_chart_svg".to_string(), serde_json::Value::String(svg)); } } + + // ADR-022: best-effort AI enrichment of the lead sentence via the general + // AI foundation. Bounded by `AI_SUMMARY_TIMEOUT`; any timeout / error / + // empty reply keeps the deterministic Tier-1 text. Gated on the project's + // opt-in toggle AND the foundation reporting AI is configured. + let mut message = details.message; + if let Some(ai) = &self.ai { + if self.ai_summaries_enabled(rule.project_id).await && ai.is_available().await { + if let Some(req) = alert_summary_request(rule, &metadata, &message) { + let fut = temps_core::ai::complete_text(ai.as_ref(), req); + if let Ok(Some(text)) = tokio::time::timeout(AI_SUMMARY_TIMEOUT, fut).await { + let text = text.trim(); + if !text.is_empty() { + if let serde_json::Value::Object(map) = &mut metadata { + map.insert("ai_summary".to_string(), serde_json::Value::Bool(true)); + } + message = text.to_string(); + } + } + } + } + } + let request = FireAlarmRequest { project_id: rule.project_id, environment_id: None, @@ -919,7 +1036,7 @@ impl MetricAlertEvaluator { alarm_type: AlarmType::DeploymentMetricThreshold, severity: map_severity(&rule.severity), title: details.title, - message: details.message, + message, metadata: Some(metadata), }; match self.alarm_service.fire_alarm(request).await { From 12f17f4f50c767df27c80d616de7946f3e4c6420 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 15:07:12 +0200 Subject: [PATCH 33/56] refactor(ai): extract reusable temps-ai crate with schemas + diagnostics Move the AI foundation out of temps-core into a dedicated, dependency-light temps-ai crate so any crate can use the trait + schemas without pulling in providers/HTTP. Adds: - schemas: reusable structured-output types (ErrorDiagnosis + FailureCategory) - diagnostics::diagnose_failure: one-call build/deploy failure diagnosis (log-tail bounded, schema-typed) for the deployer / UI to reuse temps-ai-gateway and temps-otel now depend on temps-ai (temps_core::ai -> temps_ai). temps-core drops the ai module + schemars dep. --- Cargo.lock | 16 +- Cargo.toml | 1 + crates/temps-ai-gateway/Cargo.toml | 1 + crates/temps-ai-gateway/src/plugin.rs | 2 +- .../src/services/ai_service.rs | 4 +- crates/temps-ai/Cargo.toml | 22 +++ crates/temps-ai/src/diagnostics.rs | 180 ++++++++++++++++++ crates/temps-ai/src/lib.rs | 27 +++ crates/temps-ai/src/schemas.rs | 91 +++++++++ .../src/ai/mod.rs => temps-ai/src/service.rs} | 27 +-- .../src/ai => temps-ai/src}/typed.rs | 6 +- crates/temps-core/Cargo.toml | 1 - crates/temps-core/src/lib.rs | 1 - crates/temps-otel/Cargo.toml | 1 + crates/temps-otel/src/plugin.rs | 2 +- .../src/services/metric_alert_evaluator.rs | 10 +- 16 files changed, 356 insertions(+), 36 deletions(-) create mode 100644 crates/temps-ai/Cargo.toml create mode 100644 crates/temps-ai/src/diagnostics.rs create mode 100644 crates/temps-ai/src/lib.rs create mode 100644 crates/temps-ai/src/schemas.rs rename crates/{temps-core/src/ai/mod.rs => temps-ai/src/service.rs} (73%) rename crates/{temps-core/src/ai => temps-ai/src}/typed.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 2212902fb..4c58a1fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10639,6 +10639,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "temps-ai" +version = "0.1.0-beta.39" +dependencies = [ + "async-trait", + "schemars 0.8.22", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "temps-ai-gateway" version = "0.1.0-beta.39" @@ -10657,6 +10670,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "temps-ai", "temps-auth", "temps-core", "temps-entities", @@ -11149,7 +11163,6 @@ dependencies = [ "log", "once_cell", "rand 0.8.6", - "schemars 0.8.22", "serde", "serde_json", "serde_yaml", @@ -12187,6 +12200,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", + "temps-ai", "temps-auth", "temps-config", "temps-core", diff --git a/Cargo.toml b/Cargo.toml index 7770ecf99..6cbabf4a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/temps-cli", # Library crates "crates/temps-core", + "crates/temps-ai", "crates/temps-entities", "crates/temps-migrations", "crates/temps-database", diff --git a/crates/temps-ai-gateway/Cargo.toml b/crates/temps-ai-gateway/Cargo.toml index 37f890c22..100e7ffcb 100644 --- a/crates/temps-ai-gateway/Cargo.toml +++ b/crates/temps-ai-gateway/Cargo.toml @@ -10,6 +10,7 @@ homepage.workspace = true [dependencies] temps-auth = { path = "../temps-auth" } temps-core = { path = "../temps-core" } +temps-ai = { path = "../temps-ai" } temps-entities = { path = "../temps-entities" } serde = { workspace = true } diff --git a/crates/temps-ai-gateway/src/plugin.rs b/crates/temps-ai-gateway/src/plugin.rs index 327ec66f6..765c6d5e9 100644 --- a/crates/temps-ai-gateway/src/plugin.rs +++ b/crates/temps-ai-gateway/src/plugin.rs @@ -55,7 +55,7 @@ impl TempsPlugin for AiGatewayPlugin { gateway_service.clone(), db.clone(), )); - context.register_service(ai_service as Arc); + context.register_service(ai_service as Arc); let usage_service = Arc::new(UsageService::new(db)); context.register_service(usage_service.clone()); diff --git a/crates/temps-ai-gateway/src/services/ai_service.rs b/crates/temps-ai-gateway/src/services/ai_service.rs index c6a11c769..2ac8d09d4 100644 --- a/crates/temps-ai-gateway/src/services/ai_service.rs +++ b/crates/temps-ai-gateway/src/services/ai_service.rs @@ -12,7 +12,7 @@ use async_trait::async_trait; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; use tracing::debug; -use temps_core::ai::{AiError, AiRequest, AiResponse, AiService}; +use temps_ai::{AiError, AiRequest, AiResponse, AiService}; use crate::services::{ByokOverride, GatewayService}; use crate::types::{ChatCompletionRequest, ChatCompletionResponse, MessageContent}; @@ -147,7 +147,7 @@ impl AiService for GatewayAiService { let text = first_text(&resp).unwrap_or_default(); let json = if wants_json { - temps_core::ai::extract_json_block(&text) + temps_ai::extract_json_block(&text) } else { None }; diff --git a/crates/temps-ai/Cargo.toml b/crates/temps-ai/Cargo.toml new file mode 100644 index 000000000..22efa139a --- /dev/null +++ b/crates/temps-ai/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "temps-ai" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Temps AI foundation: a governed, provider-agnostic seam for text and typed/structured output, plus reusable schemas (ADR-022)" +keywords = ["ai", "llm", "platform"] +categories = ["development-tools"] + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +schemars = { workspace = true } +async-trait = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true } diff --git a/crates/temps-ai/src/diagnostics.rs b/crates/temps-ai/src/diagnostics.rs new file mode 100644 index 000000000..aba8f6a68 --- /dev/null +++ b/crates/temps-ai/src/diagnostics.rs @@ -0,0 +1,180 @@ +//! High-level, reusable AI helpers that pair a prompt with a schema. +//! +//! [`diagnose_failure`] is the one-call entry point for "explain this failed +//! build / deployment": any crate (the deployer on build failure, a retry +//! handler, the UI on demand) passes a [`FailureContext`] and gets a structured +//! [`ErrorDiagnosis`] back, best-effort. No prompt-wrangling or JSON parsing at +//! the call site. + +use crate::schemas::ErrorDiagnosis; +use crate::service::{AiRequest, AiService}; +use crate::typed::complete_typed; + +/// Keep the log tail bounded so a giant build log can't blow the token budget. +/// We feed the *tail* — failures surface at the end — capped to this many bytes +/// (~the last chunk of output), trimmed to a line boundary. +const MAX_LOG_TAIL_BYTES: usize = 12_000; + +/// What kind of failure is being diagnosed (shapes the prompt framing). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FailureKind { + /// A container image build (Docker/Nixpacks/buildpacks) failed. + Build, + /// A deployment failed to start or pass health checks. + Deployment, +} + +impl FailureKind { + fn label(self) -> &'static str { + match self { + FailureKind::Build => "container image build", + FailureKind::Deployment => "deployment", + } + } +} + +/// Inputs for diagnosing a failed build or deployment. Carries only what the +/// model needs — no secrets; callers should pass a log tail with env values +/// already redacted if necessary. +#[derive(Debug, Clone)] +pub struct FailureContext { + pub kind: FailureKind, + /// Governance/usage scope. + pub project_id: Option, + /// The failing step/command, if known (e.g. a Dockerfile `RUN` line). + pub failed_step: Option, + /// Process exit code, if known. + pub exit_code: Option, + /// The build/deploy log (the tail is used; see [`MAX_LOG_TAIL_BYTES`]). + pub log: String, +} + +const DIAGNOSIS_SYSTEM: &str = "You are a senior platform/DevOps engineer diagnosing a failed build or deployment \ +from its log output. Identify the single most likely root cause and concrete, ordered fixes a developer can \ +apply. Base everything strictly on the evidence in the log — do NOT invent file names, versions, or causes you \ +cannot see. Quote the key log line(s) verbatim. Be concise and practical. Respond ONLY with JSON matching the \ +provided schema."; + +impl FailureContext { + /// The last [`MAX_LOG_TAIL_BYTES`] of the log, trimmed to a line boundary. + fn log_tail(&self) -> &str { + let log = self.log.trim_end(); + if log.len() <= MAX_LOG_TAIL_BYTES { + return log; + } + let start = log.len() - MAX_LOG_TAIL_BYTES; + // Advance to the next newline so we don't start mid-line / mid-char. + match log[start..].find('\n') { + Some(nl) => &log[start + nl + 1..], + None => &log[start..], + } + } + + fn to_prompt(&self) -> String { + let mut s = String::new(); + s.push_str(&format!("A {} failed.\n", self.kind.label())); + if let Some(step) = &self.failed_step { + s.push_str(&format!("Failing step: {step}\n")); + } + if let Some(code) = self.exit_code { + s.push_str(&format!("Exit code: {code}\n")); + } + s.push_str("\nLog tail:\n"); + s.push_str("```\n"); + s.push_str(self.log_tail()); + s.push_str("\n```"); + s + } +} + +/// Diagnose a failed build/deployment into a structured [`ErrorDiagnosis`]. +/// `None` when AI is unavailable or the reply doesn't conform — callers fall +/// back to showing the raw log. Best-effort: wrap in a timeout at the call site. +pub async fn diagnose_failure(ai: &dyn AiService, ctx: &FailureContext) -> Option { + if ctx.log.trim().is_empty() { + return None; + } + let req = AiRequest { + purpose: "deploy.failure_diagnosis".to_string(), + project_id: ctx.project_id, + system: Some(DIAGNOSIS_SYSTEM.to_string()), + prompt: ctx.to_prompt(), + max_tokens: Some(700), + temperature: Some(0.1), + ..Default::default() + }; + complete_typed(ai, req).await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schemas::FailureCategory; + use crate::service::{AiError, AiRequest as Req, AiResponse}; + use async_trait::async_trait; + + fn ctx(log: &str) -> FailureContext { + FailureContext { + kind: FailureKind::Build, + project_id: Some(7), + failed_step: Some("RUN cargo build --release".into()), + exit_code: Some(101), + log: log.into(), + } + } + + #[test] + fn test_prompt_includes_step_and_log() { + let p = ctx("error: linker `cc` not found").to_prompt(); + assert!(p.contains("container image build failed")); + assert!(p.contains("Failing step: RUN cargo build --release")); + assert!(p.contains("Exit code: 101")); + assert!(p.contains("error: linker `cc` not found")); + } + + #[test] + fn test_log_tail_truncates_to_line_boundary() { + let big = format!("{}\nFINAL ERROR LINE", "x".repeat(20_000)); + let c = ctx(&big); + let tail = c.log_tail(); + assert!(tail.len() <= MAX_LOG_TAIL_BYTES); + assert!(tail.ends_with("FINAL ERROR LINE")); + assert!(!tail.starts_with('x') || tail.len() < 20_000); // started at a line boundary + } + + struct CannedAi(String); + #[async_trait] + impl AiService for CannedAi { + async fn is_available(&self) -> bool { + true + } + async fn complete(&self, _r: Req) -> Result { + Ok(AiResponse { + text: self.0.clone(), + json: None, + model: "mock".into(), + }) + } + } + + #[tokio::test] + async fn test_diagnose_failure_parses_structured_reply() { + let reply = r#"```json +{"summary":"Missing C compiler.","likely_cause":"The build image has no `cc`.", +"suggested_fixes":["Install build-essential in the Dockerfile."], +"category":"dependency","key_log_lines":["error: linker `cc` not found"],"confidence":0.9} +```"#; + let ai = CannedAi(reply.into()); + let d = diagnose_failure(&ai, &ctx("error: linker `cc` not found")) + .await + .unwrap(); + assert_eq!(d.category, FailureCategory::Dependency); + assert_eq!(d.suggested_fixes.len(), 1); + } + + #[tokio::test] + async fn test_diagnose_failure_none_on_empty_log() { + let ai = CannedAi("{}".into()); + assert!(diagnose_failure(&ai, &ctx(" ")).await.is_none()); + } +} diff --git a/crates/temps-ai/src/lib.rs b/crates/temps-ai/src/lib.rs new file mode 100644 index 000000000..57e02944d --- /dev/null +++ b/crates/temps-ai/src/lib.rs @@ -0,0 +1,27 @@ +//! The Temps AI foundation (ADR-022). +//! +//! A single, governed, provider-agnostic way for any crate to ask the configured +//! model for either free text or **typed, structured data** — plus a library of +//! reusable [`schemas`] and high-level [`diagnostics`] helpers (debugging deploy +//! / docker build failures, etc.) so consumers don't re-roll prompts or parsing. +//! +//! Layers: +//! - [`service`] — the object-safe [`AiService`] trait, registered + resolved +//! through the plugin DI as `Arc`. The implementation lives in +//! `temps-ai-gateway`; this crate stays dependency-light so any crate can use +//! the trait + schemas without pulling in providers/HTTP. +//! - [`typed`] — `complete_text` / `complete_typed::` ergonomics on top. +//! - [`schemas`] — reusable structured-output types (`JsonSchema + Deserialize`). +//! - [`diagnostics`] — one-call helpers that pair a prompt with a schema. +//! +//! Everything is best-effort: the trait returns [`Result`], the helpers return +//! [`Option`], and AI must never sit on a path that can block or fail a core +//! operation — callers wrap calls in a timeout. + +pub mod diagnostics; +pub mod schemas; +pub mod service; +pub mod typed; + +pub use service::{AiError, AiRequest, AiResponse, AiService}; +pub use typed::{complete_text, complete_typed, extract_json_block}; diff --git a/crates/temps-ai/src/schemas.rs b/crates/temps-ai/src/schemas.rs new file mode 100644 index 000000000..1d89acad5 --- /dev/null +++ b/crates/temps-ai/src/schemas.rs @@ -0,0 +1,91 @@ +//! Reusable structured-output schemas. +//! +//! Types here derive `serde::{Serialize, Deserialize}` + `schemars::JsonSchema` +//! so they can be used as the `T` in [`crate::complete_typed`] by any crate, and +//! serialized back out to the API/UI. Keep them small and flat — wide/deeply +//! nested schemas degrade structured-output reliability. + +use serde::{Deserialize, Serialize}; + +/// Best-fit category for a diagnosed build/deployment failure. A constrained +/// enum (rather than a free string) so the schema pins the model to known +/// buckets and consumers can branch/colour on it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum FailureCategory { + /// Bad/invalid Dockerfile instruction, base image, or build context. + Dockerfile, + /// Missing/incompatible/unresolved dependency or package. + Dependency, + /// Source compilation / type / lint error. + Compilation, + /// Misconfiguration: env vars, ports, build args, paths. + Configuration, + /// Network failure: registry, package mirror, DNS, timeout. + Network, + /// Killed for exceeding a memory limit (OOM). + OutOfMemory, + /// Permission / ownership / access denied. + Permissions, + /// Other resource limit: disk, CPU time, file descriptors. + Resource, + /// App started but crashed/failed health checks at runtime. + Runtime, + /// Couldn't be classified from the available evidence. + Unknown, +} + +/// A structured diagnosis of a failed build or deployment — produced by the AI +/// foundation from a log tail, consumed by the deploy UI / error tracking. +/// +/// Reusable across producers (the deployer on build/deploy failure) and +/// consumers (rendering the diagnosis, attaching it to a deployment, surfacing +/// it in error tracking). +#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct ErrorDiagnosis { + /// One-sentence, plain-English summary of what went wrong. + pub summary: String, + /// The single most likely root cause. + pub likely_cause: String, + /// Ordered, concrete remediation steps the developer can act on. + pub suggested_fixes: Vec, + /// Best-fit category. + pub category: FailureCategory, + /// The key line(s) from the log that evidence the diagnosis, copied + /// verbatim so the developer can locate them. + #[serde(default)] + pub key_log_lines: Vec, + /// Confidence in the diagnosis, 0.0–1.0. + pub confidence: f32, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_diagnosis_roundtrips() { + let json = serde_json::json!({ + "summary": "Build failed: missing dependency 'libpq'.", + "likely_cause": "The base image lacks libpq-dev required by the pg crate.", + "suggested_fixes": ["Add `apt-get install -y libpq-dev` before the build step."], + "category": "dependency", + "key_log_lines": ["error: could not find system library 'libpq'"], + "confidence": 0.82 + }); + let d: ErrorDiagnosis = serde_json::from_value(json).unwrap(); + assert_eq!(d.category, FailureCategory::Dependency); + assert_eq!(d.suggested_fixes.len(), 1); + assert!((d.confidence - 0.82).abs() < 1e-6); + } + + #[test] + fn test_schema_is_derivable() { + // The schema must build (used as the `T` in complete_typed). + let schema = serde_json::to_value(schemars::schema_for!(ErrorDiagnosis)).unwrap(); + assert_eq!(schema["title"], "ErrorDiagnosis"); + // The category enum is constrained in the schema. + let props = &schema["properties"]; + assert!(props.get("category").is_some()); + } +} diff --git a/crates/temps-core/src/ai/mod.rs b/crates/temps-ai/src/service.rs similarity index 73% rename from crates/temps-core/src/ai/mod.rs rename to crates/temps-ai/src/service.rs index 9733dc0f9..63f2211b9 100644 --- a/crates/temps-core/src/ai/mod.rs +++ b/crates/temps-ai/src/service.rs @@ -1,19 +1,4 @@ -//! The Temps AI foundation (ADR-022). -//! -//! A single, governed, provider-agnostic way for any crate to ask the configured -//! model for either free text or **typed, structured data**. The object-safe -//! [`AiService`] trait is registered + resolved through the plugin DI as -//! `Arc` (like [`crate::notifications::NotificationService`]); the -//! generic [`complete_typed`]/[`complete_text`] helpers in [`typed`] add the -//! ergonomic, schema-derived API on top. -//! -//! Everything here is best-effort: the trait returns [`Result`], the helpers -//! return [`Option`], and AI must never sit on a path that can block or fail a -//! core operation — callers wrap calls in a timeout. - -pub mod typed; - -pub use typed::{complete_text, complete_typed, extract_json_block}; +//! The object-safe [`AiService`] seam and its request/response types. use async_trait::async_trait; use serde::{Deserialize, Serialize}; @@ -27,7 +12,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct AiRequest { /// Short tag for logging, usage attribution, and per-purpose budgets, - /// e.g. `"alert.summary"` or `"deploy.risk"`. + /// e.g. `"alert.summary"` or `"deploy.build_diagnosis"`. pub purpose: String, /// Optional governance + usage scope (per-project budgets / allow-lists). pub project_id: Option, @@ -42,8 +27,8 @@ pub struct AiRequest { /// Sampling temperature (provider default when `None`). pub temperature: Option, /// When set, the provider is asked to return JSON matching this JSON Schema. - /// Usually populated by [`complete_typed`] from a Rust type rather than by - /// hand. + /// Usually populated by [`crate::complete_typed`] from a Rust type rather than + /// by hand. pub response_schema: Option, } @@ -87,7 +72,7 @@ pub trait AiService: Send + Sync { /// caller skip prompt construction when AI is unavailable. async fn is_available(&self) -> bool; - /// Low-level completion. Prefer the [`complete_text`] / [`complete_typed`] - /// helpers for everyday use. + /// Low-level completion. Prefer the [`crate::complete_text`] / + /// [`crate::complete_typed`] helpers for everyday use. async fn complete(&self, request: AiRequest) -> Result; } diff --git a/crates/temps-core/src/ai/typed.rs b/crates/temps-ai/src/typed.rs similarity index 98% rename from crates/temps-core/src/ai/typed.rs rename to crates/temps-ai/src/typed.rs index 13c725c99..c5395a7a1 100644 --- a/crates/temps-core/src/ai/typed.rs +++ b/crates/temps-ai/src/typed.rs @@ -1,4 +1,4 @@ -//! Ergonomic, schema-derived helpers over the object-safe [`AiService`](super::AiService). +//! Ergonomic, schema-derived helpers over the object-safe [`AiService`](crate::AiService). //! //! Object-safety forbids generic methods on the trait object, so the typed API //! lives here as free functions: [`complete_typed`] derives a JSON Schema from a @@ -6,7 +6,7 @@ use serde::de::DeserializeOwned; -use super::{AiRequest, AiService}; +use crate::service::{AiRequest, AiService}; /// Plain-text completion. `None` if AI is unavailable or the call fails. pub async fn complete_text(ai: &dyn AiService, request: AiRequest) -> Option { @@ -110,7 +110,7 @@ fn balanced_span(s: &str, open: char, close: char) -> Option<&str> { #[cfg(test)] mod tests { use super::*; - use crate::ai::{AiError, AiResponse}; + use crate::service::{AiError, AiResponse}; use async_trait::async_trait; /// A mock that returns a canned text reply. diff --git a/crates/temps-core/Cargo.toml b/crates/temps-core/Cargo.toml index ea4420001..0788eb427 100644 --- a/crates/temps-core/Cargo.toml +++ b/crates/temps-core/Cargo.toml @@ -17,7 +17,6 @@ serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } thiserror = { workspace = true } -schemars = { workspace = true } anyhow = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } diff --git a/crates/temps-core/src/lib.rs b/crates/temps-core/src/lib.rs index 0a839caee..69ca4045c 100644 --- a/crates/temps-core/src/lib.rs +++ b/crates/temps-core/src/lib.rs @@ -1,7 +1,6 @@ //! Core utilities and types shared across all Temps crates pub mod admin_gate; -pub mod ai; pub mod audit; pub mod config; pub mod deployment; diff --git a/crates/temps-otel/Cargo.toml b/crates/temps-otel/Cargo.toml index dfa16c5e6..6c80a8839 100644 --- a/crates/temps-otel/Cargo.toml +++ b/crates/temps-otel/Cargo.toml @@ -10,6 +10,7 @@ homepage.workspace = true [dependencies] # Internal crates temps-core = { path = "../temps-core" } +temps-ai = { path = "../temps-ai" } temps-config = { path = "../temps-config" } temps-database = { path = "../temps-database" } temps-entities = { path = "../temps-entities" } diff --git a/crates/temps-otel/src/plugin.rs b/crates/temps-otel/src/plugin.rs index 95af758d8..0a10cfbbd 100644 --- a/crates/temps-otel/src/plugin.rs +++ b/crates/temps-otel/src/plugin.rs @@ -531,7 +531,7 @@ impl TempsPlugin for OtelPlugin { )); // ADR-022: optional general AI foundation, registered by the AI // gateway plugin when present. Absent -> deterministic Tier-1 text. - let ai = context.get_service::(); + let ai = context.get_service::(); let evaluator = Arc::new(crate::services::MetricAlertEvaluator::new( metric_alert_service.clone(), otel_service.clone(), diff --git a/crates/temps-otel/src/services/metric_alert_evaluator.rs b/crates/temps-otel/src/services/metric_alert_evaluator.rs index c1babf197..b84752d64 100644 --- a/crates/temps-otel/src/services/metric_alert_evaluator.rs +++ b/crates/temps-otel/src/services/metric_alert_evaluator.rs @@ -340,7 +340,7 @@ fn alert_summary_request( rule: &AlertRule, metadata: &serde_json::Value, deterministic_summary: &str, -) -> Option { +) -> Option { let num = |k: &str| metadata.get(k).and_then(serde_json::Value::as_f64); let value = num("value")?; let mut facts = String::new(); @@ -376,7 +376,7 @@ fn alert_summary_request( "Deterministic summary for reference: {deterministic_summary}" )); - Some(temps_core::ai::AiRequest { + Some(temps_ai::AiRequest { purpose: "alert.summary".to_string(), project_id: Some(rule.project_id), system: Some(ALERT_SUMMARY_SYSTEM.to_string()), @@ -675,7 +675,7 @@ pub struct MetricAlertEvaluator { /// deterministic Tier-1 text; when present (and the project opts in) it is /// called inside a timeout on each fire and may replace the lead sentence, /// never block or fail the alert. - ai: Option>, + ai: Option>, } impl MetricAlertEvaluator { @@ -684,7 +684,7 @@ impl MetricAlertEvaluator { otel_service: Arc, alarm_service: Arc, db: Arc, - ai: Option>, + ai: Option>, ) -> Self { Self { alert_service, @@ -1013,7 +1013,7 @@ impl MetricAlertEvaluator { if let Some(ai) = &self.ai { if self.ai_summaries_enabled(rule.project_id).await && ai.is_available().await { if let Some(req) = alert_summary_request(rule, &metadata, &message) { - let fut = temps_core::ai::complete_text(ai.as_ref(), req); + let fut = temps_ai::complete_text(ai.as_ref(), req); if let Ok(Some(text)) = tokio::time::timeout(AI_SUMMARY_TIMEOUT, fut).await { let text = text.trim(); if !text.is_empty() { From f9d254593a21e2c45aa065a64eed92c7b0f24807 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 16:59:09 +0200 Subject: [PATCH 34/56] docs(adr): ADR-023 persistent AI debugging conversations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A generic ai_conversations + ai_messages model keyed by polymorphic (context_type, context_id) — a resumable chat per interaction, deployment failures first. New temps-ai-chat crate over the temps-ai foundation; adds streaming/multi-turn to AiService; seeds via a per-context provider (deployment provider = diagnose_failure + job logs); SSE message endpoint replaying stored history through the gateway. OSS chat+explain (BYO key, opt-in); EE = proactive cross-signal AiSre. Reuses the dormant workspace_messages schema + Autopilot chat UI. --- docs/adr/023-ai-debugging-conversations.md | 285 +++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 docs/adr/023-ai-debugging-conversations.md diff --git a/docs/adr/023-ai-debugging-conversations.md b/docs/adr/023-ai-debugging-conversations.md new file mode 100644 index 000000000..0aabec751 --- /dev/null +++ b/docs/adr/023-ai-debugging-conversations.md @@ -0,0 +1,285 @@ +--- +title: "ADR-023: Persistent AI debugging conversations (chat per interaction)" +status: Proposed +date: 2026-06-27 +author: David Viejo +--- + +# ADR-023: Persistent AI debugging conversations (chat per interaction) + +**Status:** Proposed +**Date:** 2026-06-27 +**Author:** David Viejo + +## Context + +We want AI-assisted debugging in Temps: **a resumable chat per interaction** — one +per deployment failure first, then generally one per any entity the user is +looking at (alert, error group, …) — that the user can reopen and continue. + +This is a step beyond the one-shot `diagnose_failure` helper (ADR-022): it needs +persisted multi-turn history, streaming replies, context seeding from the entity, +and a clean OSS/EE line. A parallel code survey (6 read-only explorers) established +what exists: + +- **No AI SRE companion is built yet** — only the ADR-022 foundation and a defined + but unemitted `TelemetryEventKind::AiSreConversationStarted` + (`crates/temps-core/src/telemetry.rs:78`). We are building the chat, not + generalizing an existing one. +- **A dormant conversations+messages schema already exists**: `workspace_sessions` + + `workspace_messages` (feature removed) at + `crates/temps-migrations/.../m20260421_000001_squash_apr_post_v006.rs:68-156` — + `messages(session_id FK, role VARCHAR(20), content TEXT, metadata JSONB, created_at)` + with a `(session_id, created_at)` composite index. A ready template. +- **Streaming is production-ready** in the gateway: + `GatewayService::chat_completion_stream` (`gateway_service.rs:146`) → + `Stream>`; the HTTP handler streams SSE via + `Body::from_stream` with usage tracking on stream-end (`handlers/gateway.rs:107-195,409-473`). + Multi-turn is already native (`ChatCompletionRequest.messages: Vec`, + roles system/user/assistant/tool). But **`temps-ai`'s `AiService` is + request/response only** (`crates/temps-ai/src/service.rs:69-78`) — it needs a + streaming, multi-turn method. +- **The deploy-failure path is OSS and chat-friendly**: `deployments.state` + (`failed`) + `cancelled_reason`; per-step `deployment_jobs.{status,log_id,error_message}`; + logs via `LogService.get_log_content(log_id)`. Failed deployments are *never* + torn down (`mark_deployment_complete.rs:1531`), so logs survive for long-lived + chats. Routes follow `/projects/{pid}/deployments/{id}/*` + `permission_guard!`. +- **A chat UI pattern exists**: `web/src/components/agents/AutopilotRunDetail.tsx` + (streaming event parse, feedback textarea, tokens/cost, `MessageSquare`), plus a + generated SSE client `web/src/api/client/core/serverSentEvents.gen.ts`. + `web/src/pages/DeploymentDetails.tsx` is where "Debug with AI" mounts. +- **The Autofixer is OSS** (`crates/temps-agents`: `agent_runs`/`agent_run_logs`, + Claude-CLI `--continue` resume, SSE-via-`get_logs_after` polling) and is the + natural hand-off ("open a fix PR"). + +### Rejected alternatives + +- **Reuse `agent_runs`/`agent_run_logs` for chat storage.** Rejected: that schema + is task-execution-shaped (`phase`, `branch_name`, `pr_url`, `analysis`); logs are + `level`-typed, not `role`-typed turns. Forcing chat into it clutters both. We add + a purpose-built conversations/messages schema and *link* to an autofixer run when + a chat delegates a fix. +- **One bespoke table per context** (`deployment_debug_chats`, `alert_chats`, …). + Rejected: the requirement is "a chat for *every* interaction." A polymorphic + `(context_type, context_id)` generalizes without N tables. +- **Provider-side session resume** (Claude-CLI `--continue`, as the autofixer does). + Rejected for the gateway chat: it's sandbox/CLI-specific. The BYO-key gateway is + OpenAI-compatible and stateless, so we **replay stored history** each turn + (send `messages[]`), which is provider-agnostic and the source of truth is our DB. + +## Decision + +**Add a generic, persisted, streaming conversation system — `ai_conversations` + +`ai_messages` keyed by a polymorphic `(context_type, context_id)` — in a new +`temps-ai-chat` crate built on the `temps-ai` foundation. Seed each conversation +from its entity via a `ConversationContextProvider` (deployment-failure first), +stream replies over SSE by replaying stored history through the gateway, and keep +the OSS/EE line: chat + explain is OSS (BYO key, opt-in); proactive cross-signal +investigation is EE.** + +### 1. Data model (`temps-entities` + migration) + +Generalize the dormant `workspace_messages` shape: + +```sql +CREATE TABLE ai_conversations ( + id BIGSERIAL PRIMARY KEY, + public_id TEXT NOT NULL UNIQUE, -- url-safe id + project_id INT NOT NULL, + context_type TEXT NOT NULL, -- 'deployment' | 'alert' | 'error_group' | 'general' + context_id TEXT NOT NULL, -- the entity id (string; ints stringified) + title TEXT, -- auto-summarized from first turn + status TEXT NOT NULL DEFAULT 'active',-- 'active' | 'archived' + created_by INT, -- user id (nullable for system-started) + metadata JSONB, -- seed refs (log_ids, deployment state, etc.) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_activity_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX idx_ai_conversations_context ON ai_conversations(project_id, context_type, context_id); + +CREATE TABLE ai_messages ( + id BIGSERIAL PRIMARY KEY, + conversation_id BIGINT NOT NULL REFERENCES ai_conversations(id) ON DELETE CASCADE, + role TEXT NOT NULL, -- 'system' | 'user' | 'assistant' | 'tool' + content TEXT NOT NULL, + metadata JSONB, -- structured diagnosis, tool calls, citations + tokens_in INT, + tokens_out INT, + cost_microcents BIGINT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX idx_ai_messages_conversation ON ai_messages(conversation_id, created_at); +``` + +`(context_type, context_id)` makes "a chat per interaction" a lookup, not a new +table per surface. Per-message `tokens_*`/`cost_microcents` give per-conversation +cost (user-controlled spend, §5). + +### 2. `temps-ai` gains streaming + multi-turn + +`AiService` today is single request/response. Add a multi-turn streaming method +(object-safe; the gateway already supports both): + +```rust +pub struct ChatTurnRequest { + pub purpose: String, + pub project_id: Option, + pub messages: Vec, // {role, content} — full replayed history + pub model: Option, + pub max_tokens: Option, + pub temperature: Option, +} +pub type TokenStream = Pin> + Send>>; + +#[async_trait] +pub trait AiService { + // … existing is_available / complete … + async fn chat_stream(&self, req: ChatTurnRequest) -> Result; +} +``` + +`GatewayAiService::chat_stream` calls `chat_completion_stream`, parses the +OpenAI SSE chunks (`ChatCompletionChunk.choices[].delta.content`) into a token +stream — reusing the gateway's existing SSE buffering. `ChatMessage`/role types +move to (or are re-exported from) `temps-ai` so consumers don't depend on the +gateway. + +### 3. `temps-ai-chat` crate — the conversation service + +New crate depending on `temps-ai` + `temps-entities` (not the gateway directly; +it gets `Arc` via DI): + +- `ConversationService`: create/find-by-context/get-history/append-message; + `send_message(conversation, user_text) -> TokenStream` that (a) loads history, + (b) prepends the context provider's fresh system context, (c) `ai.chat_stream(...)`, + (d) persists the assistant message on stream-end (mirroring the gateway's + usage-tracking-on-end at `handlers/gateway.rs:154-194`). +- **`ConversationContextProvider` trait** (DI-registered, keyed by `context_type`): + + ```rust + #[async_trait] + pub trait ConversationContextProvider: Send + Sync { + fn context_type(&self) -> &'static str; // "deployment" + async fn authorize(&self, project_id: i32, context_id: &str, auth: &AuthCtx) -> bool; + async fn seed(&self, project_id: i32, context_id: &str) -> ConversationSeed; // system prompt + first assistant msg + async fn live_context(&self, project_id: i32, context_id: &str) -> String; // refreshed each turn + } + ``` + + The **deployment provider** (first) lives near `temps-deployments`: `seed` pulls + the failed `deployment_jobs` logs via `LogService`, runs `temps_ai::diagnostics::diagnose_failure`, + and returns `{ system: failure-context, first_assistant: rendered diagnosis }`. + Adding alerts/error-groups later = another provider, no schema change. + +### 4. HTTP API (OSS surface) + +Under `/projects/{project_id}/ai/conversations` (auth + `permission_guard!`, the +provider's `authorize` enforces context-level access): + +- `GET ?context_type=&context_id=` → find/list (so a page opens the existing chat). +- `POST` `{context_type, context_id}` → create (idempotent per context; lazy — created on first open). +- `GET /{id}` → full message history. +- `POST /{id}/messages` `{content}` → **SSE stream** of the assistant reply + (`text/event-stream`, mirroring `handlers/gateway.rs` / `log_handler.rs` SSE), then + persisted. Gated: `ai.is_available()` + the per-project opt-in (§5) → 409/empty if off. +- `POST /{id}/archive`, `DELETE /{id}` (audit-logged). + +### 5. User control + OSS/EE boundary + +Per ADR-022 and the pricing-honesty stance: + +- **Off until configured + opted in.** Capability gate `ai.is_available()` (a + provider key exists); feature gate a per-project toggle (`projects.ai_debug_chat_enabled`, + tri-state like `ai_alert_summaries_enabled`); spend is governed by `ai_gateway_config` + and surfaced per-conversation via the `tokens/cost` columns. It's the user's key, + cost, and scope. +- **OSS** = the conversation system + the deployment-failure debugging chat + (explain, Q&A, with the user's own key). This is the adoption hook; gating it + behind EE would repel. +- **EE (`Feature::AiSre`)** = the *proactive, cross-signal investigator*: a chat + that autonomously pulls correlated traces/metrics/errors, reasons across signals, + and runs unattended — the `AiSreConversationStarted` event already reserves the + name. Same `temps-ai-chat` substrate; the EE crate adds richer context providers + + autonomy. +- **Hand-off to the Autofixer** ("open a fix PR") stays OSS (the autofixer is + un-gated) — the chat can spawn an autofixer run and link it via + `ai_conversations.metadata.autofixer_run_id`. + +### 6. Frontend + +- A reusable `` mounted on + `DeploymentDetails.tsx` (a "Debug with AI" button on failed deployments), built + by mirroring `AutopilotRunDetail.tsx`'s message list + feedback textarea and + consuming the SSE message endpoint via the generated `serverSentEvents.gen.ts` + client. New endpoints are defined in the backend OpenAPI and **consumed through + the regenerated SDK** (never hand-rolled fetch). +- The same component later mounts on alert/error-group pages by passing a + different `contextType`. + +## Consequences + +### Positive +- One resumable chat per interaction, generic across entities, with persisted + history and streaming — the requested model. +- Reuses a dormant-but-proven schema, the gateway's streaming, the autofixer's + SSE/hand-off, and the Autopilot chat UI — little net-new plumbing. +- Clean layering: `temps-ai` (foundation + streaming) → `temps-ai-chat` + (conversation + providers) → domain providers + UI. New surfaces = new provider. +- OSS adoption hook with a protected EE upgrade (proactive cross-signal AiSre). + +### Negative / risks +- **Cost on a human-driven loop**: every turn spends the user's tokens. Mitigated + by opt-in, `is_available`, per-project budgets, history truncation (sliding window + + always-keep seed), and visible per-conversation cost. +- **Prompt-injection / data exposure**: logs go into prompts; providers must pass + only intended data and redact secrets in the seed. Document the trust boundary. +- **Streaming/persistence consistency**: if the client disconnects mid-stream, the + partial assistant message must still persist (reuse the gateway's on-end spawn). +- **History growth**: long chats need truncation for the model and pagination for + the UI; the `(conversation_id, created_at)` index supports both. +- **New crate + 2 tables + streaming + UI** is a multi-layer feature; phase it. + +### Neutral +- No behaviour change when no key is configured or the toggle is off — the chat UI + simply doesn't offer itself. + +## Phased plan + +1. **P1 — deployment-failure chat (OSS, end-to-end):** migration + entities + (`ai_conversations`/`ai_messages`); `chat_stream` in `temps-ai` + gateway; + `temps-ai-chat` crate (service + provider trait); the deployment context + provider (seed = `diagnose_failure` + job logs); the 4 endpoints (SSE send); + `projects.ai_debug_chat_enabled` toggle; `` on `DeploymentDetails`. +2. **P2 — generalize:** alert + error-group providers (no schema change); the chat + component mounted on those pages; conversation list/archive UI. +3. **P3 — autonomy + EE:** "open a fix PR" hand-off to the Autofixer (OSS); + EE `Feature::AiSre` proactive cross-signal investigator providers. + +## Open questions +1. **Toggle exposure**: ship `ai_debug_chat_enabled` settable via the project API + + a settings switch in P1, or SQL-only until P2? (Recommend P1 — it's the control surface.) +2. **History truncation policy**: fixed sliding window of last N turns + always the + seed, or token-budget-based? Summarize older turns into the seed? +3. **Auto-start vs lazy**: create the deployment chat eagerly on failure (so a + notification can deep-link) or lazily on first open? (Recommend lazy + a "Debug + with AI" entry point; eager later if we deep-link from alerts.) +4. **Drop the dormant `workspace_*` tables** in the same migration, or leave them? +5. **Default model** for chat — reuse `ai_gateway_config.allowed_models[0]` (ADR-022) + or a dedicated chat model setting? + +## References +- ADR-022 (AI foundation) — `temps-ai` `AiService`, `complete_typed`, `diagnose_failure`; + this builds the streaming/multi-turn + conversation layer on top. +- `crates/temps-migrations/.../m20260421_000001_squash_apr_post_v006.rs:68-156` — + dormant `workspace_sessions`/`workspace_messages` schema (the template). +- `crates/temps-ai-gateway/src/services/gateway_service.rs:146` — `chat_completion_stream`; + `src/handlers/gateway.rs:107-195,409-473` — SSE streaming + usage-on-end. +- `crates/temps-ai/src/service.rs:69-78` — `AiService` (needs `chat_stream`). +- `crates/temps-entities/src/{deployments.rs,deployment_jobs.rs}` — failure state + + `log_id`; `LogService.get_log_content()` for seed context. +- `crates/temps-agents` — Autofixer (`agent_runs`/`agent_run_logs`, `--continue`, + `get_logs_after` SSE) — hand-off target; OSS, un-gated. +- `web/src/components/agents/AutopilotRunDetail.tsx` (chat UI to mirror), + `web/src/api/client/core/serverSentEvents.gen.ts` (SSE client), + `web/src/pages/DeploymentDetails.tsx` (mount point). +- `crates/temps-core/src/telemetry.rs:78` — reserved `AiSreConversationStarted` (EE). From 48610b5ae9ce369122874a1e93412f36bf4c2132 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 17:11:52 +0200 Subject: [PATCH 35/56] feat(ai): add multi-turn streaming (chat_stream) to AiService (ADR-023) temps-ai gains ChatMessage/ChatTurnRequest/TokenStream + AiService::chat_stream (replays full history, streams the assistant reply token-by-token). GatewayAiService implements it over chat_completion_stream, parsing the OpenAI-SSE byte stream into text deltas. --- Cargo.lock | 1 + .../src/services/ai_service.rs | 87 ++++++++++++++++++- crates/temps-ai/Cargo.toml | 1 + crates/temps-ai/src/diagnostics.rs | 7 ++ crates/temps-ai/src/lib.rs | 2 + crates/temps-ai/src/service.rs | 7 ++ crates/temps-ai/src/streaming.rs | 69 +++++++++++++++ crates/temps-ai/src/typed.rs | 7 ++ 8 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 crates/temps-ai/src/streaming.rs diff --git a/Cargo.lock b/Cargo.lock index 4c58a1fc9..c3317f59f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10644,6 +10644,7 @@ name = "temps-ai" version = "0.1.0-beta.39" dependencies = [ "async-trait", + "futures", "schemars 0.8.22", "serde", "serde_json", diff --git a/crates/temps-ai-gateway/src/services/ai_service.rs b/crates/temps-ai-gateway/src/services/ai_service.rs index 2ac8d09d4..3063c7767 100644 --- a/crates/temps-ai-gateway/src/services/ai_service.rs +++ b/crates/temps-ai-gateway/src/services/ai_service.rs @@ -9,13 +9,16 @@ use std::sync::Arc; use async_trait::async_trait; +use futures_util::StreamExt; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; use tracing::debug; -use temps_ai::{AiError, AiRequest, AiResponse, AiService}; +use temps_ai::{AiError, AiRequest, AiResponse, AiService, ChatTurnRequest, TokenStream}; use crate::services::{ByokOverride, GatewayService}; -use crate::types::{ChatCompletionRequest, ChatCompletionResponse, MessageContent}; +use crate::types::{ + ChatCompletionChunk, ChatCompletionRequest, ChatCompletionResponse, MessageContent, +}; /// Gateway-backed [`AiService`] (ADR-022). pub struct GatewayAiService { @@ -157,6 +160,86 @@ impl AiService for GatewayAiService { model, }) } + + async fn chat_stream(&self, request: ChatTurnRequest) -> Result { + let model = self + .resolve_model(request.project_id, request.model.as_deref()) + .await + .ok_or_else(|| AiError::NoModel { + purpose: request.purpose.clone(), + })?; + + let messages: Vec = request + .messages + .iter() + .map(|m| serde_json::json!({ "role": m.role, "content": m.content })) + .collect(); + let mut body = serde_json::json!({ "model": model, "messages": messages, "stream": true }); + if let Some(mt) = request.max_tokens { + body["max_tokens"] = serde_json::json!(mt); + } + if let Some(t) = request.temperature { + body["temperature"] = serde_json::json!(t); + } + let chat_req: ChatCompletionRequest = + serde_json::from_value(body).map_err(|e| AiError::Provider { + purpose: request.purpose.clone(), + reason: format!("malformed request: {e}"), + })?; + + let purpose = request.purpose.clone(); + let (byte_stream, _cred) = self + .gateway + .chat_completion_stream(&chat_req, &ByokOverride::default()) + .await + .map_err(|e| AiError::Provider { + purpose: purpose.clone(), + reason: e.to_string(), + })?; + + // Parse the gateway's OpenAI-format SSE byte stream into assistant text + // deltas. `data:` lines may split across byte chunks, so buffer to line + // boundaries; `data: [DONE]` terminates. + let token_stream = async_stream::stream! { + let mut byte_stream = byte_stream; + let mut buf = String::new(); + while let Some(item) = byte_stream.next().await { + match item { + Ok(bytes) => { + buf.push_str(&String::from_utf8_lossy(&bytes)); + while let Some(nl) = buf.find('\n') { + let line: String = buf.drain(..=nl).collect(); + let line = line.trim(); + let Some(data) = line.strip_prefix("data:") else { continue }; + let data = data.trim(); + if data == "[DONE]" { + return; + } + if let Ok(chunk) = serde_json::from_str::(data) { + if let Some(content) = chunk + .choices + .first() + .and_then(|c| c.delta.content.as_ref()) + { + if !content.is_empty() { + yield Ok(content.clone()); + } + } + } + } + } + Err(e) => { + yield Err(AiError::Provider { + purpose: purpose.clone(), + reason: e.to_string(), + }); + return; + } + } + } + }; + Ok(Box::pin(token_stream)) + } } #[cfg(test)] diff --git a/crates/temps-ai/Cargo.toml b/crates/temps-ai/Cargo.toml index 22efa139a..ef77c37be 100644 --- a/crates/temps-ai/Cargo.toml +++ b/crates/temps-ai/Cargo.toml @@ -17,6 +17,7 @@ schemars = { workspace = true } async-trait = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +futures = { workspace = true } [dev-dependencies] tokio = { workspace = true } diff --git a/crates/temps-ai/src/diagnostics.rs b/crates/temps-ai/src/diagnostics.rs index aba8f6a68..674bbd7e1 100644 --- a/crates/temps-ai/src/diagnostics.rs +++ b/crates/temps-ai/src/diagnostics.rs @@ -155,6 +155,13 @@ mod tests { model: "mock".into(), }) } + async fn chat_stream( + &self, + _r: crate::streaming::ChatTurnRequest, + ) -> Result { + let text = self.0.clone(); + Ok(Box::pin(futures::stream::once(async move { Ok(text) }))) + } } #[tokio::test] diff --git a/crates/temps-ai/src/lib.rs b/crates/temps-ai/src/lib.rs index 57e02944d..2beb9eb57 100644 --- a/crates/temps-ai/src/lib.rs +++ b/crates/temps-ai/src/lib.rs @@ -21,7 +21,9 @@ pub mod diagnostics; pub mod schemas; pub mod service; +pub mod streaming; pub mod typed; pub use service::{AiError, AiRequest, AiResponse, AiService}; +pub use streaming::{ChatMessage, ChatTurnRequest, TokenStream}; pub use typed::{complete_text, complete_typed, extract_json_block}; diff --git a/crates/temps-ai/src/service.rs b/crates/temps-ai/src/service.rs index 63f2211b9..6cc62a7d1 100644 --- a/crates/temps-ai/src/service.rs +++ b/crates/temps-ai/src/service.rs @@ -3,6 +3,8 @@ use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use crate::streaming::{ChatTurnRequest, TokenStream}; + /// A single AI completion request. Construct with `..Default::default()` and set /// only what you need: /// @@ -75,4 +77,9 @@ pub trait AiService: Send + Sync { /// Low-level completion. Prefer the [`crate::complete_text`] / /// [`crate::complete_typed`] helpers for everyday use. async fn complete(&self, request: AiRequest) -> Result; + + /// Multi-turn streaming completion (ADR-023): replays the supplied history + /// and streams the assistant reply token-by-token. The substrate for + /// persistent debugging conversations. Best-effort like [`Self::complete`]. + async fn chat_stream(&self, request: ChatTurnRequest) -> Result; } diff --git a/crates/temps-ai/src/streaming.rs b/crates/temps-ai/src/streaming.rs new file mode 100644 index 000000000..0874f0f28 --- /dev/null +++ b/crates/temps-ai/src/streaming.rs @@ -0,0 +1,69 @@ +//! Multi-turn, streaming chat types for the AI foundation (ADR-023). +//! +//! Where [`crate::AiService::complete`] is a single request→response, +//! [`crate::AiService::chat_stream`] takes a replayed message history and streams +//! the assistant's reply token-by-token — the substrate for persistent, +//! resumable debugging conversations. + +use std::pin::Pin; + +use futures::Stream; +use serde::{Deserialize, Serialize}; + +use crate::service::AiError; + +/// Who authored a turn. Mirrors the OpenAI-compatible roles the gateway accepts. +pub const ROLE_SYSTEM: &str = "system"; +pub const ROLE_USER: &str = "user"; +pub const ROLE_ASSISTANT: &str = "assistant"; + +/// One turn of a conversation. Deliberately flat (`role` + text `content`) so it +/// is provider-agnostic and trivially persisted as an `ai_messages` row. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatMessage { + /// `"system"` | `"user"` | `"assistant"`. + pub role: String, + pub content: String, +} + +impl ChatMessage { + pub fn system(content: impl Into) -> Self { + Self { + role: ROLE_SYSTEM.into(), + content: content.into(), + } + } + pub fn user(content: impl Into) -> Self { + Self { + role: ROLE_USER.into(), + content: content.into(), + } + } + pub fn assistant(content: impl Into) -> Self { + Self { + role: ROLE_ASSISTANT.into(), + content: content.into(), + } + } +} + +/// A multi-turn streaming request. The caller supplies the *full* replayed +/// history (our DB is the source of truth — see ADR-023); the provider is +/// stateless. +#[derive(Debug, Clone, Default)] +pub struct ChatTurnRequest { + /// Short tag for logging / usage attribution, e.g. `"deploy.debug_chat"`. + pub purpose: String, + /// Governance + usage scope. + pub project_id: Option, + /// Full conversation history, oldest first (system prompt usually first). + pub messages: Vec, + /// Override the configured default model. + pub model: Option, + pub max_tokens: Option, + pub temperature: Option, +} + +/// A stream of assistant text deltas. Each `Ok(String)` is an incremental chunk +/// to append; the stream ends when the reply is complete. Errors are terminal. +pub type TokenStream = Pin> + Send>>; diff --git a/crates/temps-ai/src/typed.rs b/crates/temps-ai/src/typed.rs index c5395a7a1..df1790f70 100644 --- a/crates/temps-ai/src/typed.rs +++ b/crates/temps-ai/src/typed.rs @@ -128,6 +128,13 @@ mod tests { model: "mock".into(), }) } + async fn chat_stream( + &self, + _request: crate::streaming::ChatTurnRequest, + ) -> Result { + let text = self.0.clone(); + Ok(Box::pin(futures::stream::once(async move { Ok(text) }))) + } } #[derive(serde::Deserialize, schemars::JsonSchema, PartialEq, Debug)] From 3ac9d53816cefbe89fe89f0f0d4ded6fb979ec0a Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 17:20:39 +0200 Subject: [PATCH 36/56] feat(ai): ai_conversations + ai_messages schema + project toggle (ADR-023) Migration m20260627_000002: generic conversation store keyed by polymorphic (context_type, context_id) + append-only ai_messages (role/content, per-turn token/cost), FK cascade, context + ordering indexes. Adds projects.ai_debug_chat_enabled opt-in toggle. New sea-orm entities. --- .../src/services/config_service.rs | 1 + crates/temps-agents/src/services/executor.rs | 1 + crates/temps-entities/src/ai_conversations.rs | 36 ++++ crates/temps-entities/src/ai_messages.rs | 30 +++ crates/temps-entities/src/lib.rs | 2 + crates/temps-entities/src/projects.rs | 3 + ...20260627_000002_create_ai_conversations.rs | 191 ++++++++++++++++++ crates/temps-migrations/src/migration/mod.rs | 2 + .../src/vulnerability_notifications.rs | 1 + 9 files changed, 267 insertions(+) create mode 100644 crates/temps-entities/src/ai_conversations.rs create mode 100644 crates/temps-entities/src/ai_messages.rs create mode 100644 crates/temps-migrations/src/migration/m20260627_000002_create_ai_conversations.rs diff --git a/crates/temps-agents/src/services/config_service.rs b/crates/temps-agents/src/services/config_service.rs index f62e4b311..dea450b66 100644 --- a/crates/temps-agents/src/services/config_service.rs +++ b/crates/temps-agents/src/services/config_service.rs @@ -1028,6 +1028,7 @@ mod tests { gitlab_webhook_signing_token: None, attack_mode: false, ai_alert_summaries_enabled: None, + ai_debug_chat_enabled: None, enable_preview_environments: false, preview_envs_on_demand: false, preview_envs_idle_timeout_seconds: 300, diff --git a/crates/temps-agents/src/services/executor.rs b/crates/temps-agents/src/services/executor.rs index a6cb623b4..672069c35 100644 --- a/crates/temps-agents/src/services/executor.rs +++ b/crates/temps-agents/src/services/executor.rs @@ -4076,6 +4076,7 @@ mod tests { gitlab_webhook_signing_token: None, attack_mode: false, ai_alert_summaries_enabled: None, + ai_debug_chat_enabled: None, enable_preview_environments: true, preview_envs_on_demand: false, preview_envs_idle_timeout_seconds: 300, diff --git a/crates/temps-entities/src/ai_conversations.rs b/crates/temps-entities/src/ai_conversations.rs new file mode 100644 index 000000000..78c4dfe63 --- /dev/null +++ b/crates/temps-entities/src/ai_conversations.rs @@ -0,0 +1,36 @@ +//! Persistent AI debugging conversation (ADR-023). +//! +//! One row per resumable chat, scoped to a project and a polymorphic +//! `(context_type, context_id)` — `deployment` first, then `alert`/`error_group`. +//! The turns live in [`crate::ai_messages`]. + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; +use temps_core::DBDateTime; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "ai_conversations")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + /// URL-safe opaque id used in the API. + pub public_id: String, + pub project_id: i32, + /// `"deployment" | "alert" | "error_group" | "general"`. + pub context_type: String, + /// The attached entity's id (ints stringified). + pub context_id: String, + pub title: Option, + /// `"active" | "archived"`. + pub status: String, + pub created_by: Option, + /// Seed refs (log_ids, deployment state) + e.g. autofixer_run_id on hand-off. + pub metadata: Option, + pub created_at: DBDateTime, + pub last_activity_at: DBDateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/temps-entities/src/ai_messages.rs b/crates/temps-entities/src/ai_messages.rs new file mode 100644 index 000000000..2971f7e4d --- /dev/null +++ b/crates/temps-entities/src/ai_messages.rs @@ -0,0 +1,30 @@ +//! A single turn in an AI debugging conversation (ADR-023). +//! +//! Append-only; ordered by `(conversation_id, created_at)`. The full set is +//! replayed as history on each turn (our DB is the source of truth). + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; +use temps_core::DBDateTime; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "ai_messages")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + pub conversation_id: i64, + /// `"system" | "user" | "assistant" | "tool"`. + pub role: String, + pub content: String, + /// Structured diagnosis, tool calls, citations, seed refs. + pub metadata: Option, + pub tokens_in: Option, + pub tokens_out: Option, + pub cost_microcents: Option, + pub created_at: DBDateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/temps-entities/src/lib.rs b/crates/temps-entities/src/lib.rs index 8371eeb20..a91593776 100644 --- a/crates/temps-entities/src/lib.rs +++ b/crates/temps-entities/src/lib.rs @@ -1,6 +1,8 @@ pub mod acme_accounts; pub mod acme_orders; +pub mod ai_conversations; pub mod ai_gateway_config; +pub mod ai_messages; pub mod ai_provider_keys; pub mod ai_usage_logs; pub mod alarms; diff --git a/crates/temps-entities/src/projects.rs b/crates/temps-entities/src/projects.rs index a3afc4c72..16bbc9e01 100644 --- a/crates/temps-entities/src/projects.rs +++ b/crates/temps-entities/src/projects.rs @@ -44,6 +44,9 @@ pub struct Model { /// this project. NULL/false = off (the deterministic Tier-1 text is used); /// true = enrich with the configured AI provider when one is available. pub ai_alert_summaries_enabled: Option, + /// ADR-023: opt-in AI debugging chat (e.g. on deployment failures) for this + /// project. NULL/false = off; true = offer the chat when AI is configured. + pub ai_debug_chat_enabled: Option, /// Enable automatic preview environment creation for each branch pub enable_preview_environments: bool, /// When true, preview environments auto-created for branches are diff --git a/crates/temps-migrations/src/migration/m20260627_000002_create_ai_conversations.rs b/crates/temps-migrations/src/migration/m20260627_000002_create_ai_conversations.rs new file mode 100644 index 000000000..5cd83cdb0 --- /dev/null +++ b/crates/temps-migrations/src/migration/m20260627_000002_create_ai_conversations.rs @@ -0,0 +1,191 @@ +//! Migration for persistent AI debugging conversations (ADR-023). +//! +//! Two tables — a generic, entity-agnostic conversation store keyed by a +//! polymorphic `(context_type, context_id)` so any interaction (a deployment +//! failure first, alerts/error-groups later) gets one resumable chat. +//! `ai_conversations` is one row per chat (project + context); `ai_messages` +//! holds the turns (role/content), replayed as history each turn. Plus a +//! per-project opt-in toggle `projects.ai_debug_chat_enabled` (NULL/false = off). +//! Generalizes the dormant `workspace_messages` shape. + +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(AiConversations::Table) + .if_not_exists() + .col( + ColumnDef::new(AiConversations::Id) + .big_integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(AiConversations::PublicId) + .text() + .not_null() + .unique_key(), + ) + .col( + ColumnDef::new(AiConversations::ProjectId) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(AiConversations::ContextType) + .text() + .not_null(), + ) + .col(ColumnDef::new(AiConversations::ContextId).text().not_null()) + .col(ColumnDef::new(AiConversations::Title).text()) + .col( + ColumnDef::new(AiConversations::Status) + .text() + .not_null() + .default("active"), + ) + .col(ColumnDef::new(AiConversations::CreatedBy).integer()) + .col(ColumnDef::new(AiConversations::Metadata).json_binary()) + .col( + ColumnDef::new(AiConversations::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(AiConversations::LastActivityAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(AiMessages::Table) + .if_not_exists() + .col( + ColumnDef::new(AiMessages::Id) + .big_integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(AiMessages::ConversationId) + .big_integer() + .not_null(), + ) + .col(ColumnDef::new(AiMessages::Role).text().not_null()) + .col(ColumnDef::new(AiMessages::Content).text().not_null()) + .col(ColumnDef::new(AiMessages::Metadata).json_binary()) + .col(ColumnDef::new(AiMessages::TokensIn).integer()) + .col(ColumnDef::new(AiMessages::TokensOut).integer()) + .col(ColumnDef::new(AiMessages::CostMicrocents).big_integer()) + .col( + ColumnDef::new(AiMessages::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .to_owned(), + ) + .await?; + + // Context lookup ("open the chat for this deployment"), message ordering, + // and the FK (cascade delete a conversation's messages). + manager + .get_connection() + .execute_unprepared( + "CREATE INDEX IF NOT EXISTS idx_ai_conversations_context \ + ON ai_conversations (project_id, context_type, context_id); \ + CREATE INDEX IF NOT EXISTS idx_ai_messages_conversation \ + ON ai_messages (conversation_id, created_at); \ + ALTER TABLE ai_messages \ + ADD CONSTRAINT fk_ai_messages_conversation \ + FOREIGN KEY (conversation_id) REFERENCES ai_conversations (id) ON DELETE CASCADE;", + ) + .await?; + + // Per-project opt-in for the debugging chat (NULL/false = off). + manager + .alter_table( + Table::alter() + .table(Projects::Table) + .add_column( + ColumnDef::new(Projects::AiDebugChatEnabled) + .boolean() + .null(), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Projects::Table) + .drop_column(Projects::AiDebugChatEnabled) + .to_owned(), + ) + .await?; + manager + .drop_table(Table::drop().table(AiMessages::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(AiConversations::Table).to_owned()) + .await?; + Ok(()) + } +} + +#[derive(DeriveIden)] +enum AiConversations { + Table, + Id, + PublicId, + ProjectId, + ContextType, + ContextId, + Title, + Status, + CreatedBy, + Metadata, + CreatedAt, + LastActivityAt, +} + +#[derive(DeriveIden)] +enum AiMessages { + Table, + Id, + ConversationId, + Role, + Content, + Metadata, + TokensIn, + TokensOut, + CostMicrocents, + CreatedAt, +} + +#[derive(DeriveIden)] +enum Projects { + Table, + AiDebugChatEnabled, +} diff --git a/crates/temps-migrations/src/migration/mod.rs b/crates/temps-migrations/src/migration/mod.rs index 21cf69785..28402f1eb 100644 --- a/crates/temps-migrations/src/migration/mod.rs +++ b/crates/temps-migrations/src/migration/mod.rs @@ -125,6 +125,7 @@ mod m20260621_000001_create_telemetry_milestones; mod m20260626_000001_create_metric_dashboards; mod m20260626_000002_create_metric_alert_rules; mod m20260627_000001_add_ai_alert_summaries; +mod m20260627_000002_create_ai_conversations; pub struct Migrator; @@ -253,6 +254,7 @@ impl MigratorTrait for Migrator { Box::new(m20260626_000001_create_metric_dashboards::Migration), Box::new(m20260626_000002_create_metric_alert_rules::Migration), Box::new(m20260627_000001_add_ai_alert_summaries::Migration), + Box::new(m20260627_000002_create_ai_conversations::Migration), ] } } diff --git a/crates/temps-notifications/src/vulnerability_notifications.rs b/crates/temps-notifications/src/vulnerability_notifications.rs index fe7ae7d61..5b1c3619f 100644 --- a/crates/temps-notifications/src/vulnerability_notifications.rs +++ b/crates/temps-notifications/src/vulnerability_notifications.rs @@ -397,6 +397,7 @@ mod tests { gitlab_webhook_signing_token: None, attack_mode: false, ai_alert_summaries_enabled: None, + ai_debug_chat_enabled: None, source_type: temps_entities::source_type::SourceType::Git, }; From f86d5030f2e8342ff333f7ecef3db9642f030e7e Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 17:26:40 +0200 Subject: [PATCH 37/56] =?UTF-8?q?feat(ai):=20temps-ai-chat=20crate=20?= =?UTF-8?q?=E2=80=94=20conversation=20service=20+=20provider=20trait=20(AD?= =?UTF-8?q?R-023)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConversationService: get_or_create (idempotent per context, seeds via the provider), find/history, archive, and streaming send_message (persists the user turn, replays full history through AiService::chat_stream, persists the assistant turn on completion). ConversationContextProvider trait (context_type + authorize + seed) lets each entity type seed its chat. --- Cargo.lock | 20 +++ Cargo.toml | 1 + crates/temps-ai-chat/Cargo.toml | 31 ++++ crates/temps-ai-chat/src/lib.rs | 31 ++++ crates/temps-ai-chat/src/provider.rs | 40 +++++ crates/temps-ai-chat/src/service.rs | 251 +++++++++++++++++++++++++++ 6 files changed, 374 insertions(+) create mode 100644 crates/temps-ai-chat/Cargo.toml create mode 100644 crates/temps-ai-chat/src/lib.rs create mode 100644 crates/temps-ai-chat/src/provider.rs create mode 100644 crates/temps-ai-chat/src/service.rs diff --git a/Cargo.lock b/Cargo.lock index c3317f59f..5d69f314c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10653,6 +10653,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "temps-ai-chat" +version = "0.1.0-beta.39" +dependencies = [ + "async-stream", + "async-trait", + "chrono", + "futures", + "futures-util", + "sea-orm", + "serde", + "serde_json", + "temps-ai", + "temps-entities", + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", +] + [[package]] name = "temps-ai-gateway" version = "0.1.0-beta.39" diff --git a/Cargo.toml b/Cargo.toml index 6cbabf4a4..158471c7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ # Library crates "crates/temps-core", "crates/temps-ai", + "crates/temps-ai-chat", "crates/temps-entities", "crates/temps-migrations", "crates/temps-database", diff --git a/crates/temps-ai-chat/Cargo.toml b/crates/temps-ai-chat/Cargo.toml new file mode 100644 index 000000000..6e6f45646 --- /dev/null +++ b/crates/temps-ai-chat/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "temps-ai-chat" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Persistent, resumable AI debugging conversations attached to any entity (ADR-023)" +keywords = ["ai", "chat", "platform"] +categories = ["development-tools"] + +[dependencies] +temps-ai = { path = "../temps-ai" } +temps-entities = { path = "../temps-entities" } + +sea-orm = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +async-trait = { workspace = true } +async-stream = { workspace = true } +futures = { workspace = true } +futures-util = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true } diff --git a/crates/temps-ai-chat/src/lib.rs b/crates/temps-ai-chat/src/lib.rs new file mode 100644 index 000000000..dabc58ba2 --- /dev/null +++ b/crates/temps-ai-chat/src/lib.rs @@ -0,0 +1,31 @@ +//! Persistent, resumable AI debugging conversations (ADR-023). +//! +//! A generic conversation store keyed by a polymorphic `(context_type, +//! context_id)` — one resumable chat per interaction. [`ConversationService`] +//! owns create/find/history + streaming `send_message`; each context type +//! supplies a [`ConversationContextProvider`] that seeds the chat (the +//! deployment provider seeds from a failure diagnosis). Built on the `temps-ai` +//! foundation; the AI is injected as `Arc`. + +pub mod provider; +pub mod service; + +pub use provider::{ConversationContextProvider, ConversationSeed}; +pub use service::ConversationService; + +/// Errors from the conversation layer. All map cleanly to HTTP at the handler. +#[derive(Debug, thiserror::Error)] +pub enum ChatError { + #[error("conversation '{0}' not found")] + NotFound(String), + #[error("no AI context provider registered for type '{0}'")] + NoProvider(String), + #[error("AI is not configured for this project")] + AiUnavailable, + #[error("context not found or not accessible")] + ContextUnavailable, + #[error("database error: {0}")] + Db(#[from] sea_orm::DbErr), + #[error("AI provider error: {0}")] + Ai(String), +} diff --git a/crates/temps-ai-chat/src/provider.rs b/crates/temps-ai-chat/src/provider.rs new file mode 100644 index 000000000..ce41f98de --- /dev/null +++ b/crates/temps-ai-chat/src/provider.rs @@ -0,0 +1,40 @@ +//! Per-context seeding for conversations. +//! +//! A `ConversationContextProvider` turns an entity reference (`context_type` + +//! `context_id`) into the AI framing for a new chat. Registered one per +//! `context_type`; the deployment provider (in `temps-deployments`) is the +//! first, seeding from a build/deploy failure diagnosis. + +use async_trait::async_trait; + +/// The seed for a new conversation. +#[derive(Debug, Clone, Default)] +pub struct ConversationSeed { + /// System framing: the situation + relevant facts (logs, status). Stored as + /// the conversation's first `system` message and replayed every turn. + pub system: String, + /// Optional first assistant turn shown on open (e.g. the rendered diagnosis), + /// so the chat starts already explaining the problem. + pub first_assistant: Option, + /// Display title for the conversation. + pub title: Option, + /// Provenance refs (log_ids, status) recorded on the conversation row. + pub metadata: Option, +} + +/// Builds the AI context for one kind of entity. +#[async_trait] +pub trait ConversationContextProvider: Send + Sync { + /// The `context_type` this provider handles, e.g. `"deployment"`. + fn context_type(&self) -> &'static str; + + /// Finer-grained authorization for this context (the route already enforces + /// project-level access). Default allow. + async fn authorize(&self, _project_id: i32, _context_id: &str) -> bool { + true + } + + /// Build the seed for a new conversation. `None` if the entity can't be found + /// or has no usable context (e.g. a deployment that didn't fail). + async fn seed(&self, project_id: i32, context_id: &str) -> Option; +} diff --git a/crates/temps-ai-chat/src/service.rs b/crates/temps-ai-chat/src/service.rs new file mode 100644 index 000000000..e4bffc394 --- /dev/null +++ b/crates/temps-ai-chat/src/service.rs @@ -0,0 +1,251 @@ +//! The conversation service: create/find/history + streaming `send_message`. + +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; + +use chrono::Utc; +use futures::Stream; +use futures_util::StreamExt; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, Set, +}; + +use temps_ai::{AiService, ChatMessage, ChatTurnRequest}; +use temps_entities::{ai_conversations, ai_messages}; + +use crate::provider::ConversationContextProvider; +use crate::ChatError; + +/// Owns conversation persistence + AI turn streaming. Construct once with the +/// registered context providers; resolve via the plugin DI. +pub struct ConversationService { + db: Arc, + ai: Arc, + providers: HashMap<&'static str, Arc>, +} + +impl ConversationService { + pub fn new( + db: Arc, + ai: Arc, + providers: Vec>, + ) -> Self { + let providers = providers + .into_iter() + .map(|p| (p.context_type(), p)) + .collect(); + Self { db, ai, providers } + } + + /// Is AI configured at all? (Capability gate; feature opt-in is checked at the handler.) + pub async fn ai_available(&self) -> bool { + self.ai.is_available().await + } + + /// The active conversation for a context, if one exists. + pub async fn find_by_context( + &self, + project_id: i32, + context_type: &str, + context_id: &str, + ) -> Result, ChatError> { + Ok(ai_conversations::Entity::find() + .filter(ai_conversations::Column::ProjectId.eq(project_id)) + .filter(ai_conversations::Column::ContextType.eq(context_type)) + .filter(ai_conversations::Column::ContextId.eq(context_id)) + .filter(ai_conversations::Column::Status.eq("active")) + .one(self.db.as_ref()) + .await?) + } + + /// A conversation by its public id, scoped to the project. + pub async fn get_by_public_id( + &self, + project_id: i32, + public_id: &str, + ) -> Result { + ai_conversations::Entity::find() + .filter(ai_conversations::Column::ProjectId.eq(project_id)) + .filter(ai_conversations::Column::PublicId.eq(public_id)) + .one(self.db.as_ref()) + .await? + .ok_or_else(|| ChatError::NotFound(public_id.to_string())) + } + + /// All turns of a conversation, oldest first. + pub async fn messages( + &self, + conversation_id: i64, + ) -> Result, ChatError> { + Ok(ai_messages::Entity::find() + .filter(ai_messages::Column::ConversationId.eq(conversation_id)) + .order_by_asc(ai_messages::Column::CreatedAt) + .order_by_asc(ai_messages::Column::Id) + .all(self.db.as_ref()) + .await?) + } + + /// Find or create the conversation for a context (idempotent per active + /// context). On create, seeds via the provider: a `system` framing message + /// plus an optional first `assistant` message (e.g. the diagnosis). + pub async fn get_or_create( + &self, + project_id: i32, + context_type: &str, + context_id: &str, + user_id: Option, + ) -> Result { + if let Some(existing) = self + .find_by_context(project_id, context_type, context_id) + .await? + { + return Ok(existing); + } + let provider = self + .providers + .get(context_type) + .ok_or_else(|| ChatError::NoProvider(context_type.to_string()))?; + if !provider.authorize(project_id, context_id).await { + return Err(ChatError::ContextUnavailable); + } + let seed = provider + .seed(project_id, context_id) + .await + .ok_or(ChatError::ContextUnavailable)?; + + let now = Utc::now(); + let conv = ai_conversations::ActiveModel { + public_id: Set(uuid::Uuid::new_v4().simple().to_string()), + project_id: Set(project_id), + context_type: Set(context_type.to_string()), + context_id: Set(context_id.to_string()), + title: Set(seed.title.clone()), + status: Set("active".to_string()), + created_by: Set(user_id), + metadata: Set(seed.metadata.clone()), + created_at: Set(now), + last_activity_at: Set(now), + ..Default::default() + } + .insert(self.db.as_ref()) + .await?; + + self.insert_message(conv.id, "system", &seed.system, None) + .await?; + if let Some(first) = &seed.first_assistant { + self.insert_message(conv.id, "assistant", first, None) + .await?; + } + Ok(conv) + } + + async fn insert_message( + &self, + conversation_id: i64, + role: &str, + content: &str, + metadata: Option, + ) -> Result { + Ok(ai_messages::ActiveModel { + conversation_id: Set(conversation_id), + role: Set(role.to_string()), + content: Set(content.to_string()), + metadata: Set(metadata), + created_at: Set(Utc::now()), + ..Default::default() + } + .insert(self.db.as_ref()) + .await?) + } + + async fn touch(&self, conversation_id: i64) { + let am = ai_conversations::ActiveModel { + id: Set(conversation_id), + last_activity_at: Set(Utc::now()), + ..Default::default() + }; + let _ = am.update(self.db.as_ref()).await; + } + + /// Append a user message and stream the assistant reply. Persists the user + /// message up front and the assistant message when the stream completes + /// (the `system` seed is already the first stored turn, so history replay is + /// the full context). Errors before streaming starts return `Err`; errors + /// mid-stream arrive as a stream item. + pub async fn send_message( + &self, + conv: &ai_conversations::Model, + user_text: &str, + ) -> Result> + Send>>, ChatError> { + if !self.ai.is_available().await { + return Err(ChatError::AiUnavailable); + } + self.insert_message(conv.id, "user", user_text, None) + .await?; + self.touch(conv.id).await; + + let history = self.messages(conv.id).await?; + let messages: Vec = history + .iter() + .filter(|m| matches!(m.role.as_str(), "system" | "user" | "assistant")) + .map(|m| ChatMessage { + role: m.role.clone(), + content: m.content.clone(), + }) + .collect(); + + let req = ChatTurnRequest { + purpose: format!("chat.{}", conv.context_type), + project_id: Some(conv.project_id), + messages, + ..Default::default() + }; + let mut token_stream = self + .ai + .chat_stream(req) + .await + .map_err(|e| ChatError::Ai(e.to_string()))?; + + let db = self.db.clone(); + let conv_id = conv.id; + let out = async_stream::stream! { + let mut acc = String::new(); + while let Some(item) = token_stream.next().await { + match item { + Ok(tok) => { + acc.push_str(&tok); + yield Ok(tok); + } + Err(e) => { + yield Err(ChatError::Ai(e.to_string())); + break; + } + } + } + // Persist the assistant turn once the reply is complete. + if !acc.is_empty() { + let am = ai_messages::ActiveModel { + conversation_id: Set(conv_id), + role: Set("assistant".to_string()), + content: Set(acc), + created_at: Set(Utc::now()), + ..Default::default() + }; + let _ = am.insert(db.as_ref()).await; + } + }; + Ok(Box::pin(out)) + } + + /// Archive a conversation (soft delete). + pub async fn archive(&self, conv: &ai_conversations::Model) -> Result<(), ChatError> { + let am = ai_conversations::ActiveModel { + id: Set(conv.id), + status: Set("archived".to_string()), + ..Default::default() + }; + am.update(self.db.as_ref()).await?; + Ok(()) + } +} From ec19a258ddd0b7461b11741c58f8e150c34ab14f Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 18:00:48 +0200 Subject: [PATCH 38/56] feat(ai): deployment debug-chat provider + SSE endpoints + plugin (ADR-023) DeploymentChatProvider seeds a chat from a failed deployment (state + cancelled_reason + failed jobs' error messages; log-tail enrichment TBD). AiChatPlugin builds ConversationService with the provider and mounts the chat routes (find / get-or-create / history / SSE send / archive), gated on ai_debug_chat_enabled + is_available. Registered in serve after the gateway. --- Cargo.lock | 5 + crates/temps-ai-chat/Cargo.toml | 4 + crates/temps-ai-chat/src/handlers.rs | 331 ++++++++++++++++++ crates/temps-ai-chat/src/lib.rs | 5 + crates/temps-ai-chat/src/plugin.rs | 74 ++++ .../temps-ai-chat/src/providers/deployment.rs | 103 ++++++ crates/temps-ai-chat/src/providers/mod.rs | 3 + crates/temps-cli/Cargo.toml | 1 + .../temps-cli/src/commands/serve/console.rs | 6 + 9 files changed, 532 insertions(+) create mode 100644 crates/temps-ai-chat/src/handlers.rs create mode 100644 crates/temps-ai-chat/src/plugin.rs create mode 100644 crates/temps-ai-chat/src/providers/deployment.rs create mode 100644 crates/temps-ai-chat/src/providers/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5d69f314c..872f9107f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10659,6 +10659,7 @@ version = "0.1.0-beta.39" dependencies = [ "async-stream", "async-trait", + "axum 0.8.9", "chrono", "futures", "futures-util", @@ -10666,10 +10667,13 @@ dependencies = [ "serde", "serde_json", "temps-ai", + "temps-auth", + "temps-core", "temps-entities", "thiserror 2.0.18", "tokio", "tracing", + "utoipa", "uuid", ] @@ -11067,6 +11071,7 @@ dependencies = [ "tar", "temps-agent", "temps-agents", + "temps-ai-chat", "temps-ai-gateway", "temps-analytics", "temps-analytics-events", diff --git a/crates/temps-ai-chat/Cargo.toml b/crates/temps-ai-chat/Cargo.toml index 6e6f45646..5e7ebb9c5 100644 --- a/crates/temps-ai-chat/Cargo.toml +++ b/crates/temps-ai-chat/Cargo.toml @@ -13,7 +13,11 @@ categories = ["development-tools"] [dependencies] temps-ai = { path = "../temps-ai" } temps-entities = { path = "../temps-entities" } +temps-core = { path = "../temps-core" } +temps-auth = { path = "../temps-auth" } +axum = { workspace = true } +utoipa = { workspace = true } sea-orm = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/temps-ai-chat/src/handlers.rs b/crates/temps-ai-chat/src/handlers.rs new file mode 100644 index 000000000..678ce65e1 --- /dev/null +++ b/crates/temps-ai-chat/src/handlers.rs @@ -0,0 +1,331 @@ +//! HTTP surface for AI debugging conversations (ADR-023). +//! +//! `GET/POST /projects/{project_id}/ai/conversations` (find / get-or-create), +//! `GET .../{public_id}` (history), `POST .../{public_id}/messages` (SSE stream +//! of the assistant reply), `POST .../{public_id}/archive`. All gated on the +//! per-project `ai_debug_chat_enabled` toggle + AI being configured. + +use std::convert::Infallible; +use std::sync::Arc; + +use axum::{ + extract::{Path, Query, State}, + response::sse::{Event, KeepAlive, Sse}, + routing::{get, post}, + Json, Router, +}; +use futures::stream::Stream; +use futures_util::StreamExt; +use sea_orm::{DatabaseConnection, EntityTrait}; +use serde::{Deserialize, Serialize}; +use utoipa::{OpenApi, ToSchema}; + +use temps_auth::{permission_guard, RequireAuth}; +use temps_core::problemdetails::{self, Problem}; +use temps_entities::{ai_conversations, ai_messages}; + +use crate::{ChatError, ConversationService}; + +/// Shared state for the chat routes. +pub struct AppState { + pub service: Arc, + pub db: Arc, +} + +// --- DTOs -------------------------------------------------------------------- + +#[derive(Debug, Serialize, ToSchema)] +pub struct ConversationResponse { + pub public_id: String, + pub context_type: String, + pub context_id: String, + pub title: Option, + pub status: String, + pub created_at: String, + pub last_activity_at: String, +} + +impl From for ConversationResponse { + fn from(m: ai_conversations::Model) -> Self { + Self { + public_id: m.public_id, + context_type: m.context_type, + context_id: m.context_id, + title: m.title, + status: m.status, + created_at: m.created_at.to_rfc3339(), + last_activity_at: m.last_activity_at.to_rfc3339(), + } + } +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct MessageResponse { + pub role: String, + pub content: String, + pub created_at: String, +} + +impl From for MessageResponse { + fn from(m: ai_messages::Model) -> Self { + Self { + role: m.role, + content: m.content, + created_at: m.created_at.to_rfc3339(), + } + } +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct ConversationDetailResponse { + #[serde(flatten)] + pub conversation: ConversationResponse, + /// Turns oldest-first. The `system` seed message is omitted (internal). + pub messages: Vec, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct CreateConversationRequest { + /// e.g. `"deployment"`. + pub context_type: String, + /// The entity id (ints stringified). + pub context_id: String, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct FindConversationQuery { + pub context_type: String, + pub context_id: String, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct SendMessageRequest { + pub content: String, +} + +// --- error mapping ----------------------------------------------------------- + +impl From for Problem { + fn from(e: ChatError) -> Self { + match e { + ChatError::NotFound(_) => problemdetails::new(axum::http::StatusCode::NOT_FOUND) + .with_title("Conversation Not Found") + .with_detail(e.to_string()), + ChatError::NoProvider(_) | ChatError::ContextUnavailable => { + problemdetails::new(axum::http::StatusCode::NOT_FOUND) + .with_title("Context Not Available") + .with_detail(e.to_string()) + } + ChatError::AiUnavailable => problemdetails::new(axum::http::StatusCode::CONFLICT) + .with_title("AI Not Configured") + .with_detail(e.to_string()), + ChatError::Db(_) | ChatError::Ai(_) => { + problemdetails::new(axum::http::StatusCode::INTERNAL_SERVER_ERROR) + .with_title("Internal Server Error") + .with_detail(e.to_string()) + } + } + } +} + +/// Gate: the project must have opted into AI debug chat AND AI must be configured. +async fn ensure_enabled(state: &AppState, project_id: i32) -> Result<(), Problem> { + let project = temps_entities::projects::Entity::find_by_id(project_id) + .one(state.db.as_ref()) + .await + .map_err(|e| { + problemdetails::new(axum::http::StatusCode::INTERNAL_SERVER_ERROR) + .with_detail(e.to_string()) + })?; + let enabled = matches!(project.and_then(|p| p.ai_debug_chat_enabled), Some(true)); + if !enabled { + return Err(problemdetails::new(axum::http::StatusCode::FORBIDDEN) + .with_title("AI Debug Chat Disabled") + .with_detail("Enable AI debug chat for this project to use it.")); + } + if !state.service.ai_available().await { + return Err(problemdetails::new(axum::http::StatusCode::CONFLICT) + .with_title("AI Not Configured") + .with_detail("Configure an AI provider to use debugging chat.")); + } + Ok(()) +} + +// --- handlers ---------------------------------------------------------------- + +/// Find the existing chat for a context (returns `null` if none yet). +#[utoipa::path( + get, tag = "AI Chat", + path = "/projects/{project_id}/ai/conversations", + params(("project_id" = i32, Path,), ("context_type" = String, Query,), ("context_id" = String, Query,)), + responses((status = 200, body = Option), (status = 401), (status = 403)), + security(("bearer_auth" = [])) +)] +pub async fn find_conversation( + RequireAuth(auth): RequireAuth, + State(state): State>, + Path(project_id): Path, + Query(q): Query, +) -> Result>, Problem> { + permission_guard!(auth, ProjectsRead); + let found = state + .service + .find_by_context(project_id, &q.context_type, &q.context_id) + .await?; + Ok(Json(found.map(ConversationResponse::from))) +} + +/// Get-or-create the chat for a context (seeds it on first open). +#[utoipa::path( + post, tag = "AI Chat", + path = "/projects/{project_id}/ai/conversations", + params(("project_id" = i32, Path,)), + request_body = CreateConversationRequest, + responses((status = 200, body = ConversationResponse), (status = 401), (status = 403), (status = 404)), + security(("bearer_auth" = [])) +)] +pub async fn create_conversation( + RequireAuth(auth): RequireAuth, + State(state): State>, + Path(project_id): Path, + Json(req): Json, +) -> Result, Problem> { + permission_guard!(auth, ProjectsRead); + ensure_enabled(&state, project_id).await?; + let conv = state + .service + .get_or_create( + project_id, + &req.context_type, + &req.context_id, + Some(auth.user_id()), + ) + .await?; + Ok(Json(ConversationResponse::from(conv))) +} + +/// Full conversation history (excluding the internal system seed). +#[utoipa::path( + get, tag = "AI Chat", + path = "/projects/{project_id}/ai/conversations/{public_id}", + params(("project_id" = i32, Path,), ("public_id" = String, Path,)), + responses((status = 200, body = ConversationDetailResponse), (status = 401), (status = 403), (status = 404)), + security(("bearer_auth" = [])) +)] +pub async fn get_conversation( + RequireAuth(auth): RequireAuth, + State(state): State>, + Path((project_id, public_id)): Path<(i32, String)>, +) -> Result, Problem> { + permission_guard!(auth, ProjectsRead); + let conv = state + .service + .get_by_public_id(project_id, &public_id) + .await?; + let messages = state + .service + .messages(conv.id) + .await? + .into_iter() + .filter(|m| m.role != "system") + .map(MessageResponse::from) + .collect(); + Ok(Json(ConversationDetailResponse { + conversation: ConversationResponse::from(conv), + messages, + })) +} + +/// Send a user message; stream the assistant reply as Server-Sent Events. +#[utoipa::path( + post, tag = "AI Chat", + path = "/projects/{project_id}/ai/conversations/{public_id}/messages", + params(("project_id" = i32, Path,), ("public_id" = String, Path,)), + request_body = SendMessageRequest, + responses((status = 200, description = "SSE stream of assistant text deltas", content_type = "text/event-stream"), (status = 401), (status = 403), (status = 404)), + security(("bearer_auth" = [])) +)] +pub async fn send_message( + RequireAuth(auth): RequireAuth, + State(state): State>, + Path((project_id, public_id)): Path<(i32, String)>, + Json(req): Json, +) -> Result>>, Problem> { + permission_guard!(auth, ProjectsRead); + ensure_enabled(&state, project_id).await?; + let conv = state + .service + .get_by_public_id(project_id, &public_id) + .await?; + let token_stream = state.service.send_message(&conv, &req.content).await?; + + let sse = token_stream.map(|item| { + let event = match item { + Ok(tok) => Event::default().data(tok), + Err(e) => Event::default().event("error").data(e.to_string()), + }; + Ok::<_, Infallible>(event) + }); + Ok(Sse::new(sse).keep_alive(KeepAlive::default())) +} + +/// Archive (soft-delete) a conversation. +#[utoipa::path( + post, tag = "AI Chat", + path = "/projects/{project_id}/ai/conversations/{public_id}/archive", + params(("project_id" = i32, Path,), ("public_id" = String, Path,)), + responses((status = 204), (status = 401), (status = 403), (status = 404)), + security(("bearer_auth" = [])) +)] +pub async fn archive_conversation( + RequireAuth(auth): RequireAuth, + State(state): State>, + Path((project_id, public_id)): Path<(i32, String)>, +) -> Result { + permission_guard!(auth, ProjectsWrite); + let conv = state + .service + .get_by_public_id(project_id, &public_id) + .await?; + state.service.archive(&conv).await?; + Ok(axum::http::StatusCode::NO_CONTENT) +} + +pub fn configure_routes() -> Router> { + Router::new() + .route( + "/projects/{project_id}/ai/conversations", + get(find_conversation).post(create_conversation), + ) + .route( + "/projects/{project_id}/ai/conversations/{public_id}", + get(get_conversation), + ) + .route( + "/projects/{project_id}/ai/conversations/{public_id}/messages", + post(send_message), + ) + .route( + "/projects/{project_id}/ai/conversations/{public_id}/archive", + post(archive_conversation), + ) +} + +#[derive(OpenApi)] +#[openapi( + paths( + find_conversation, + create_conversation, + get_conversation, + send_message, + archive_conversation, + ), + components(schemas( + ConversationResponse, + MessageResponse, + ConversationDetailResponse, + CreateConversationRequest, + SendMessageRequest, + )) +)] +pub struct AiChatApiDoc; diff --git a/crates/temps-ai-chat/src/lib.rs b/crates/temps-ai-chat/src/lib.rs index dabc58ba2..705b39007 100644 --- a/crates/temps-ai-chat/src/lib.rs +++ b/crates/temps-ai-chat/src/lib.rs @@ -7,10 +7,15 @@ //! deployment provider seeds from a failure diagnosis). Built on the `temps-ai` //! foundation; the AI is injected as `Arc`. +pub mod handlers; +pub mod plugin; pub mod provider; +pub mod providers; pub mod service; +pub use plugin::AiChatPlugin; pub use provider::{ConversationContextProvider, ConversationSeed}; +pub use providers::deployment::DeploymentChatProvider; pub use service::ConversationService; /// Errors from the conversation layer. All map cleanly to HTTP at the handler. diff --git a/crates/temps-ai-chat/src/plugin.rs b/crates/temps-ai-chat/src/plugin.rs new file mode 100644 index 000000000..01fc40ec9 --- /dev/null +++ b/crates/temps-ai-chat/src/plugin.rs @@ -0,0 +1,74 @@ +//! Plugin wiring for AI debugging conversations (ADR-023). +//! +//! Builds the [`ConversationService`] with the registered context providers (the +//! deployment provider for v1), stores route state, and mounts the chat routes. +//! Registers after the AI gateway plugin so `Arc` is available. + +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use temps_core::plugin::{ + PluginContext, PluginError, PluginRoutes, ServiceRegistrationContext, TempsPlugin, +}; +use utoipa::openapi::OpenApi; +use utoipa::OpenApi as OpenApiTrait; + +use crate::handlers::{self, AiChatApiDoc, AppState}; +use crate::provider::ConversationContextProvider; +use crate::providers::deployment::DeploymentChatProvider; +use crate::ConversationService; + +pub struct AiChatPlugin; + +impl AiChatPlugin { + pub fn new() -> Self { + Self + } +} + +impl Default for AiChatPlugin { + fn default() -> Self { + Self::new() + } +} + +impl TempsPlugin for AiChatPlugin { + fn name(&self) -> &'static str { + "ai_chat" + } + + fn register_services<'a>( + &'a self, + context: &'a ServiceRegistrationContext, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let db = context.require_service::(); + let ai = context.require_service::(); + + // v1: one built-in provider. Future context types register their own + // provider here (or via a registry once there are several). + let providers: Vec> = + vec![Arc::new(DeploymentChatProvider::new(db.clone()))]; + + let service = Arc::new(ConversationService::new(db.clone(), ai, providers)); + context.register_service(service.clone()); + + let app_state = Arc::new(AppState { service, db }); + context.register_plugin_state("ai_chat", app_state); + + tracing::debug!("AI chat plugin services registered successfully"); + Ok(()) + }) + } + + fn configure_routes(&self, context: &PluginContext) -> Option { + let app_state = context.get_plugin_state::("ai_chat")?; + let router = handlers::configure_routes().with_state(app_state); + Some(PluginRoutes::new(router)) + } + + fn openapi_schema(&self) -> Option { + Some(::openapi()) + } +} diff --git a/crates/temps-ai-chat/src/providers/deployment.rs b/crates/temps-ai-chat/src/providers/deployment.rs new file mode 100644 index 000000000..b27f1cd9f --- /dev/null +++ b/crates/temps-ai-chat/src/providers/deployment.rs @@ -0,0 +1,103 @@ +//! The deployment-failure context provider (ADR-023, consumer #1). +//! +//! Seeds a chat from a failed deployment: its state + reason, and each failed +//! step's error message. `context_id` is the deployment's integer id. +//! +//! v1 seeds from the deployment/job rows (always present, no extra I/O); a fast +//! follow-up enriches the system context with the failed jobs' log *tails* via +//! `LogService::get_log_content(job.log_id)`. + +use std::sync::Arc; + +use async_trait::async_trait; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; + +use temps_entities::types::JobStatus; +use temps_entities::{deployment_jobs, deployments}; + +use crate::provider::{ConversationContextProvider, ConversationSeed}; + +const SYSTEM_PREAMBLE: &str = "You are a senior platform/DevOps engineer helping a developer debug a FAILED Temps \ +deployment. Use the failure context below. Identify the most likely root cause and concrete, ordered fixes, \ +grounded strictly in the evidence — do not invent file names, versions, or causes you cannot see. Ask a brief \ +clarifying question only when essential. Be concise and practical."; + +/// Seeds deployment-failure debugging chats. +pub struct DeploymentChatProvider { + db: Arc, +} + +impl DeploymentChatProvider { + pub fn new(db: Arc) -> Self { + Self { db } + } +} + +#[async_trait] +impl ConversationContextProvider for DeploymentChatProvider { + fn context_type(&self) -> &'static str { + "deployment" + } + + async fn seed(&self, project_id: i32, context_id: &str) -> Option { + let deployment_id: i32 = context_id.parse().ok()?; + let dep = deployments::Entity::find_by_id(deployment_id) + .one(self.db.as_ref()) + .await + .ok()??; + // Authorization belt-and-braces: the row must belong to this project. + if dep.project_id != project_id { + return None; + } + + let jobs = deployment_jobs::Entity::find() + .filter(deployment_jobs::Column::DeploymentId.eq(deployment_id)) + .all(self.db.as_ref()) + .await + .ok()?; + let failed: Vec<&deployment_jobs::Model> = jobs + .iter() + .filter(|j| matches!(j.status, JobStatus::Failure)) + .collect(); + + let mut ctx = String::new(); + ctx.push_str(SYSTEM_PREAMBLE); + ctx.push_str("\n\n--- Failure context ---\n"); + ctx.push_str(&format!("Deployment #{} — state: {}\n", dep.id, dep.state)); + if let Some(branch) = &dep.branch_ref { + ctx.push_str(&format!("Branch: {branch}\n")); + } + if let Some(commit) = &dep.commit_sha { + let short: String = commit.chars().take(8).collect(); + ctx.push_str(&format!("Commit: {short}\n")); + } + if let Some(reason) = &dep.cancelled_reason { + ctx.push_str(&format!("Failure reason: {reason}\n")); + } + if failed.is_empty() { + ctx.push_str("No individual job failures were recorded.\n"); + } else { + ctx.push_str("Failed steps:\n"); + for j in &failed { + ctx.push_str(&format!( + "- '{}': {}\n", + j.job_id, + j.error_message.as_deref().unwrap_or("(no error message)") + )); + } + } + + let metadata = serde_json::json!({ + "deployment_id": deployment_id, + "state": dep.state, + "failed_job_ids": failed.iter().map(|j| j.job_id.clone()).collect::>(), + }); + + Some(ConversationSeed { + system: ctx, + first_assistant: None, + title: Some(format!("Debug deployment #{deployment_id}")), + metadata: Some(metadata), + }) + } +} diff --git a/crates/temps-ai-chat/src/providers/mod.rs b/crates/temps-ai-chat/src/providers/mod.rs new file mode 100644 index 000000000..881428063 --- /dev/null +++ b/crates/temps-ai-chat/src/providers/mod.rs @@ -0,0 +1,3 @@ +//! Built-in [`crate::ConversationContextProvider`] implementations. + +pub mod deployment; diff --git a/crates/temps-cli/Cargo.toml b/crates/temps-cli/Cargo.toml index d9a05357b..a26ec4065 100644 --- a/crates/temps-cli/Cargo.toml +++ b/crates/temps-cli/Cargo.toml @@ -23,6 +23,7 @@ path = "src/lib.rs" temps-agent = { path = "../temps-agent" } temps-edge = { path = "../temps-edge" } temps-ai-gateway = { path = "../temps-ai-gateway" } +temps-ai-chat = { path = "../temps-ai-chat" } temps-analytics = { path = "../temps-analytics" } temps-analytics-events = { path = "../temps-analytics-events" } temps-blob = { path = "../temps-blob" } diff --git a/crates/temps-cli/src/commands/serve/console.rs b/crates/temps-cli/src/commands/serve/console.rs index 6146eba67..0a6a94dc8 100644 --- a/crates/temps-cli/src/commands/serve/console.rs +++ b/crates/temps-cli/src/commands/serve/console.rs @@ -1304,6 +1304,12 @@ pub async fn start_console_api(params: ConsoleApiParams) -> anyhow::Result<()> { let ai_gateway_plugin = Box::new(temps_ai_gateway::AiGatewayPlugin::new()); plugin_manager.register_plugin(ai_gateway_plugin); + // AI Chat Plugin - persistent AI debugging conversations (ADR-023). After the + // AI gateway so the AiService it provides is registered. + debug!("Registering AiChatPlugin"); + let ai_chat_plugin = Box::new(temps_ai_chat::AiChatPlugin::new()); + plugin_manager.register_plugin(ai_chat_plugin); + // 12. ApiKeyPlugin - provides API key management (depends on auth services) debug!("Registering ApiKeyPlugin"); let apikey_plugin = Box::new(ApiKeyPlugin::new()); From b5afc7cf189bf40c520cd7603b1e2dd86e62fea2 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 18:06:50 +0200 Subject: [PATCH 39/56] feat(projects): expose ai_alert_summaries_enabled + ai_debug_chat_enabled in project API (ADR-021/023) Thread both AI opt-in toggles through update_project_settings (request DTO, service signature + apply, handler call, test) and ProjectResponse (+ service Project struct + entity mapping) so they are settable + readable via the project API, ready for the settings UI switch. --- .../temps-projects/src/handlers/handlers.rs | 2 ++ crates/temps-projects/src/handlers/types.rs | 10 +++++++ crates/temps-projects/src/services/project.rs | 26 +++++++++++++++++++ crates/temps-projects/src/services/types.rs | 2 ++ 4 files changed, 40 insertions(+) diff --git a/crates/temps-projects/src/handlers/handlers.rs b/crates/temps-projects/src/handlers/handlers.rs index 25f5d24f3..788a81f7f 100644 --- a/crates/temps-projects/src/handlers/handlers.rs +++ b/crates/temps-projects/src/handlers/handlers.rs @@ -538,6 +538,8 @@ pub async fn update_project_settings( settings.preview_envs_idle_timeout_seconds, settings.preview_envs_wake_timeout_seconds, settings.preset_config.clone(), + settings.ai_alert_summaries_enabled, + settings.ai_debug_chat_enabled, ) .await .map_err(Problem::from)?; diff --git a/crates/temps-projects/src/handlers/types.rs b/crates/temps-projects/src/handlers/types.rs index 50da00440..a86a6e1d0 100644 --- a/crates/temps-projects/src/handlers/types.rs +++ b/crates/temps-projects/src/handlers/types.rs @@ -266,6 +266,10 @@ pub struct ProjectResponse { pub deployment_config: DeploymentConfig, /// Attack mode - when enabled, requires CAPTCHA verification for all project environments pub attack_mode: bool, + /// Opt-in to AI summarization of metric alert notifications (NULL/false = off). + pub ai_alert_summaries_enabled: Option, + /// Opt-in to AI debugging chat, e.g. on deployment failures (NULL/false = off). + pub ai_debug_chat_enabled: Option, /// Enable automatic preview environment creation for each branch pub enable_preview_environments: bool, /// When true, newly-created preview environments default to on-demand mode @@ -310,6 +314,8 @@ impl ProjectResponse { git_provider_connection_id: project.git_provider_connection_id, git_url: project.git_url, attack_mode: project.attack_mode, + ai_alert_summaries_enabled: project.ai_alert_summaries_enabled, + ai_debug_chat_enabled: project.ai_debug_chat_enabled, enable_preview_environments: project.enable_preview_environments, preview_envs_on_demand: project.preview_envs_on_demand, preview_envs_idle_timeout_seconds: project.preview_envs_idle_timeout_seconds, @@ -525,6 +531,10 @@ pub struct UpdateProjectSettingsRequest { pub directory: Option, /// Enable/disable attack mode (CAPTCHA protection) for all project environments pub attack_mode: Option, + /// Opt in to AI summarization of metric alert notifications (ADR-021). + pub ai_alert_summaries_enabled: Option, + /// Opt in to AI debugging chat, e.g. on deployment failures (ADR-023). + pub ai_debug_chat_enabled: Option, /// Enable automatic preview environment creation for each branch pub enable_preview_environments: Option, /// When true, newly-created preview environments default to on-demand mode. diff --git a/crates/temps-projects/src/services/project.rs b/crates/temps-projects/src/services/project.rs index 610199747..23cd560e4 100644 --- a/crates/temps-projects/src/services/project.rs +++ b/crates/temps-projects/src/services/project.rs @@ -668,6 +668,8 @@ impl ProjectService { preview_envs_idle_timeout_seconds: Option, preview_envs_wake_timeout_seconds: Option, preset_config: Option, + ai_alert_summaries_enabled: Option, + ai_debug_chat_enabled: Option, ) -> Result { // Validate preview env on-demand timeouts before touching the DB. // Mirrors DeploymentConfig::validate so the project-level defaults are @@ -797,6 +799,26 @@ impl ProjectService { active_project.update(self.db.as_ref()).await?; } + // Update AI feature toggles if provided (ADR-021 / ADR-023). Both are + // tri-state opt-ins (Some(true) = on), stored as nullable columns. + if ai_alert_summaries_enabled.is_some() || ai_debug_chat_enabled.is_some() { + let project = projects::Entity::find_by_id(project_id) + .one(self.db.as_ref()) + .await? + .ok_or(ProjectError::NotFound(format!( + "Project {} not found", + project_id + )))?; + let mut active_project: projects::ActiveModel = project.into(); + if let Some(v) = ai_alert_summaries_enabled { + active_project.ai_alert_summaries_enabled = Set(Some(v)); + } + if let Some(v) = ai_debug_chat_enabled { + active_project.ai_debug_chat_enabled = Set(Some(v)); + } + active_project.update(self.db.as_ref()).await?; + } + // Update preview environment settings if any are provided let needs_preview_update = enable_preview_environments.is_some() || preview_envs_on_demand.is_some() @@ -1700,6 +1722,8 @@ impl ProjectService { is_on_demand: false, // Deprecated field, default to false deployment_config: deployment_config.clone(), attack_mode: db_project.attack_mode, + ai_alert_summaries_enabled: db_project.ai_alert_summaries_enabled, + ai_debug_chat_enabled: db_project.ai_debug_chat_enabled, enable_preview_environments: db_project.enable_preview_environments, preview_envs_on_demand: db_project.preview_envs_on_demand, preview_envs_idle_timeout_seconds: db_project.preview_envs_idle_timeout_seconds, @@ -2232,6 +2256,8 @@ mod tests { None, None, None, + None, + None, ) .await; diff --git a/crates/temps-projects/src/services/types.rs b/crates/temps-projects/src/services/types.rs index 95d834ae8..db5870719 100644 --- a/crates/temps-projects/src/services/types.rs +++ b/crates/temps-projects/src/services/types.rs @@ -63,6 +63,8 @@ pub struct Project { pub is_on_demand: bool, pub deployment_config: Option, pub attack_mode: bool, + pub ai_alert_summaries_enabled: Option, + pub ai_debug_chat_enabled: Option, pub enable_preview_environments: bool, /// When true, newly-created preview environments default to on-demand mode. pub preview_envs_on_demand: bool, From e9f7dc471bdf6bdcc6a5fe955e0648edaba6fdcd Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 18:13:17 +0200 Subject: [PATCH 40/56] chore(web): regenerate SDK for AI chat routes + project AI toggles (ADR-023) Adds typed client + react-query hooks for /projects/{id}/ai/conversations (find/create/get/send/archive) and the ai_debug_chat_enabled / ai_alert_summaries_enabled fields on the project response/update. --- .../api/client/@tanstack/react-query.gen.ts | 95 +++++++++- web/src/api/client/sdk.gen.ts | 90 +++++++++- web/src/api/client/types.gen.ts | 165 ++++++++++++++++++ 3 files changed, 347 insertions(+), 3 deletions(-) diff --git a/web/src/api/client/@tanstack/react-query.gen.ts b/web/src/api/client/@tanstack/react-query.gen.ts index 99ea4ff07..697ff95bc 100644 --- a/web/src/api/client/@tanstack/react-query.gen.ts +++ b/web/src/api/client/@tanstack/react-query.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, getAdminGate, patchAdminGate, listOidcProviders, createOidcProvider, deleteOidcProvider, updateOidcProvider, listOidcRoleMappings, createOidcRoleMapping, testOidcProvider, listOidcProviderUsers, deleteOidcRoleMapping, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, oidcCallback, startOidcLoginBySlug, listPublicProviders, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listServiceSchedules, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, listScheduleServices, attachScheduleServices, detachScheduleService, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, deploymentMetricsGetRange, deploymentMetricsToggle, deploymentMetricsGetLatest, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, getOnDemandCertStatus, listOnDemandCerts, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, updateEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, externalServiceMetricsGetRange, externalServiceMetricsGetAlertRules, externalServiceMetricsCreateAlertRule, externalServiceMetricsDeleteAlertRule, externalServiceMetricsUpdateAlertRule, externalServiceMetricsByDatabase, externalServiceMetricsToggle, externalServiceMetricsGetLatest, externalServiceMetricsStatus, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, nodeMetricsGetRange, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider2, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, listAlerts, createAlert, previewAlert, deleteAlert, getAlert, updateAlert, listDashboards, createDashboard, deleteDashboard, getDashboard, updateDashboard, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, listDeploymentContainerLogs, getDeploymentContainerLogContent, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, listKnownAiAgents, getProxyLogByRequestId, getAiAgentPages, getAiAgentBreakdown, getAiAgentTimeline, getAiPageBreakdown, getAiStatusBreakdown, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, getDiskStatus, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; +import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, getAdminGate, patchAdminGate, listOidcProviders, createOidcProvider, deleteOidcProvider, updateOidcProvider, listOidcRoleMappings, createOidcRoleMapping, testOidcProvider, listOidcProviderUsers, deleteOidcRoleMapping, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, oidcCallback, startOidcLoginBySlug, listPublicProviders, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listServiceSchedules, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, listScheduleServices, attachScheduleServices, detachScheduleService, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, deploymentMetricsGetRange, deploymentMetricsToggle, deploymentMetricsGetLatest, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, getOnDemandCertStatus, listOnDemandCerts, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, updateEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, externalServiceMetricsGetRange, externalServiceMetricsGetAlertRules, externalServiceMetricsCreateAlertRule, externalServiceMetricsDeleteAlertRule, externalServiceMetricsUpdateAlertRule, externalServiceMetricsByDatabase, externalServiceMetricsToggle, externalServiceMetricsGetLatest, externalServiceMetricsStatus, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, nodeMetricsGetRange, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider2, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, listAlerts, createAlert, previewAlert, deleteAlert, getAlert, updateAlert, listDashboards, createDashboard, deleteDashboard, getDashboard, updateDashboard, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, findConversation, createConversation, getConversation, archiveConversation, sendMessage, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, listDeploymentContainerLogs, getDeploymentContainerLogContent, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, listKnownAiAgents, getProxyLogByRequestId, getAiAgentPages, getAiAgentBreakdown, getAiAgentTimeline, getAiPageBreakdown, getAiStatusBreakdown, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, getDiskStatus, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; import { queryOptions, type UseMutationOptions, type DefaultError, infiniteQueryOptions, type InfiniteData } from '@tanstack/react-query'; -import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, GetAdminGateData, PatchAdminGateData, PatchAdminGateResponse, ListOidcProvidersData, CreateOidcProviderData, CreateOidcProviderResponse, DeleteOidcProviderData, DeleteOidcProviderResponse, UpdateOidcProviderData, UpdateOidcProviderResponse, ListOidcRoleMappingsData, CreateOidcRoleMappingData, CreateOidcRoleMappingResponse, TestOidcProviderData, TestOidcProviderResponse, ListOidcProviderUsersData, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageRecentError, GetUsageRecentResponse, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, OidcCallbackData, StartOidcLoginBySlugData, ListPublicProvidersData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListServiceSchedulesData, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, ListScheduleServicesData, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesResponse2 as AttachScheduleServicesResponse, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, DeploymentMetricsGetRangeData, DeploymentMetricsToggleData, DeploymentMetricsGetLatestData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, GetOnDemandCertStatusData, ListOnDemandCertsData, ListOnDemandCertsResponse2 as ListOnDemandCertsResponse, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, UpdateEmailProviderData, UpdateEmailProviderResponse, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponse, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponse, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponse, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsToggleData, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsStatusData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, NodeMetricsGetRangeData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProvider2Data, UpdateEmailProvider2Response, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, ListAlertsData, ListAlertsError, ListAlertsResponse, CreateAlertData, CreateAlertError, CreateAlertResponse, PreviewAlertData, PreviewAlertError, PreviewAlertResponse, DeleteAlertData, DeleteAlertError, DeleteAlertResponse, GetAlertData, UpdateAlertData, UpdateAlertError, UpdateAlertResponse, ListDashboardsData, ListDashboardsError, ListDashboardsResponse, CreateDashboardData, CreateDashboardError, CreateDashboardResponse, DeleteDashboardData, DeleteDashboardError, DeleteDashboardResponse, GetDashboardData, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, ListDeploymentContainerLogsData, GetDeploymentContainerLogContentData, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, ListKnownAiAgentsData, GetProxyLogByRequestIdData, GetAiAgentPagesData, GetAiAgentBreakdownData, GetAiAgentTimelineData, GetAiPageBreakdownData, GetAiStatusBreakdownData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, GetDiskStatusData, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; +import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, GetAdminGateData, PatchAdminGateData, PatchAdminGateResponse, ListOidcProvidersData, CreateOidcProviderData, CreateOidcProviderResponse, DeleteOidcProviderData, DeleteOidcProviderResponse, UpdateOidcProviderData, UpdateOidcProviderResponse, ListOidcRoleMappingsData, CreateOidcRoleMappingData, CreateOidcRoleMappingResponse, TestOidcProviderData, TestOidcProviderResponse, ListOidcProviderUsersData, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageRecentError, GetUsageRecentResponse, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, OidcCallbackData, StartOidcLoginBySlugData, ListPublicProvidersData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListServiceSchedulesData, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, ListScheduleServicesData, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesResponse2 as AttachScheduleServicesResponse, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, DeploymentMetricsGetRangeData, DeploymentMetricsToggleData, DeploymentMetricsGetLatestData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, GetOnDemandCertStatusData, ListOnDemandCertsData, ListOnDemandCertsResponse2 as ListOnDemandCertsResponse, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, UpdateEmailProviderData, UpdateEmailProviderResponse, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponse, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponse, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponse, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsToggleData, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsStatusData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, NodeMetricsGetRangeData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProvider2Data, UpdateEmailProvider2Response, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, ListAlertsData, ListAlertsError, ListAlertsResponse, CreateAlertData, CreateAlertError, CreateAlertResponse, PreviewAlertData, PreviewAlertError, PreviewAlertResponse, DeleteAlertData, DeleteAlertError, DeleteAlertResponse, GetAlertData, UpdateAlertData, UpdateAlertError, UpdateAlertResponse, ListDashboardsData, ListDashboardsError, ListDashboardsResponse, CreateDashboardData, CreateDashboardError, CreateDashboardResponse, DeleteDashboardData, DeleteDashboardError, DeleteDashboardResponse, GetDashboardData, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, FindConversationData, CreateConversationData, CreateConversationResponse, GetConversationData, ArchiveConversationData, ArchiveConversationResponse, SendMessageData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, ListDeploymentContainerLogsData, GetDeploymentContainerLogContentData, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, ListKnownAiAgentsData, GetProxyLogByRequestIdData, GetAiAgentPagesData, GetAiAgentBreakdownData, GetAiAgentTimelineData, GetAiPageBreakdownData, GetAiStatusBreakdownData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, GetDiskStatusData, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; import { client } from '../client.gen'; export type QueryKey = [ @@ -8986,6 +8986,97 @@ export const getAggregatedBucketsOptions = (options: Options) => createQueryKey('findConversation', options); + +/** + * Find the existing chat for a context (returns `null` if none yet). + */ +export const findConversationOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await findConversation({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: findConversationQueryKey(options) + }); +}; + +/** + * Get-or-create the chat for a context (seeds it on first open). + */ +export const createConversationMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await createConversation({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConversationQueryKey = (options: Options) => createQueryKey('getConversation', options); + +/** + * Full conversation history (excluding the internal system seed). + */ +export const getConversationOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConversation({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConversationQueryKey(options) + }); +}; + +/** + * Archive (soft-delete) a conversation. + */ +export const archiveConversationMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await archiveConversation({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Send a user message; stream the assistant reply as Server-Sent Events. + */ +export const sendMessageMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await sendMessage({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + /** * Start an autofixer analysis run for the given error group. * Creates the run record immediately and spawns analysis in the background. diff --git a/web/src/api/client/sdk.gen.ts b/web/src/api/client/sdk.gen.ts index e047d24c6..6aa3f26c3 100644 --- a/web/src/api/client/sdk.gen.ts +++ b/web/src/api/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { type Options as ClientOptions, type Client, type TDataShape, formDataBodySerializer } from './client'; -import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, GetAdminGateData, GetAdminGateResponses, GetAdminGateErrors, PatchAdminGateData, PatchAdminGateResponses, PatchAdminGateErrors, ListOidcProvidersData, ListOidcProvidersResponses, CreateOidcProviderData, CreateOidcProviderResponses, CreateOidcProviderErrors, DeleteOidcProviderData, DeleteOidcProviderResponses, UpdateOidcProviderData, UpdateOidcProviderResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingResponses, TestOidcProviderData, TestOidcProviderResponses, ListOidcProviderUsersData, ListOidcProviderUsersResponses, ListOidcProviderUsersErrors, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponses, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, OidcCallbackData, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, ListPublicProvidersData, ListPublicProvidersResponses, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListServiceSchedulesData, ListServiceSchedulesResponses, ListServiceSchedulesErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, ListScheduleServicesData, ListScheduleServicesResponses, ListScheduleServicesErrors, AttachScheduleServicesData, AttachScheduleServicesResponses, AttachScheduleServicesErrors, DetachScheduleServiceData, DetachScheduleServiceResponses, DetachScheduleServiceErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeResponses, DeploymentMetricsGetRangeErrors, DeploymentMetricsToggleData, DeploymentMetricsToggleResponses, DeploymentMetricsToggleErrors, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetLatestErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, GetOnDemandCertStatusData, GetOnDemandCertStatusResponses, GetOnDemandCertStatusErrors, ListOnDemandCertsData, ListOnDemandCertsResponses, ListOnDemandCertsErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsStatusErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, NodeMetricsGetRangeData, NodeMetricsGetRangeResponses, NodeMetricsGetRangeErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProvider2Data, UpdateEmailProvider2Responses, UpdateEmailProvider2Errors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, ListAlertsData, ListAlertsResponses, ListAlertsErrors, CreateAlertData, CreateAlertResponses, CreateAlertErrors, PreviewAlertData, PreviewAlertResponses, PreviewAlertErrors, DeleteAlertData, DeleteAlertResponses, DeleteAlertErrors, GetAlertData, GetAlertResponses, GetAlertErrors, UpdateAlertData, UpdateAlertResponses, UpdateAlertErrors, ListDashboardsData, ListDashboardsResponses, ListDashboardsErrors, CreateDashboardData, CreateDashboardResponses, CreateDashboardErrors, DeleteDashboardData, DeleteDashboardResponses, DeleteDashboardErrors, GetDashboardData, GetDashboardResponses, GetDashboardErrors, UpdateDashboardData, UpdateDashboardResponses, UpdateDashboardErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, ListDeploymentContainerLogsData, ListDeploymentContainerLogsResponses, ListDeploymentContainerLogsErrors, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentResponses, GetDeploymentContainerLogContentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, ListKnownAiAgentsData, ListKnownAiAgentsResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetAiAgentPagesData, GetAiAgentPagesResponses, GetAiAgentPagesErrors, GetAiAgentBreakdownData, GetAiAgentBreakdownResponses, GetAiAgentBreakdownErrors, GetAiAgentTimelineData, GetAiAgentTimelineResponses, GetAiAgentTimelineErrors, GetAiPageBreakdownData, GetAiPageBreakdownResponses, GetAiPageBreakdownErrors, GetAiStatusBreakdownData, GetAiStatusBreakdownResponses, GetAiStatusBreakdownErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, GetDiskStatusData, GetDiskStatusResponses, GetDiskStatusErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; +import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, GetAdminGateData, GetAdminGateResponses, GetAdminGateErrors, PatchAdminGateData, PatchAdminGateResponses, PatchAdminGateErrors, ListOidcProvidersData, ListOidcProvidersResponses, CreateOidcProviderData, CreateOidcProviderResponses, CreateOidcProviderErrors, DeleteOidcProviderData, DeleteOidcProviderResponses, UpdateOidcProviderData, UpdateOidcProviderResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingResponses, TestOidcProviderData, TestOidcProviderResponses, ListOidcProviderUsersData, ListOidcProviderUsersResponses, ListOidcProviderUsersErrors, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponses, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, OidcCallbackData, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, ListPublicProvidersData, ListPublicProvidersResponses, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListServiceSchedulesData, ListServiceSchedulesResponses, ListServiceSchedulesErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, ListScheduleServicesData, ListScheduleServicesResponses, ListScheduleServicesErrors, AttachScheduleServicesData, AttachScheduleServicesResponses, AttachScheduleServicesErrors, DetachScheduleServiceData, DetachScheduleServiceResponses, DetachScheduleServiceErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeResponses, DeploymentMetricsGetRangeErrors, DeploymentMetricsToggleData, DeploymentMetricsToggleResponses, DeploymentMetricsToggleErrors, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetLatestErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, GetOnDemandCertStatusData, GetOnDemandCertStatusResponses, GetOnDemandCertStatusErrors, ListOnDemandCertsData, ListOnDemandCertsResponses, ListOnDemandCertsErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsStatusErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, NodeMetricsGetRangeData, NodeMetricsGetRangeResponses, NodeMetricsGetRangeErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProvider2Data, UpdateEmailProvider2Responses, UpdateEmailProvider2Errors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, ListAlertsData, ListAlertsResponses, ListAlertsErrors, CreateAlertData, CreateAlertResponses, CreateAlertErrors, PreviewAlertData, PreviewAlertResponses, PreviewAlertErrors, DeleteAlertData, DeleteAlertResponses, DeleteAlertErrors, GetAlertData, GetAlertResponses, GetAlertErrors, UpdateAlertData, UpdateAlertResponses, UpdateAlertErrors, ListDashboardsData, ListDashboardsResponses, ListDashboardsErrors, CreateDashboardData, CreateDashboardResponses, CreateDashboardErrors, DeleteDashboardData, DeleteDashboardResponses, DeleteDashboardErrors, GetDashboardData, GetDashboardResponses, GetDashboardErrors, UpdateDashboardData, UpdateDashboardResponses, UpdateDashboardErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, FindConversationData, FindConversationResponses, FindConversationErrors, CreateConversationData, CreateConversationResponses, CreateConversationErrors, GetConversationData, GetConversationResponses, GetConversationErrors, ArchiveConversationData, ArchiveConversationResponses, ArchiveConversationErrors, SendMessageData, SendMessageResponses, SendMessageErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, ListDeploymentContainerLogsData, ListDeploymentContainerLogsResponses, ListDeploymentContainerLogsErrors, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentResponses, GetDeploymentContainerLogContentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, ListKnownAiAgentsData, ListKnownAiAgentsResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetAiAgentPagesData, GetAiAgentPagesResponses, GetAiAgentPagesErrors, GetAiAgentBreakdownData, GetAiAgentBreakdownResponses, GetAiAgentBreakdownErrors, GetAiAgentTimelineData, GetAiAgentTimelineResponses, GetAiAgentTimelineErrors, GetAiPageBreakdownData, GetAiPageBreakdownResponses, GetAiPageBreakdownErrors, GetAiStatusBreakdownData, GetAiStatusBreakdownResponses, GetAiStatusBreakdownErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, GetDiskStatusData, GetDiskStatusResponses, GetDiskStatusErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; import { client } from './client.gen'; export type Options = ClientOptions & { @@ -6889,6 +6889,94 @@ export const getAggregatedBuckets = (optio }); }; +/** + * Find the existing chat for a context (returns `null` if none yet). + */ +export const findConversation = (options: Options) => { + return (options.client ?? client).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/projects/{project_id}/ai/conversations', + ...options + }); +}; + +/** + * Get-or-create the chat for a context (seeds it on first open). + */ +export const createConversation = (options: Options) => { + return (options.client ?? client).post({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/projects/{project_id}/ai/conversations', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Full conversation history (excluding the internal system seed). + */ +export const getConversation = (options: Options) => { + return (options.client ?? client).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/projects/{project_id}/ai/conversations/{public_id}', + ...options + }); +}; + +/** + * Archive (soft-delete) a conversation. + */ +export const archiveConversation = (options: Options) => { + return (options.client ?? client).post({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/projects/{project_id}/ai/conversations/{public_id}/archive', + ...options + }); +}; + +/** + * Send a user message; stream the assistant reply as Server-Sent Events. + */ +export const sendMessage = (options: Options) => { + return (options.client ?? client).sse.post({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/projects/{project_id}/ai/conversations/{public_id}/messages', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + /** * Start an autofixer analysis run for the given error group. * Creates the run record immediately and spawns analysis in the background. diff --git a/web/src/api/client/types.gen.ts b/web/src/api/client/types.gen.ts index 4ae6ada6f..77930516f 100644 --- a/web/src/api/client/types.gen.ts +++ b/web/src/api/client/types.gen.ts @@ -2460,6 +2460,23 @@ export type ContextLogsResponse = { target_index: number; }; +export type ConversationDetailResponse = ConversationResponse & { + /** + * Turns oldest-first. The `system` seed message is omitted (internal). + */ + messages: Array; +}; + +export type ConversationResponse = { + context_id: string; + context_type: string; + created_at: string; + last_activity_at: string; + public_id: string; + status: string; + title?: string | null; +}; + /** * A conversation summary grouping related AI invocations. */ @@ -2603,6 +2620,17 @@ export type CreateBackupScheduleRequest = { target_all_services?: boolean | null; }; +export type CreateConversationRequest = { + /** + * The entity id (ints stringified). + */ + context_id: string; + /** + * e.g. `"deployment"`. + */ + context_type: string; +}; + export type CreateDsnRequest = { base_url?: string | null; deployment_id?: number | null; @@ -8115,6 +8143,12 @@ export type McpDefinitionResponse = { export type MessageContent = string | Array; +export type MessageResponse = { + content: string; + created_at: string; + role: string; +}; + /** * How to treat metered-billing subscriptions when computing MRR. * @@ -10342,6 +10376,14 @@ export type ProjectQuery = { }; export type ProjectResponse = { + /** + * Opt-in to AI summarization of metric alert notifications (NULL/false = off). + */ + ai_alert_summaries_enabled?: boolean | null; + /** + * Opt-in to AI debugging chat, e.g. on deployment failures (NULL/false = off). + */ + ai_debug_chat_enabled?: boolean | null; /** * Attack mode - when enabled, requires CAPTCHA verification for all project environments */ @@ -12373,6 +12415,10 @@ export type SendEmailResponseBody = { status: string; }; +export type SendMessageRequest = { + content: string; +}; + export type SentryChunkUploadResponse = { accept: Array; chunkSize: number; @@ -14905,6 +14951,14 @@ export type UpdateProjectSecretRequest = { }; export type UpdateProjectSettingsRequest = { + /** + * Opt in to AI summarization of metric alert notifications (ADR-021). + */ + ai_alert_summaries_enabled?: boolean | null; + /** + * Opt in to AI debugging chat, e.g. on deployment failures (ADR-023). + */ + ai_debug_chat_enabled?: boolean | null; /** * Enable/disable attack mode (CAPTCHA protection) for all project environments */ @@ -32134,6 +32188,117 @@ export type GetAggregatedBucketsResponses = { export type GetAggregatedBucketsResponse = GetAggregatedBucketsResponses[keyof GetAggregatedBucketsResponses]; +export type FindConversationData = { + body?: never; + path: { + project_id: number; + }; + query: { + context_type: string; + context_id: string; + }; + url: '/projects/{project_id}/ai/conversations'; +}; + +export type FindConversationErrors = { + 401: unknown; + 403: unknown; +}; + +export type FindConversationResponses = { + 200: null | ConversationResponse; +}; + +export type FindConversationResponse = FindConversationResponses[keyof FindConversationResponses]; + +export type CreateConversationData = { + body: CreateConversationRequest; + path: { + project_id: number; + }; + query?: never; + url: '/projects/{project_id}/ai/conversations'; +}; + +export type CreateConversationErrors = { + 401: unknown; + 403: unknown; + 404: unknown; +}; + +export type CreateConversationResponses = { + 200: ConversationResponse; +}; + +export type CreateConversationResponse = CreateConversationResponses[keyof CreateConversationResponses]; + +export type GetConversationData = { + body?: never; + path: { + project_id: number; + public_id: string; + }; + query?: never; + url: '/projects/{project_id}/ai/conversations/{public_id}'; +}; + +export type GetConversationErrors = { + 401: unknown; + 403: unknown; + 404: unknown; +}; + +export type GetConversationResponses = { + 200: ConversationDetailResponse; +}; + +export type GetConversationResponse = GetConversationResponses[keyof GetConversationResponses]; + +export type ArchiveConversationData = { + body?: never; + path: { + project_id: number; + public_id: string; + }; + query?: never; + url: '/projects/{project_id}/ai/conversations/{public_id}/archive'; +}; + +export type ArchiveConversationErrors = { + 401: unknown; + 403: unknown; + 404: unknown; +}; + +export type ArchiveConversationResponses = { + 204: void; +}; + +export type ArchiveConversationResponse = ArchiveConversationResponses[keyof ArchiveConversationResponses]; + +export type SendMessageData = { + body: SendMessageRequest; + path: { + project_id: number; + public_id: string; + }; + query?: never; + url: '/projects/{project_id}/ai/conversations/{public_id}/messages'; +}; + +export type SendMessageErrors = { + 401: unknown; + 403: unknown; + 404: unknown; +}; + +export type SendMessageResponses = { + /** + * SSE stream of assistant text deltas + */ + 200: unknown; +}; + export type StartAnalysisData = { body: StartAnalysisRequest; path: { From 4dcf7bb2b096042dd3088d63d05882a2657bb475 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 18:17:10 +0200 Subject: [PATCH 41/56] =?UTF-8?q?feat(web):=20AI=20Assistance=20settings?= =?UTF-8?q?=20card=20=E2=80=94=20debug=20chat=20+=20alert=20summary=20togg?= =?UTF-8?q?les=20(ADR-021/023)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the attack_mode pattern: two switches (ai_debug_chat_enabled, ai_alert_summaries_enabled) in project security settings, saved via the project update mutation. Off by default; framed as using the user's own provider key/budget. --- .../settings/ProjectSecuritySettings.tsx | 101 ++++++++++++++++-- 1 file changed, 90 insertions(+), 11 deletions(-) diff --git a/web/src/components/project/settings/ProjectSecuritySettings.tsx b/web/src/components/project/settings/ProjectSecuritySettings.tsx index 1ff5ef79f..3937841ee 100644 --- a/web/src/components/project/settings/ProjectSecuritySettings.tsx +++ b/web/src/components/project/settings/ProjectSecuritySettings.tsx @@ -24,7 +24,7 @@ import { } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Switch } from '@/components/ui/switch' -import { InfoIcon, Shield } from 'lucide-react' +import { InfoIcon, MessageSquare, Shield } from 'lucide-react' import { useForm, Controller, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { useMutation } from '@tanstack/react-query' @@ -58,6 +58,8 @@ interface SecurityConfig { interface FormData { security: SecurityConfig attack_mode?: boolean + ai_debug_chat_enabled?: boolean + ai_alert_summaries_enabled?: boolean } export function ProjectSecuritySettings({ @@ -88,6 +90,8 @@ export function ProjectSecuritySettings({ } = useForm({ defaultValues: { attack_mode: project.attack_mode ?? false, + ai_debug_chat_enabled: project.ai_debug_chat_enabled ?? false, + ai_alert_summaries_enabled: project.ai_alert_summaries_enabled ?? false, security: { enabled: project.deployment_config?.security?.enabled ?? undefined, headers: { @@ -130,22 +134,39 @@ export function ProjectSecuritySettings({ if (!project?.id) return try { - // Check if attack_mode has changed - const attackModeChanged = data.attack_mode !== project.attack_mode + // Collect changed project-level toggles (attack mode + AI opt-ins). + const projectSettings: { + attack_mode?: boolean + ai_debug_chat_enabled?: boolean + ai_alert_summaries_enabled?: boolean + } = {} + if (data.attack_mode !== project.attack_mode) { + projectSettings.attack_mode = data.attack_mode + } + if ( + (data.ai_debug_chat_enabled ?? false) !== + (project.ai_debug_chat_enabled ?? false) + ) { + projectSettings.ai_debug_chat_enabled = data.ai_debug_chat_enabled + } + if ( + (data.ai_alert_summaries_enabled ?? false) !== + (project.ai_alert_summaries_enabled ?? false) + ) { + projectSettings.ai_alert_summaries_enabled = + data.ai_alert_summaries_enabled + } - // Update attack mode if changed - if (attackModeChanged) { + if (Object.keys(projectSettings).length > 0) { await toast.promise( updateProjectSettings.mutateAsync({ path: { project_id: project.id }, - body: { - attack_mode: data.attack_mode, - }, + body: projectSettings, }), { - loading: 'Updating attack mode...', - success: 'Attack mode updated successfully', - error: 'Failed to update attack mode', + loading: 'Updating project settings...', + success: 'Project settings updated successfully', + error: 'Failed to update project settings', } ) } @@ -288,6 +309,64 @@ export function ProjectSecuritySettings({ + {/* AI Assistance Card */} + + + + + AI Assistance + + + Opt in to AI features powered by your configured AI provider. Off by + default; uses your own provider key and counts against its budget. + + + +
+
+ +

+ Offer a “Debug with AI” chat on failed deployments to + investigate and fix problems. +

+
+ + setValue('ai_debug_chat_enabled', checked, { + shouldDirty: true, + }) + } + /> +
+ +
+
+ +

+ Enrich metric alert notifications with a plain-language AI + summary. +

+
+ + setValue('ai_alert_summaries_enabled', checked, { + shouldDirty: true, + }) + } + /> +
+
+ + + +
+ {/* Security Headers Card */} From f95cf4aacc01aecdc7668a9432edb10194c3b7b4 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 27 Jun 2026 18:21:14 +0200 Subject: [PATCH 42/56] =?UTF-8?q?feat(web):=20DeploymentDebugChat=20?= =?UTF-8?q?=E2=80=94=20streaming=20AI=20debug=20chat=20on=20failed=20deplo?= =?UTF-8?q?yments=20(ADR-023)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-contained chat panel mounted on the deployment page only when ai_debug_chat_enabled is on AND the deployment failed. Finds/loads an existing conversation or starts one (auto-asks for a diagnosis), renders the turn history, and streams the assistant reply via a manual SSE fetch reader (the generated SDK can't stream). Defensive: degrades to an error message, never blocks the page. --- .../deployments/DeploymentDebugChat.tsx | 272 ++++++++++++++++++ web/src/pages/DeploymentDetails.tsx | 10 + 2 files changed, 282 insertions(+) create mode 100644 web/src/components/deployments/DeploymentDebugChat.tsx diff --git a/web/src/components/deployments/DeploymentDebugChat.tsx b/web/src/components/deployments/DeploymentDebugChat.tsx new file mode 100644 index 000000000..8c257d9b6 --- /dev/null +++ b/web/src/components/deployments/DeploymentDebugChat.tsx @@ -0,0 +1,272 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' +import { Loader2, MessageSquare, Send } from 'lucide-react' +import { useCallback, useEffect, useRef, useState } from 'react' + +interface ChatMessage { + role: string + content: string +} + +interface DeploymentDebugChatProps { + projectId: number + deploymentId: number +} + +/** + * Persistent AI debugging chat for a failed deployment (ADR-023). Rendered only + * when the project's `ai_debug_chat_enabled` toggle is on and the deployment + * failed. The streaming reply is consumed via a manual SSE fetch (the generated + * SDK can't stream); find/history use the same cookie-authed `/api` surface. + */ +export function DeploymentDebugChat({ + projectId, + deploymentId, +}: DeploymentDebugChatProps) { + const base = `/api/projects/${projectId}/ai/conversations` + const [publicId, setPublicId] = useState(null) + const [messages, setMessages] = useState([]) + const [input, setInput] = useState('') + const [streaming, setStreaming] = useState(false) + const [starting, setStarting] = useState(false) + const [error, setError] = useState(null) + const scrollRef = useRef(null) + + // Load an existing conversation for this deployment, if one exists. + useEffect(() => { + let ignore = false + fetch(`${base}?context_type=deployment&context_id=${deploymentId}`, { + credentials: 'include', + }) + .then((r) => (r.ok ? r.json() : null)) + .then(async (conv) => { + if (ignore || !conv) return + setPublicId(conv.public_id) + const detail = await fetch(`${base}/${conv.public_id}`, { + credentials: 'include', + }) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null) + if (!ignore && detail) setMessages(detail.messages ?? []) + }) + .catch(() => {}) + return () => { + ignore = true + } + }, [base, deploymentId]) + + useEffect(() => { + scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }) + }, [messages]) + + const send = useCallback( + async (text: string, conversationId?: string) => { + const id = conversationId ?? publicId + const content = text.trim() + if (!id || !content) return + setInput('') + setError(null) + setStreaming(true) + setMessages((m) => [ + ...m, + { role: 'user', content }, + { role: 'assistant', content: '' }, + ]) + try { + const res = await fetch(`${base}/${id}/messages`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + }, + body: JSON.stringify({ content }), + }) + if (!res.ok || !res.body) { + const problem = await res.json().catch(() => ({})) + setError(problem.detail || 'The AI request failed.') + return + } + const reader = res.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + for (;;) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + let boundary + while ((boundary = buffer.indexOf('\n\n')) >= 0) { + const rawEvent = buffer.slice(0, boundary) + buffer = buffer.slice(boundary + 2) + let isError = false + const dataParts: string[] = [] + for (const line of rawEvent.split('\n')) { + if (line.startsWith('event:')) { + if (line.slice(6).trim() === 'error') isError = true + } else if (line.startsWith('data:')) { + dataParts.push(line.slice(5).replace(/^ /, '')) + } + } + const chunk = dataParts.join('\n') + if (isError) { + if (chunk) setError(chunk) + continue + } + if (chunk) { + setMessages((m) => { + const copy = [...m] + const last = copy[copy.length - 1] + copy[copy.length - 1] = { + role: 'assistant', + content: (last?.content ?? '') + chunk, + } + return copy + }) + } + } + } + } catch { + setError('Connection error while talking to the AI.') + } finally { + setStreaming(false) + } + }, + [base, publicId] + ) + + const start = useCallback(async () => { + setStarting(true) + setError(null) + try { + const res = await fetch(base, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + context_type: 'deployment', + context_id: String(deploymentId), + }), + }) + if (!res.ok) { + const problem = await res.json().catch(() => ({})) + setError( + problem.detail || + 'Could not start the chat. Make sure an AI provider is configured.' + ) + return + } + const conv = await res.json() + setPublicId(conv.public_id) + setMessages([]) + void send( + 'Diagnose this deployment failure and suggest concrete fixes.', + conv.public_id + ) + } catch { + setError('Could not start the chat.') + } finally { + setStarting(false) + } + }, [base, deploymentId, send]) + + const visible = messages.filter((m) => m.role !== 'system') + + if (!publicId) { + return ( + + + + + Debug with AI + + + Start an AI chat to investigate why this deployment failed. It opens + with a diagnosis and you can ask follow-up questions. + + + + + {error &&

{error}

} +
+
+ ) + } + + return ( + + + + + Debug with AI + + + +
+ {visible.length === 0 && ( +

+ Analyzing the failure… +

+ )} + {visible.map((m, i) => ( +
+
+ {m.content || + (streaming && i === visible.length - 1 ? '…' : '')} +
+
+ ))} +
+ {error &&

{error}

} +
+