Skip to content

Commit 92e7fa5

Browse files
committed
Enabled
1 parent b3d239f commit 92e7fa5

File tree

7 files changed

+126
-10
lines changed

7 files changed

+126
-10
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ retry-policies = "0.4.0"
6161
reqwest-retry = "0.7.0"
6262
reqwest-middleware = "0.4.2"
6363
vrl = { version = "0.27.0", features = ["compiler", "parser", "value", "diagnostic", "stdlib", "core"] }
64+
regex-automata = "0.4.10"

bin/router/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ reqwest-retry = { workspace = true }
4646
reqwest-middleware = { workspace = true }
4747
vrl = { workspace = true }
4848
serde_json = { workspace = true }
49+
regex-automata = { workspace = true }
4950

5051
mimalloc = { version = "0.1.48", features = ["v3"] }
5152
moka = { version = "0.12.10", features = ["future"] }
5253
ulid = "1.2.1"
5354
tokio-util = "0.7.16"
5455
cookie = "0.18.1"
55-
regex-automata = "0.4.10"
5656
arc-swap = "1.7.1"

bin/router/src/pipeline/usage_reporting.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub fn collect_usage_report(
4242
execution_result: &PlanExecutionOutput,
4343
) {
4444
let mut rng = rand::rng();
45-
let sampled = rng.random::<f64>() < usage_config.sample_rate;
45+
let sampled = rng.random::<f64>() < usage_config.sample_rate.as_f64();
4646
if !sampled {
4747
return;
4848
}

lib/executor/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ xxhash-rust = { workspace = true }
3030
tokio = { workspace = true, features = ["sync"] }
3131
dashmap = { workspace = true }
3232
vrl = { workspace = true }
33+
regex-automata = { workspace = true }
3334

3435
ahash = "0.8.12"
35-
regex-automata = "0.4.10"
3636
strum = { version = "0.27.2", features = ["derive"] }
3737
ntex-http = "0.1.15"
3838
ordered-float = "4.2.0"

lib/router-config/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ http = { workspace = true }
2323
jsonwebtoken = { workspace = true }
2424
retry-policies = { workspace = true}
2525
tracing = { workspace = true }
26+
regex-automata = { workspace = true }
2627

2728
schemars = "1.0.4"
2829
humantime-serde = "1.1.1"

lib/router-config/src/usage_reporting.rs

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::time::Duration;
1+
use std::{fmt::Display, time::Duration};
22

33
use schemars::JsonSchema;
44
use serde::{Deserialize, Serialize};
@@ -11,16 +11,19 @@ pub struct UsageReportingConfig {
1111
/// Your [Registry Access Token](https://the-guild.dev/graphql/hive/docs/management/targets#registry-access-tokens) with write permission.
1212
pub access_token: String,
1313
/// A target ID, this can either be a slug following the format “$organizationSlug/$projectSlug/$targetSlug” (e.g “the-guild/graphql-hive/staging”) or an UUID (e.g. “a0f4c605-6541-4350-8cfe-b31f21a4bf80”). To be used when the token is configured with an organization access token.
14+
#[serde(deserialize_with = "deserialize_target_id")]
1415
pub target_id: Option<String>,
1516
/// For self-hosting, you can override `/usage` endpoint (defaults to `https://app.graphql-hive.com/usage`).
1617
#[serde(default = "default_endpoint")]
1718
pub endpoint: String,
1819
/// Sample rate to determine sampling.
19-
/// 0.0 = 0% chance of being sent
20-
/// 1.0 = 100% chance of being sent
21-
/// Default: 1.0
20+
/// 0% = never being sent
21+
/// 50% = half of the requests being sent
22+
/// 100% = always being sent
23+
/// Default: 100%
2224
#[serde(default = "default_sample_rate")]
23-
pub sample_rate: f64,
25+
#[schemars(with = "String")]
26+
pub sample_rate: Percentage,
2427
/// A list of operations (by name) to be ignored by Hive.
2528
/// Example: ["IntrospectionQuery", "MeQuery"]
2629
#[serde(default)]
@@ -94,8 +97,8 @@ fn default_endpoint() -> String {
9497
"https://app.graphql-hive.com/usage".to_string()
9598
}
9699

97-
fn default_sample_rate() -> f64 {
98-
1.0
100+
fn default_sample_rate() -> Percentage {
101+
Percentage::from_f64(1.0).unwrap()
99102
}
100103

101104
fn default_client_name_header() -> String {
@@ -125,3 +128,113 @@ fn default_connect_timeout() -> Duration {
125128
fn default_flush_interval() -> Duration {
126129
Duration::from_secs(5)
127130
}
131+
132+
// Target ID regexp for validation: slug format
133+
const TARGET_ID_SLUG_REGEX: &str = r"^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+$";
134+
// Target ID regexp for validation: UUID format
135+
const TARGET_ID_UUID_REGEX: &str =
136+
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
137+
138+
fn deserialize_target_id<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
139+
where
140+
D: serde::Deserializer<'de>,
141+
{
142+
let opt = Option::<String>::deserialize(deserializer)?;
143+
if let Some(ref s) = opt {
144+
let trimmed_s = s.trim();
145+
if trimmed_s.is_empty() {
146+
Ok(None)
147+
} else {
148+
let slug_regex =
149+
regex_automata::meta::Regex::new(TARGET_ID_SLUG_REGEX).map_err(|err| {
150+
serde::de::Error::custom(format!(
151+
"Failed to compile target_id slug regex: {}",
152+
err
153+
))
154+
})?;
155+
if slug_regex.is_match(trimmed_s) {
156+
return Ok(Some(trimmed_s.to_string()));
157+
}
158+
let uuid_regex =
159+
regex_automata::meta::Regex::new(TARGET_ID_UUID_REGEX).map_err(|err| {
160+
serde::de::Error::custom(format!(
161+
"Failed to compile target_id UUID regex: {}",
162+
err
163+
))
164+
})?;
165+
if uuid_regex.is_match(trimmed_s) {
166+
return Ok(Some(trimmed_s.to_string()));
167+
}
168+
Err(serde::de::Error::custom(format!(
169+
"Invalid target_id format: '{}'. It must be either in slug format '$organizationSlug/$projectSlug/$targetSlug' or UUID format 'a0f4c605-6541-4350-8cfe-b31f21a4bf80'",
170+
trimmed_s
171+
)))
172+
}
173+
} else {
174+
Ok(None)
175+
}
176+
}
177+
178+
#[derive(Debug, Clone, Copy)]
179+
pub struct Percentage {
180+
value: f64,
181+
}
182+
183+
impl Percentage {
184+
pub fn from_str(s: &str) -> Result<Self, String> {
185+
let s_trimmed = s.trim();
186+
if let Some(number_part) = s_trimmed.strip_suffix('%') {
187+
let value: f64 = number_part.parse().map_err(|err| {
188+
format!(
189+
"Failed to parse percentage value '{}': {}",
190+
number_part, err
191+
)
192+
})?;
193+
Ok(Percentage::from_f64(value / 100.0)?)
194+
} else {
195+
Err(format!(
196+
"Percentage value must end with '%', got: '{}'",
197+
s_trimmed
198+
))
199+
}
200+
}
201+
pub fn from_f64(value: f64) -> Result<Self, String> {
202+
if !(0.0..=1.0).contains(&value) {
203+
return Err(format!(
204+
"Percentage value must be between 0 and 1, got: {}",
205+
value
206+
));
207+
}
208+
Ok(Percentage { value })
209+
}
210+
pub fn as_f64(&self) -> f64 {
211+
self.value
212+
}
213+
}
214+
215+
impl Display for Percentage {
216+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217+
write!(f, "{}%", self.value * 100.0)
218+
}
219+
}
220+
221+
// Deserializer from `n%` string to `Percentage` struct
222+
impl<'de> Deserialize<'de> for Percentage {
223+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224+
where
225+
D: serde::Deserializer<'de>,
226+
{
227+
let s = String::deserialize(deserializer)?;
228+
Percentage::from_str(&s).map_err(serde::de::Error::custom)
229+
}
230+
}
231+
232+
// Serializer from `Percentage` struct to `n%` string
233+
impl Serialize for Percentage {
234+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
235+
where
236+
S: serde::Serializer,
237+
{
238+
serializer.serialize_str(&self.to_string())
239+
}
240+
}

0 commit comments

Comments
 (0)