diff --git a/CLAUDE.md b/CLAUDE.md index 7f51271..d35c3fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -197,3 +197,4 @@ Core clients are tested using mocked API calls with wiremock. All functions maki ### Comments - When doing anything out of the ordinary or breaking conventions or patterns, add a comment explaining the "why" behind it +- Do not reference Linear issue IDs (e.g. `DX-1234`), PR numbers, ticket URLs, or other internal tracking identifiers in code comments. The "why" should be self-contained — describe the constraint or behaviour itself, not where to look it up. Internal context belongs in the commit message or PR description. diff --git a/crates/core/README.md b/crates/core/README.md index 7983c37..3a2bdb0 100644 --- a/crates/core/README.md +++ b/crates/core/README.md @@ -836,7 +836,7 @@ Returns metric series for an endpoint over a time period. **Parameters**: `id` (endpoint id, required); body: `period` (`"hour"` | `"day"` | `"week"` | `"month"`), `metric` (e.g. `"method_calls_over_time"`, `"response_status_breakdown"`). -**Returns**: `GetEndpointMetricsResponse` with `data: EndpointMetric[]`. +**Returns**: `GetEndpointMetricsResponse` with `data: Vec`. Each `EndpointMetric` has `tag: Vec` and `data: Vec>` of `[timestamp, value]` pairs. Single-axis series (e.g. `response_time_over_time` with a percentile) come back as a one-element tag like `vec!["p95"]`; multi-axis series come back as `vec!["network", "arbitrum-mainnet"]`. ```rust // Rust @@ -853,7 +853,7 @@ Returns account-level metric series. Supports an optional `percentile` (e.g. `"p **Parameters**: `period` (required), `metric` (required), `percentile` (string, optional). -**Returns**: `GetAccountMetricsResponse` with `data: EndpointMetric[]`. +**Returns**: `GetAccountMetricsResponse` with `data: Vec`. See `get_endpoint_metrics` above for the `tag: Vec` shape. ```rust // Rust diff --git a/crates/core/examples/admin_e2e.rs b/crates/core/examples/admin_e2e.rs index 542c2ac..e78496d 100644 --- a/crates/core/examples/admin_e2e.rs +++ b/crates/core/examples/admin_e2e.rs @@ -97,7 +97,17 @@ async fn main() { }) .await { - Ok(resp) => println!("get_account_metrics: {} series", resp.data.len()), + Ok(resp) => { + let first = resp + .data + .first() + .map(|m| m.tag.join(":")) + .unwrap_or_else(|| "".to_string()); + println!( + "get_account_metrics: {} series, first tag: {first}", + resp.data.len() + ); + } Err(e) => eprintln!("get_account_metrics error: {e}"), } @@ -254,7 +264,17 @@ async fn main() { ) .await { - Ok(resp) => println!("get_endpoint_metrics: {} series", resp.data.len()), + Ok(resp) => { + let first = resp + .data + .first() + .map(|m| m.tag.join(":")) + .unwrap_or_else(|| "".to_string()); + println!( + "get_endpoint_metrics: {} series, first tag: {first}", + resp.data.len() + ); + } Err(e) => eprintln!("get_endpoint_metrics error: {e}"), } diff --git a/crates/core/examples/streams_e2e.rs b/crates/core/examples/streams_e2e.rs index cc1ac40..b8f0272 100644 --- a/crates/core/examples/streams_e2e.rs +++ b/crates/core/examples/streams_e2e.rs @@ -33,7 +33,7 @@ async fn main() { network: "ethereum-mainnet".to_string(), dataset: StreamDataset::Block, block: "17811625".to_string(), - filter_function: Some("ZnVuY3Rpb24gbWFpbihkYXRhKSB7IHJldHVybiBkYXRhOyB9".to_string()), + filter_function: "ZnVuY3Rpb24gbWFpbihkYXRhKSB7IHJldHVybiBkYXRhOyB9".to_string(), filter_language: None, address_book_config: None, }; diff --git a/crates/core/src/admin/endpoint_metrics.rs b/crates/core/src/admin/endpoint_metrics.rs index 74979b0..d69bdfc 100644 --- a/crates/core/src/admin/endpoint_metrics.rs +++ b/crates/core/src/admin/endpoint_metrics.rs @@ -4,7 +4,34 @@ use napi_derive::napi; use pyo3::pyclass; #[cfg(feature = "python")] use pyo3_stub_gen::derive::gen_stub_pyclass; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; + +// The metrics endpoints return `tag` as either a plain string (single-axis +// series like `"total"` or `"p95"`) or a tuple like `["network", "mainnet"]` +// (multi-axis series). Normalise both to a `Vec` so callers always +// see an array. +fn tag_as_vec<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + use serde::de::Error; + match serde_json::Value::deserialize(deserializer)? { + serde_json::Value::String(s) => Ok(vec![s]), + serde_json::Value::Array(items) => items + .into_iter() + .map(|v| match v { + serde_json::Value::String(s) => Ok(s), + other => Err(D::Error::custom(format!( + "expected string in tag array, got {other}" + ))), + }) + .collect(), + serde_json::Value::Null => Ok(Vec::new()), + other => Err(D::Error::custom(format!( + "expected string or array of strings for tag, got {other}" + ))), + } +} /// Parameters for `get_endpoint_metrics`. #[cfg_attr(feature = "python", gen_stub_pyclass)] @@ -41,8 +68,11 @@ pub struct GetAccountMetricsRequest { pub struct EndpointMetric { /// Data points, each as `[timestamp, value]`. pub data: Vec>, - /// Human-readable tag identifying the series. - pub tag: String, + /// Tag identifying the series. Single-axis metrics return a one-element + /// vector (e.g. `["total"]`, `["p95"]`); multi-axis metrics return the + /// key/value pair (e.g. `["network", "arbitrum-mainnet"]`). + #[serde(deserialize_with = "tag_as_vec")] + pub tag: Vec, } /// Response from `get_endpoint_metrics`. @@ -70,3 +100,49 @@ pub struct GetAccountMetricsResponse { /// Error message when the request did not succeed. pub error: Option, } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +mod tests { + use super::EndpointMetric; + + #[test] + fn tag_deserializes_from_string() { + let m: EndpointMetric = + serde_json::from_str(r#"{"data": [[1, 2]], "tag": "total"}"#).unwrap(); + assert_eq!(m.tag, vec!["total".to_string()]); + } + + #[test] + fn tag_deserializes_from_tuple() { + let m: EndpointMetric = + serde_json::from_str(r#"{"data": [[1, 2]], "tag": ["network", "arbitrum-mainnet"]}"#) + .unwrap(); + assert_eq!( + m.tag, + vec!["network".to_string(), "arbitrum-mainnet".to_string()] + ); + } + + #[test] + fn tag_deserializes_from_null() { + let m: EndpointMetric = serde_json::from_str(r#"{"data": [[1, 2]], "tag": null}"#).unwrap(); + assert!(m.tag.is_empty()); + } + + #[test] + fn tag_rejects_mixed_array() { + let err = + serde_json::from_str::(r#"{"data": [], "tag": ["x", 5]}"#).unwrap_err(); + assert!(err.to_string().contains("expected string in tag array")); + } + + #[test] + fn tag_rejects_object() { + let err = serde_json::from_str::(r#"{"data": [], "tag": {"k": "v"}}"#) + .unwrap_err(); + assert!(err + .to_string() + .contains("expected string or array of strings for tag")); + } +} diff --git a/crates/core/src/admin/mod.rs b/crates/core/src/admin/mod.rs index 31b9927..42ec3b2 100644 --- a/crates/core/src/admin/mod.rs +++ b/crates/core/src/admin/mod.rs @@ -2638,7 +2638,7 @@ mod tests { Mock::given(method("GET")) .and(path("/endpoints/ep123/metrics")) .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "data": [{"data": [[1700000000, 42]], "tag": "mainnet"}], + "data": [{"data": [[1700000000, 42]], "tag": ["network", "mainnet"]}], "error": null }))) .mount(&server) @@ -2655,7 +2655,10 @@ mod tests { .await .unwrap(); assert_eq!(resp.data.len(), 1); - assert_eq!(resp.data[0].tag, "mainnet"); + assert_eq!( + resp.data[0].tag, + vec!["network".to_string(), "mainnet".to_string()] + ); } #[tokio::test] @@ -2679,6 +2682,47 @@ mod tests { }; let resp = sdk.admin.get_account_metrics(¶ms).await.unwrap(); assert_eq!(resp.data.len(), 1); + assert_eq!(resp.data[0].tag, vec!["total".to_string()]); + } + + // Regression: the metrics endpoints return `tag` as either a plain string + // (single-axis series) or a `[key, value]` tuple (multi-axis series). + // Exercise both shapes so any future serde change that breaks either + // branch fails loudly. + #[tokio::test] + async fn get_account_metrics_decodes_tuple_tag() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/metrics")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "data": [ + {"tag": ["network", "arbitrum-mainnet"], "data": [[1779109200, 40]]}, + {"tag": ["network", "mainnet"], "data": [[1779116400, 40]]}, + {"tag": "p95", "data": [[1779116400, 12]]} + ], + "error": null + }))) + .mount(&server) + .await; + + let sdk = make_sdk(format!("{}/", server.uri())); + let params = GetAccountMetricsRequest { + period: "day".to_string(), + metric: "credits_over_time".to_string(), + percentile: None, + }; + let resp = sdk.admin.get_account_metrics(¶ms).await.unwrap(); + assert_eq!(resp.data.len(), 3); + assert_eq!( + resp.data[0].tag, + vec!["network".to_string(), "arbitrum-mainnet".to_string()] + ); + assert_eq!( + resp.data[1].tag, + vec!["network".to_string(), "mainnet".to_string()] + ); + assert_eq!(resp.data[2].tag, vec!["p95".to_string()]); } #[tokio::test] @@ -2932,6 +2976,29 @@ mod tests { assert!(resp.data.unwrap().success.unwrap()); } + // Wire-inspection regression: confirm an empty endpoint_ids array reaches + // the wire as `[]` (not omitted), so any future `skip_serializing_if` + // change that drops the empty case fails loudly. + #[tokio::test] + async fn update_team_endpoints_empty_array_wire_body() { + use wiremock::matchers::body_json; + let server = MockServer::start().await; + Mock::given(method("PATCH")) + .and(path("/teams/1/endpoints")) + .and(body_json(serde_json::json!({ "endpoint_ids": [] }))) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "data": {"success": true}, + "error": null + }))) + .mount(&server) + .await; + let sdk = make_sdk(format!("{}/", server.uri())); + let params = UpdateTeamEndpointsRequest { + endpoint_ids: vec![], + }; + sdk.admin.update_team_endpoints(1, ¶ms).await.unwrap(); + } + #[tokio::test] async fn invite_team_member_success() { let server = MockServer::start().await; diff --git a/crates/core/src/kvstore/mod.rs b/crates/core/src/kvstore/mod.rs index 965bdbc..94172b9 100644 --- a/crates/core/src/kvstore/mod.rs +++ b/crates/core/src/kvstore/mod.rs @@ -681,7 +681,7 @@ impl KvStoreApiClient { mod tests { use super::*; use crate::{KvStoreConfig, QuicknodeSdk, SdkFullConfig}; - use wiremock::matchers::{method, path}; + use wiremock::matchers::{body_json, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; fn make_sdk(base_url: String) -> QuicknodeSdk { @@ -938,6 +938,59 @@ mod tests { assert!(matches!(err, SdkError::Api { .. })); } + // Wire-inspection regressions: confirm that addSets and deleteSets reach + // the wire under the names the API expects, so any future serde rename of + // these fields fails loudly. + #[tokio::test] + async fn bulk_sets_wire_body_add_sets() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/sets/bulk")) + .and(body_json(serde_json::json!({ + "add_sets": {"k1": "v1"} + }))) + .respond_with( + ResponseTemplate::new(201) + .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})), + ) + .mount(&server) + .await; + let sdk = make_sdk(format!("{}/", server.uri())); + let mut add = HashMap::new(); + add.insert("k1".to_string(), "v1".to_string()); + sdk.kvstore + .bulk_sets(&BulkSetsParams { + add_sets: Some(add), + delete_sets: None, + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn bulk_sets_wire_body_delete_sets() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/sets/bulk")) + .and(body_json(serde_json::json!({ + "delete_sets": ["k1", "k2"] + }))) + .respond_with( + ResponseTemplate::new(201) + .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})), + ) + .mount(&server) + .await; + let sdk = make_sdk(format!("{}/", server.uri())); + sdk.kvstore + .bulk_sets(&BulkSetsParams { + add_sets: None, + delete_sets: Some(vec!["k1".to_string(), "k2".to_string()]), + }) + .await + .unwrap(); + } + #[tokio::test] async fn delete_set_success() { let server = MockServer::start().await; @@ -1192,6 +1245,37 @@ mod tests { .unwrap(); } + // Wire-inspection regression: confirm addItems/removeItems reach the wire + // under the names the API expects, so any future serde rename of these + // fields fails loudly. + #[tokio::test] + async fn update_list_wire_body() { + let server = MockServer::start().await; + Mock::given(method("PATCH")) + .and(path("/lists/my-list")) + .and(body_json(serde_json::json!({ + "add_items": ["c"], + "remove_items": ["a"] + }))) + .respond_with( + ResponseTemplate::new(200) + .set_body_json(serde_json::json!({"code": 0, "msg": "ok", "data": null})), + ) + .mount(&server) + .await; + let sdk = make_sdk(format!("{}/", server.uri())); + sdk.kvstore + .update_list( + "my-list", + &UpdateListParams { + add_items: Some(vec!["c".to_string()]), + remove_items: Some(vec!["a".to_string()]), + }, + ) + .await + .unwrap(); + } + #[tokio::test] async fn update_list_api_error() { let server = MockServer::start().await; diff --git a/crates/core/src/streams/mod.rs b/crates/core/src/streams/mod.rs index edb83be..5b107d2 100644 --- a/crates/core/src/streams/mod.rs +++ b/crates/core/src/streams/mod.rs @@ -944,7 +944,7 @@ mod tests { network: "ethereum-mainnet".to_string(), dataset: StreamDataset::Block, block: "17811625".to_string(), - filter_function: None, + filter_function: "ZnVuY3Rpb24gbWFpbihkYXRhKSB7IHJldHVybiBkYXRhOyB9".to_string(), filter_language: None, address_book_config: None, }; @@ -965,7 +965,7 @@ mod tests { network: "ethereum-mainnet".to_string(), dataset: StreamDataset::Block, block: "17811625".to_string(), - filter_function: None, + filter_function: "ZnVuY3Rpb24gbWFpbihkYXRhKSB7IHJldHVybiBkYXRhOyB9".to_string(), filter_language: None, address_book_config: None, }; diff --git a/crates/core/src/streams/stream.rs b/crates/core/src/streams/stream.rs index 156c923..7213cbe 100644 --- a/crates/core/src/streams/stream.rs +++ b/crates/core/src/streams/stream.rs @@ -806,9 +806,8 @@ pub struct TestFilterParams { pub dataset: StreamDataset, /// Specific block number to feed into the filter for the test. pub block: String, - /// Base64-encoded filter function to evaluate. - #[serde(skip_serializing_if = "Option::is_none")] - pub filter_function: Option, + /// Base64-encoded filter function to evaluate. Required by the API. To inspect raw block data with no transformation, supply a base64-encoded identity function such as `function main(d){return d;}`. + pub filter_function: String, /// Language the filter function is written in. #[serde(skip_serializing_if = "Option::is_none")] pub filter_language: Option, diff --git a/crates/core/src/webhooks/mod.rs b/crates/core/src/webhooks/mod.rs index dadf52c..0bbd919 100644 --- a/crates/core/src/webhooks/mod.rs +++ b/crates/core/src/webhooks/mod.rs @@ -712,6 +712,34 @@ mod tests { assert_eq!(resp.id, "wh-1234-5678"); } + // Wire-inspection regression: confirm that `name` reaches the wire when + // supplied so any future serde rename/drop of the field fails loudly. + #[tokio::test] + async fn update_webhook_template_wire_body_includes_name() { + use wiremock::matchers::body_partial_json; + let server = MockServer::start().await; + Mock::given(method("PATCH")) + .and(path_regex("/webhooks/test-id/template/evmWalletFilter")) + .and(body_partial_json(serde_json::json!({"name": "new-name"}))) + .respond_with(ResponseTemplate::new(200).set_body_json(webhook_response_json())) + .mount(&server) + .await; + let sdk = make_sdk(format!("{}/", server.uri())); + let template_args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { + wallets: vec!["0xabc".to_string()], + }); + let params = UpdateWebhookTemplateParams { + name: Some("new-name".to_string()), + notification_email: None, + destination_attributes: None, + template_args, + }; + sdk.webhooks + .update_webhook_template("test-id", ¶ms) + .await + .unwrap(); + } + #[tokio::test] async fn update_webhook_template_api_error() { let server = MockServer::start().await; diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs index 8d792f5..ae0da52 100644 --- a/crates/python/src/lib.rs +++ b/crates/python/src/lib.rs @@ -1729,7 +1729,7 @@ impl StreamsApiClient { /// Runs a filter function against a specified block on a given network and /// dataset, returning the filter's output so it can be validated before /// being attached to a live stream. - #[pyo3(signature = (network, dataset, block, filter_function=None, filter_language=None))] + #[pyo3(signature = (network, dataset, block, filter_function, filter_language=None))] #[gen_stub(override_return_type( type_repr = "typing.Coroutine[typing.Any, typing.Any, TestFilterResponse]" ))] @@ -1739,7 +1739,7 @@ impl StreamsApiClient { network: String, dataset: String, block: String, - filter_function: Option, + filter_function: String, filter_language: Option, ) -> PyResult> { let client = self.inner.clone(); diff --git a/crates/ruby/src/lib.rs b/crates/ruby/src/lib.rs index 75e5aca..4b49dac 100644 --- a/crates/ruby/src/lib.rs +++ b/crates/ruby/src/lib.rs @@ -1367,7 +1367,7 @@ impl StreamsApiClient { network: hash_require_string(&opts, "network")?, dataset, block: hash_require_string(&opts, "block")?, - filter_function: hash_get_string(&opts, "filter_function")?, + filter_function: hash_require_string(&opts, "filter_function")?, filter_language, address_book_config: None, }; diff --git a/npm/README.md b/npm/README.md index 462eec6..9b0e694 100644 --- a/npm/README.md +++ b/npm/README.md @@ -796,7 +796,7 @@ Returns metric series for an endpoint over a time period. **Parameters**: `id` (endpoint id, required); body: `period` (`"hour"` | `"day"` | `"week"` | `"month"`), `metric` (e.g. `"method_calls_over_time"`, `"response_status_breakdown"`). -**Returns**: `GetEndpointMetricsResponse` with `data: EndpointMetric[]`. +**Returns**: `GetEndpointMetricsResponse` with `data: EndpointMetric[]`. Each `EndpointMetric` has a `tag: string[]` and a `data: [timestamp, value][]`. Single-axis series (e.g. `response_time_over_time` with a percentile) come back as a one-element tag like `["p95"]`; multi-axis series come back as `["network", "arbitrum-mainnet"]`. ```typescript // Node.js @@ -812,7 +812,7 @@ Returns account-level metric series. Supports an optional `percentile` (e.g. `"p **Parameters**: `period` (required), `metric` (required), `percentile` (string, optional). -**Returns**: `GetAccountMetricsResponse` with `data: EndpointMetric[]`. +**Returns**: `GetAccountMetricsResponse` with `data: EndpointMetric[]`. See `getEndpointMetrics` above for the `tag: string[]` shape. ```typescript // Node.js diff --git a/npm/examples/admin.ts b/npm/examples/admin.ts index 405684b..a36f418 100644 --- a/npm/examples/admin.ts +++ b/npm/examples/admin.ts @@ -29,6 +29,13 @@ async function main() { console.log(`account tags: ${tags.data.tags.length}`); } + const metrics = await qn.admin.getAccountMetrics({ + period: "day", + metric: "credits_over_time", + }); + const firstTag = metrics.data[0]?.tag.join(":") ?? ""; + console.log(`getAccountMetrics: ${metrics.data.length} series, first tag: ${firstTag}`); + if (response.data.length > 0) { const sec = await qn.admin.getEndpointSecurity(response.data[0].id); console.log(`getEndpointSecurity: has_data=${sec.data !== undefined && sec.data !== null}`); diff --git a/npm/index.d.ts b/npm/index.d.ts index c4ebbbe..91db289 100644 --- a/npm/index.d.ts +++ b/npm/index.d.ts @@ -208,8 +208,12 @@ export interface GetAccountMetricsRequest { export interface EndpointMetric { /** Data points, each as `[timestamp, value]`. */ data: Array> - /** Human-readable tag identifying the series. */ - tag: string + /** + * Tag identifying the series. Single-axis metrics return a one-element + * vector (e.g. `["total"]`, `["p95"]`); multi-axis metrics return the + * key/value pair (e.g. `["network", "arbitrum-mainnet"]`). + */ + tag: Array } /** Response from `get_endpoint_metrics`. */ export interface GetEndpointMetricsResponse { @@ -1420,8 +1424,8 @@ export interface TestFilterParams { dataset: StreamDataset /** Specific block number to feed into the filter for the test. */ block: string - /** Base64-encoded filter function to evaluate. */ - filterFunction?: string + /** Base64-encoded filter function to evaluate. Required by the API. To inspect raw block data with no transformation, supply a base64-encoded identity function such as `function main(d){return d;}`. */ + filterFunction: string /** Language the filter function is written in. */ filterLanguage?: FilterLanguage /** Address book linked to the filter, if any. */ diff --git a/python/README.md b/python/README.md index 4aafae0..c7a62c4 100644 --- a/python/README.md +++ b/python/README.md @@ -802,7 +802,7 @@ Returns metric series for an endpoint over a time period. **Parameters**: `id` (endpoint id, required); body: `period` (`"hour"` | `"day"` | `"week"` | `"month"`), `metric` (e.g. `"method_calls_over_time"`, `"response_status_breakdown"`). -**Returns**: `GetEndpointMetricsResponse` with `data: EndpointMetric[]`. +**Returns**: `GetEndpointMetricsResponse` with `data: list[EndpointMetric]`. Each `EndpointMetric` has a `tag: list[str]` and a `data: list[list[int]]` of `[timestamp, value]` pairs. Single-axis series (e.g. `response_time_over_time` with a percentile) come back as a one-element tag like `["p95"]`; multi-axis series come back as `["network", "arbitrum-mainnet"]`. ```python # Python @@ -819,7 +819,7 @@ Returns account-level metric series. Supports an optional `percentile` (e.g. `"p **Parameters**: `period` (required), `metric` (required), `percentile` (string, optional). -**Returns**: `GetAccountMetricsResponse` with `data: EndpointMetric[]`. +**Returns**: `GetAccountMetricsResponse` with `data: list[EndpointMetric]`. See `get_endpoint_metrics` above for the `tag: list[str]` shape. ```python # Python diff --git a/python/examples/admin.py b/python/examples/admin.py index bc83ace..842a108 100644 --- a/python/examples/admin.py +++ b/python/examples/admin.py @@ -32,6 +32,12 @@ async def main(): if tags.data is not None: print(f"account tags: {len(tags.data.tags)}") + metrics = await qn.admin.get_account_metrics( + period="day", metric="credits_over_time" + ) + first_tag = ":".join(metrics.data[0].tag) if metrics.data else "" + print(f"get_account_metrics: {len(metrics.data)} series, first tag: {first_tag}") + if response.data: sec = await qn.admin.get_endpoint_security(response.data[0].id) print(f"get_endpoint_security: has_data={sec.data is not None}") diff --git a/python/sdk/_core/__init__.pyi b/python/sdk/_core/__init__.pyi index eb0dc58..4e76384 100644 --- a/python/sdk/_core/__init__.pyi +++ b/python/sdk/_core/__init__.pyi @@ -2024,14 +2024,18 @@ class EndpointMetric: Data points, each as `[timestamp, value]`. """ @property - def tag(self) -> builtins.str: + def tag(self) -> builtins.list[builtins.str]: r""" - Human-readable tag identifying the series. + Tag identifying the series. Single-axis metrics return a one-element + vector (e.g. `["total"]`, `["p95"]`); multi-axis metrics return the + key/value pair (e.g. `["network", "arbitrum-mainnet"]`). """ @tag.setter - def tag(self, value: builtins.str) -> None: + def tag(self, value: builtins.list[builtins.str]) -> None: r""" - Human-readable tag identifying the series. + Tag identifying the series. Single-axis metrics return a one-element + vector (e.g. `["total"]`, `["p95"]`); multi-axis metrics return the + key/value pair (e.g. `["network", "arbitrum-mainnet"]`). """ @typing.final @@ -5211,7 +5215,7 @@ class StreamsApiClient: r""" Pauses a stream by ID, halting delivery until it is activated again. """ - def test_filter(self, network: builtins.str, dataset: builtins.str, block: builtins.str, filter_function: typing.Optional[builtins.str] = None, filter_language: typing.Optional[builtins.str] = None) -> typing.Coroutine[typing.Any, typing.Any, TestFilterResponse]: + def test_filter(self, network: builtins.str, dataset: builtins.str, block: builtins.str, filter_function: builtins.str, filter_language: typing.Optional[builtins.str] = None) -> typing.Coroutine[typing.Any, typing.Any, TestFilterResponse]: r""" Runs a filter function against a specified block on a given network and dataset, returning the filter's output so it can be validated before diff --git a/ruby/README.md b/ruby/README.md index e70b1e4..bb5c938 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -791,7 +791,7 @@ Returns metric series for an endpoint over a time period. **Parameters**: `id` (endpoint id, required); body: `period` (`"hour"` | `"day"` | `"week"` | `"month"`), `metric` (e.g. `"method_calls_over_time"`, `"response_status_breakdown"`). -**Returns**: `GetEndpointMetricsResponse` with `data: EndpointMetric[]`. +**Returns**: `GetEndpointMetricsResponse` (as a `Hashie::Mash`) with `data: Array`. Each `EndpointMetric` has `tag: Array` and `data: Array>` of `[timestamp, value]` pairs. Single-axis series (e.g. `response_time_over_time` with a percentile) come back as a one-element tag like `["p95"]`; multi-axis series come back as `["network", "arbitrum-mainnet"]`. ```ruby # Ruby @@ -808,7 +808,7 @@ Returns account-level metric series. Supports an optional `percentile` (e.g. `"p **Parameters**: `period` (required), `metric` (required), `percentile` (string, optional). -**Returns**: `GetAccountMetricsResponse` with `data: EndpointMetric[]`. +**Returns**: `GetAccountMetricsResponse` (as a `Hashie::Mash`) with `data: Array`. See `get_endpoint_metrics` above for the `tag: Array` shape. ```ruby # Ruby diff --git a/ruby/examples/admin_e2e.rb b/ruby/examples/admin_e2e.rb index 2fb2cf3..722fc74 100644 --- a/ruby/examples/admin_e2e.rb +++ b/ruby/examples/admin_e2e.rb @@ -56,7 +56,8 @@ puts "list_tags: #{resp.dig(:data, :tags)&.length || 0} tags" resp = qn.admin.get_account_metrics(period: "day", metric: "requests") -puts "get_account_metrics: #{resp[:data].length} series" +first_tag = resp[:data].first&.dig(:tag)&.join(":") || "" +puts "get_account_metrics: #{resp[:data].length} series, first tag: #{first_tag}" resp = qn.admin.list_invoices puts "list_invoices: #{resp[:data].inspect}" @@ -122,7 +123,8 @@ sleep 1 resp = qn.admin.get_endpoint_metrics(id: endpoint_id, period: "day", metric: "credits_over_time") -puts "get_endpoint_metrics: #{resp[:data].length} series" +first_tag = resp[:data].first&.dig(:tag)&.join(":") || "" +puts "get_endpoint_metrics: #{resp[:data].length} series, first tag: #{first_tag}" # ── Security options ────────────────────────────────────────────────────────── diff --git a/ruby/sig/quicknode_sdk.rbs b/ruby/sig/quicknode_sdk.rbs index 0ef4ab4..15a6f77 100644 --- a/ruby/sig/quicknode_sdk.rbs +++ b/ruby/sig/quicknode_sdk.rbs @@ -118,7 +118,7 @@ module QuicknodeSdk def delete_stream: (id: String) -> void def activate_stream: (id: String) -> void def pause_stream: (id: String) -> void - def test_filter: (network: String, dataset: String, block: String, ?filter_function: String, ?filter_language: String) -> untyped + def test_filter: (network: String, dataset: String, block: String, filter_function: String, ?filter_language: String) -> untyped def get_enabled_count: (?stream_type: String) -> untyped end