Skip to main content

quicknode_sdk/admin/
endpoint_metrics.rs

1#[cfg(feature = "node")]
2use napi_derive::napi;
3#[cfg(feature = "python")]
4use pyo3::pyclass;
5#[cfg(feature = "python")]
6use pyo3_stub_gen::derive::gen_stub_pyclass;
7use serde::{Deserialize, Deserializer, Serialize};
8
9// The metrics endpoints return `tag` as either a plain string (single-axis
10// series like `"total"` or `"p95"`) or a tuple like `["network", "mainnet"]`
11// (multi-axis series). Normalise both to a `Vec<String>` so callers always
12// see an array.
13fn tag_as_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
14where
15    D: Deserializer<'de>,
16{
17    use serde::de::Error;
18    match serde_json::Value::deserialize(deserializer)? {
19        serde_json::Value::String(s) => Ok(vec![s]),
20        serde_json::Value::Array(items) => items
21            .into_iter()
22            .map(|v| match v {
23                serde_json::Value::String(s) => Ok(s),
24                other => Err(D::Error::custom(format!(
25                    "expected string in tag array, got {other}"
26                ))),
27            })
28            .collect(),
29        serde_json::Value::Null => Ok(Vec::new()),
30        other => Err(D::Error::custom(format!(
31            "expected string or array of strings for tag, got {other}"
32        ))),
33    }
34}
35
36/// Parameters for `get_endpoint_metrics`.
37#[cfg_attr(feature = "python", gen_stub_pyclass)]
38#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
39#[cfg_attr(feature = "node", napi(object))]
40#[derive(Debug, Clone, Default, Serialize)]
41pub struct GetEndpointMetricsRequest {
42    /// Time period (`hour`, `day`, `week`, or `month`).
43    pub period: String,
44    /// Metric name (e.g. `method_calls_over_time`, `response_status_breakdown`).
45    pub metric: String,
46}
47
48/// Parameters for `get_account_metrics`.
49#[cfg_attr(feature = "python", gen_stub_pyclass)]
50#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
51#[cfg_attr(feature = "node", napi(object))]
52#[derive(Debug, Clone, Default, Serialize)]
53pub struct GetAccountMetricsRequest {
54    /// Time period (`hour`, `day`, `week`, or `month`).
55    pub period: String,
56    /// Metric name (e.g. `method_calls_over_time`, `credits_over_time`).
57    pub metric: String,
58    /// Optional percentile for latency metrics (e.g. `p50`, `p95`, `p99`).
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub percentile: Option<String>,
61}
62
63/// A single metric series, consisting of a descriptive tag and timestamped data points.
64#[cfg_attr(feature = "python", gen_stub_pyclass)]
65#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
66#[cfg_attr(feature = "node", napi(object))]
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct EndpointMetric {
69    /// Data points, each as `[timestamp, value]`.
70    pub data: Vec<Vec<i64>>,
71    /// Tag identifying the series. Single-axis metrics return a one-element
72    /// vector (e.g. `["total"]`, `["p95"]`); multi-axis metrics return the
73    /// key/value pair (e.g. `["network", "arbitrum-mainnet"]`).
74    #[serde(deserialize_with = "tag_as_vec")]
75    pub tag: Vec<String>,
76}
77
78/// Response from `get_endpoint_metrics`.
79#[cfg_attr(feature = "python", gen_stub_pyclass)]
80#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
81#[cfg_attr(feature = "node", napi(object))]
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct GetEndpointMetricsResponse {
84    /// Metric series returned for the endpoint.
85    #[serde(default)]
86    pub data: Vec<EndpointMetric>,
87    /// Error message when the request did not succeed.
88    pub error: Option<String>,
89}
90
91/// Response from `get_account_metrics`.
92#[cfg_attr(feature = "python", gen_stub_pyclass)]
93#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
94#[cfg_attr(feature = "node", napi(object))]
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct GetAccountMetricsResponse {
97    /// Metric series returned for the account.
98    #[serde(default)]
99    pub data: Vec<EndpointMetric>,
100    /// Error message when the request did not succeed.
101    pub error: Option<String>,
102}
103
104#[cfg(test)]
105#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
106mod tests {
107    use super::EndpointMetric;
108
109    #[test]
110    fn tag_deserializes_from_string() {
111        let m: EndpointMetric =
112            serde_json::from_str(r#"{"data": [[1, 2]], "tag": "total"}"#).unwrap();
113        assert_eq!(m.tag, vec!["total".to_string()]);
114    }
115
116    #[test]
117    fn tag_deserializes_from_tuple() {
118        let m: EndpointMetric =
119            serde_json::from_str(r#"{"data": [[1, 2]], "tag": ["network", "arbitrum-mainnet"]}"#)
120                .unwrap();
121        assert_eq!(
122            m.tag,
123            vec!["network".to_string(), "arbitrum-mainnet".to_string()]
124        );
125    }
126
127    #[test]
128    fn tag_deserializes_from_null() {
129        let m: EndpointMetric = serde_json::from_str(r#"{"data": [[1, 2]], "tag": null}"#).unwrap();
130        assert!(m.tag.is_empty());
131    }
132
133    #[test]
134    fn tag_rejects_mixed_array() {
135        let err =
136            serde_json::from_str::<EndpointMetric>(r#"{"data": [], "tag": ["x", 5]}"#).unwrap_err();
137        assert!(err.to_string().contains("expected string in tag array"));
138    }
139
140    #[test]
141    fn tag_rejects_object() {
142        let err = serde_json::from_str::<EndpointMetric>(r#"{"data": [], "tag": {"k": "v"}}"#)
143            .unwrap_err();
144        assert!(err
145            .to_string()
146            .contains("expected string or array of strings for tag"));
147    }
148}