1use async_dropper_simple::{AsyncDrop, AsyncDropper};
2use graphql_tools::parser::schema::Document;
3use recloser::AsyncRecloser;
4use reqwest_middleware::ClientWithMiddleware;
5use std::{
6 collections::{hash_map::Entry, BTreeMap, HashMap},
7 sync::Arc,
8 time::Duration,
9};
10use thiserror::Error;
11use tokio_util::sync::CancellationToken;
12
13use crate::expressions::lib::FromVrlValue;
14use crate::{
15 agent::{buffer::AddStatus, utils::OperationProcessor},
16 expressions::ExecutableProgram,
17};
18use crate::{
19 agent::{buffer::Buffer, builder::UsageAgentBuilder},
20 expressions::values::boolean::BooleanConversionError,
21};
22use vrl::{compiler::Program as VrlProgram, core::Value as VrlValue, value::KeyString};
23
24#[derive(Debug, Clone, Default)]
25pub enum OperationType {
26 #[default]
27 Query,
28 Mutation,
29 Subscription,
30}
31
32#[derive(Debug, Default, Clone)]
33pub struct ExecutionReport {
34 pub schema: Arc<Document<'static, String>>,
35 pub client_name: Option<String>,
36 pub client_version: Option<String>,
37 pub timestamp: u64,
38 pub duration: Duration,
39 pub ok: bool,
40 pub errors: usize,
41 pub operation_body: String,
42 pub operation_name: Option<String>,
43 pub operation_type: Option<OperationType>,
44 pub persisted_document_hash: Option<String>,
45}
46
47typify::import_types!(schema = "./usage-report-v2.schema.json");
48
49pub struct UsageAgentInner {
50 pub(crate) endpoint: String,
51 pub(crate) buffer: Buffer<ExecutionReport>,
52 pub(crate) processor: OperationProcessor,
53 pub(crate) client: ClientWithMiddleware,
54 pub(crate) flush_interval: Duration,
55 pub(crate) circuit_breaker: AsyncRecloser,
56 pub(crate) exclude_expression: Option<VrlProgram>,
57}
58
59pub fn non_empty_string(value: Option<String>) -> Option<String> {
60 value.filter(|str| !str.is_empty())
61}
62
63#[derive(Error, Debug)]
64pub enum AgentError {
65 #[error("unable to acquire lock: {0}")]
66 Lock(String),
67 #[error("unable to send report: unauthorized")]
68 Unauthorized,
69 #[error("unable to send report: no access")]
70 Forbidden,
71 #[error("unable to send report: rate limited")]
72 RateLimited,
73 #[error("missing token")]
74 MissingToken,
75 #[error("your access token requires providing a 'target_id' option.")]
76 MissingTargetId,
77 #[error("using 'target_id' with legacy tokens is not supported")]
78 TargetIdWithLegacyToken,
79 #[error("invalid token provided")]
80 InvalidToken,
81 #[error(
82 "invalid target id provided: {0}, it should be either a slug like \"$organizationSlug/$projectSlug/$targetSlug\" or an UUID"
83 )]
84 InvalidTargetId(String),
85 #[error("unable to instantiate the http client for reports sending: {0}")]
86 HTTPClientCreationError(reqwest::Error),
87 #[error("unable to create circuit breaker: {0}")]
88 CircuitBreakerCreationError(#[from] crate::circuit_breaker::CircuitBreakerError),
89 #[error("rejected by the circuit breaker")]
90 CircuitBreakerRejected,
91 #[error("unable to send report: {0}")]
92 Unknown(String),
93 #[error("failed to compile exclude expression: {0}")]
94 ExcludeExpressionCompileError(#[from] crate::expressions::ExpressionCompileError),
95 #[error("failed to execute exclude expression: {0}")]
96 ExcludeExpressionExecutionError(#[from] crate::expressions::ExpressionExecutionError),
97 #[error("failed to convert exclude expression result to boolean: {0}")]
98 ExcludeExpressionResultConversionError(#[from] BooleanConversionError),
99}
100
101pub type UsageAgent = Arc<AsyncDropper<UsageAgentInner>>;
102
103#[async_trait::async_trait]
104pub trait UsageAgentExt {
105 fn builder() -> UsageAgentBuilder {
106 UsageAgentBuilder::default()
107 }
108 async fn flush(&self) -> Result<(), AgentError>;
109 async fn start_flush_interval(&self, token: &CancellationToken);
110 #[deprecated(note = "use `add_report_with_request` instead")]
113 async fn add_report(&self, execution_report: ExecutionReport) -> Result<(), AgentError>;
114
115 async fn add_report_with_request(
116 &self,
117 execution_report: ExecutionReport,
118 request: Option<RequestDetails>,
119 ) -> Result<(), AgentError>;
120}
121
122impl UsageAgentInner {
123 fn produce_report(&self, reports: Vec<ExecutionReport>) -> Result<Report, AgentError> {
124 let mut report = Report {
125 size: 0,
126 map: HashMap::new(),
127 operations: Vec::new(),
128 subscription_operations: Vec::new(),
129 };
130
131 for op in reports {
133 let operation = self.processor.process(&op.operation_body, &op.schema);
134 match operation {
135 Err(e) => {
136 tracing::warn!(
137 "Dropping operation \"{}\" (phase: PROCESSING): {}",
138 op.operation_name
139 .clone()
140 .or_else(|| Some("anonymous".to_string()))
141 .unwrap(),
142 e
143 );
144 continue;
145 }
146 Ok(operation) => match operation {
147 Some(operation) => {
148 let hash = operation.hash;
149
150 let client_name = non_empty_string(op.client_name);
151 let client_version = non_empty_string(op.client_version);
152
153 let metadata: Option<Metadata> =
154 if client_name.is_some() || client_version.is_some() {
155 Some(Metadata {
156 client: Some(Client {
157 name: client_name.unwrap_or_default(),
158 version: client_version.unwrap_or_default(),
159 }),
160 })
161 } else {
162 None
163 };
164 report.operations.push(RequestOperation {
165 operation_map_key: hash.clone(),
166 timestamp: op.timestamp,
167 execution: Execution {
168 ok: op.ok,
169 duration: op
176 .duration
177 .as_nanos()
178 .try_into()
179 .ok()
180 .unwrap_or(u64::MAX),
181 errors_total: op.errors.try_into().unwrap(),
182 },
183 persisted_document_hash: op
184 .persisted_document_hash
185 .map(PersistedDocumentHash),
186 metadata,
187 });
188 if let Entry::Vacant(e) = report.map.entry(ReportMapKey(hash)) {
189 e.insert(OperationMapRecord {
190 operation: operation.operation,
191 operation_name: non_empty_string(op.operation_name),
192 fields: operation.coordinates,
193 });
194 }
195 report.size += 1;
196 }
197 None => {
198 tracing::debug!(
199 "Dropping operation (phase: PROCESSING): probably introspection query"
200 );
201 }
202 },
203 }
204 }
205
206 Ok(report)
207 }
208
209 async fn send_report(&self, report: Report) -> Result<(), AgentError> {
210 if report.size == 0 {
211 return Ok(());
212 }
213 let resp_fut = self.client.post(&self.endpoint).json(&report).send();
215
216 let resp = self
217 .circuit_breaker
218 .call(resp_fut)
219 .await
220 .map_err(|e| match e {
221 recloser::Error::Inner(e) => AgentError::Unknown(e.to_string()),
222 recloser::Error::Rejected => AgentError::CircuitBreakerRejected,
223 })?;
224
225 match resp.status() {
226 reqwest::StatusCode::OK => Ok(()),
227 reqwest::StatusCode::UNAUTHORIZED => Err(AgentError::Unauthorized),
228 reqwest::StatusCode::FORBIDDEN => Err(AgentError::Forbidden),
229 reqwest::StatusCode::TOO_MANY_REQUESTS => Err(AgentError::RateLimited),
230 _ => Err(AgentError::Unknown(format!(
231 "({}) {}",
232 resp.status(),
233 resp.text().await.unwrap_or_default()
234 ))),
235 }
236 }
237
238 async fn handle_drained(&self, drained: Vec<ExecutionReport>) -> Result<(), AgentError> {
239 if drained.is_empty() {
240 return Ok(());
241 }
242 let report = self.produce_report(drained)?;
243 self.send_report(report).await
244 }
245
246 async fn flush(&self) -> Result<(), AgentError> {
247 let execution_reports = self.buffer.drain().await;
248
249 self.handle_drained(execution_reports).await?;
250
251 Ok(())
252 }
253}
254
255#[derive(Debug, Clone)]
256pub struct RequestDetails {
257 pub method: http::Method,
258 pub url: http::Uri,
259 pub headers: Vec<(String, String)>,
260}
261
262#[async_trait::async_trait]
263impl UsageAgentExt for UsageAgent {
264 async fn flush(&self) -> Result<(), AgentError> {
265 self.inner().flush().await
266 }
267
268 async fn start_flush_interval(&self, token: &CancellationToken) {
269 loop {
270 tokio::time::sleep(self.inner().flush_interval).await;
271 if token.is_cancelled() {
272 println!("Shutting down.");
273 return;
274 }
275 self.flush()
276 .await
277 .unwrap_or_else(|e| tracing::error!("Failed to flush usage reports: {}", e));
278 }
279 }
280
281 async fn add_report_with_request(
282 &self,
283 execution_report: ExecutionReport,
284 request: Option<RequestDetails>,
285 ) -> Result<(), AgentError> {
286 let inner = self.inner();
287
288 if let Some(exclude_program) = &inner.exclude_expression {
289 let result = exclude_program.execute(
290 get_vrl_value_from_execution_report_and_request(&execution_report, request),
291 )?;
292 let result_bool = bool::from_vrl_value(result)?;
293 if result_bool {
294 tracing::debug!(
295 "Excluding report for operation \"{}\" based on exclude expression evaluation",
296 execution_report
297 .operation_name
298 .clone()
299 .or_else(|| Some("anonymous".to_string()))
300 .unwrap()
301 );
302 return Ok(());
303 }
304 }
305
306 if let AddStatus::Full { drained } = inner.buffer.add(execution_report).await {
307 inner.handle_drained(drained).await?;
308 }
309
310 Ok(())
311 }
312 async fn add_report(&self, execution_report: ExecutionReport) -> Result<(), AgentError> {
313 self.add_report_with_request(execution_report, None).await
314 }
315}
316
317impl<'req, TBody> From<&'req http::Request<TBody>> for RequestDetails {
318 fn from(req: &'req http::Request<TBody>) -> Self {
319 let mut headers = Vec::with_capacity(req.headers().len());
320 for (name, value) in req.headers().iter() {
321 if let Ok(val_str) = value.to_str() {
322 headers.push((name.to_string(), val_str.to_string()));
323 }
324 }
325
326 RequestDetails {
327 method: req.method().clone(),
328 url: req.uri().clone(),
329 headers,
330 }
331 }
332}
333
334impl From<RequestDetails> for VrlValue {
335 fn from(details: RequestDetails) -> Self {
336 let mut merged_headers: BTreeMap<String, String> = BTreeMap::new();
337 for (header_name, header_value) in details.headers {
338 if let Some(existing_value) = merged_headers.get_mut(&header_name) {
339 existing_value.push_str(", ");
340 existing_value.push_str(&header_value);
341 } else {
342 merged_headers.insert(header_name, header_value);
343 }
344 }
345
346 let headers_value: BTreeMap<KeyString, VrlValue> = merged_headers
347 .into_iter()
348 .map(|(key, value)| (key.into(), value.into()))
349 .collect();
350 let headers_value = VrlValue::Object(headers_value);
351
352 let url_value = VrlValue::Object(BTreeMap::from([
354 ("host".into(), details.url.host().unwrap_or_default().into()),
355 ("path".into(), details.url.path().into()),
356 (
357 "port".into(),
358 details
359 .url
360 .port_u16()
361 .map(|p| VrlValue::Integer(p.into()))
362 .unwrap_or(VrlValue::Null),
363 ),
364 ]));
365
366 VrlValue::Object(BTreeMap::from([
368 ("method".into(), details.method.as_str().into()),
369 ("headers".into(), headers_value),
370 ("url".into(), url_value),
371 ]))
372 }
373}
374
375pub fn get_vrl_value_from_execution_report_and_request(
376 report: &ExecutionReport,
377 request: Option<RequestDetails>,
378) -> VrlValue {
379 let mut map = BTreeMap::from([("default".into(), VrlValue::Boolean(false))]);
380 let mut request_map = BTreeMap::new();
381
382 if let Some(request_details) = request {
383 if let VrlValue::Object(request_object) = VrlValue::from(request_details) {
384 request_map.extend(request_object);
385 }
386 }
387
388 request_map.insert(
389 "operation".into(),
390 VrlValue::Object(BTreeMap::from([
391 (
392 "name".into(),
393 report.operation_name.clone().unwrap_or_default().into(),
394 ),
395 (
396 "type".into(),
397 report
398 .operation_type
399 .as_ref()
400 .map(|operation_type| match operation_type {
401 OperationType::Query => "query",
402 OperationType::Mutation => "mutation",
403 OperationType::Subscription => "subscription",
404 })
405 .unwrap_or_default()
406 .into(),
407 ),
408 ("query".into(), report.operation_body.clone().into()),
409 ])),
410 );
411
412 if let Ok(timestamp_integer) = report.timestamp.try_into() {
413 let timestamp_value = VrlValue::Integer(timestamp_integer);
414 map.insert("timestamp".into(), timestamp_value);
415 request_map.insert("timestamp".into(), VrlValue::Integer(timestamp_integer));
416 }
417
418 map.insert("request".into(), VrlValue::Object(request_map));
419
420 VrlValue::Object(map)
421}
422
423#[async_trait::async_trait]
424impl AsyncDrop for UsageAgentInner {
425 async fn async_drop(&mut self) {
426 if let Err(e) = self.flush().await {
427 tracing::error!("Failed to flush usage reports during drop: {}", e);
428 }
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use std::{sync::Arc, time::Duration};
435
436 use graphql_tools::parser::{parse_query, parse_schema};
437 use reqwest::{
438 header::{AUTHORIZATION, CONTENT_TYPE, USER_AGENT},
439 Method,
440 };
441 use vrl::core::Value as VrlValue;
442 use vrl::value::KeyString;
443
444 use crate::agent::usage_agent::{
445 get_vrl_value_from_execution_report_and_request, ExecutionReport, OperationType, Report,
446 UsageAgent, UsageAgentExt,
447 };
448
449 fn vrl_get<'a>(value: &'a VrlValue, keys: &[&str]) -> &'a VrlValue {
451 let mut current = value;
452 for key in keys {
453 match current {
454 VrlValue::Object(map) => {
455 let ks: KeyString = (*key).into();
456 current = map
457 .get(&ks)
458 .unwrap_or_else(|| panic!("key '{}' not found", key));
459 }
460 _ => panic!("expected Object at key '{}'", key),
461 }
462 }
463 current
464 }
465
466 const CONTENT_TYPE_VALUE: &'static str = "application/json";
467 const GRAPHQL_CLIENT_NAME: &'static str = "Hive Client";
468 const GRAPHQL_CLIENT_VERSION: &'static str = "1.0.0";
469
470 #[tokio::test(flavor = "multi_thread")]
471 async fn should_send_data_to_hive() -> Result<(), Box<dyn std::error::Error>> {
472 let token = "Token";
473
474 let mut server = mockito::Server::new_async().await;
475
476 let server_url = server.url();
477
478 let timestamp = 1625247600;
479 let duration = Duration::from_millis(20);
480 let user_agent = "hive-router-sdk-test";
481
482 let mock = server
483 .mock("POST", "/200")
484 .match_header(AUTHORIZATION, format!("Bearer {}", token).as_str())
485 .match_header(CONTENT_TYPE, CONTENT_TYPE_VALUE)
486 .match_header(USER_AGENT, user_agent)
487 .match_header("X-Usage-API-Version", "2")
488 .match_request(move |request| {
489 let request_body = request.body().expect("Failed to extract body");
490 let report: Report = serde_json::from_slice(request_body)
491 .expect("Failed to parse request body as JSON");
492 assert_eq!(report.size, 1);
493 let record = report.map.values().next().expect("No operation record");
494 assert!(record.operation.contains("mutation deleteProject"));
496 assert_eq!(record.operation_name.as_deref(), Some("deleteProject"));
497 let expected_fields = vec![
499 "Mutation.deleteProject",
500 "Mutation.deleteProject.selector",
501 "DeleteProjectPayload.selector",
502 "ProjectSelector.organization",
503 "ProjectSelector.project",
504 "DeleteProjectPayload.deletedProject",
505 "Project.id",
506 "Project.cleanId",
507 "Project.name",
508 "Project.type",
509 "ProjectType.FEDERATION",
510 "ProjectType.STITCHING",
511 "ProjectType.SINGLE",
512 "ProjectType.CUSTOM",
513 "ProjectSelectorInput.organization",
514 "ID",
515 "ProjectSelectorInput.project",
516 ];
517 for field in &expected_fields {
518 assert!(
519 record.fields.contains(&field.to_string()),
520 "Missing field: {}",
521 field
522 );
523 }
524 assert_eq!(
525 record.fields.len(),
526 expected_fields.len(),
527 "Unexpected number of fields"
528 );
529
530 let operations = report.operations;
532 assert_eq!(operations.len(), 1); let operation = &operations[0];
535 let key = report.map.keys().next().expect("No operation key");
536 assert_eq!(operation.operation_map_key, key.0);
537 assert_eq!(operation.timestamp, timestamp);
538 assert_eq!(operation.execution.duration, duration.as_nanos() as u64);
539 assert_eq!(operation.execution.ok, true);
540 assert_eq!(operation.execution.errors_total, 0);
541 true
542 })
543 .expect(1)
544 .with_status(200)
545 .create_async()
546 .await;
547 let schema: graphql_tools::static_graphql::schema::Document = parse_schema(
548 r#"
549 type Query {
550 project(selector: ProjectSelectorInput!): Project
551 projectsByType(type: ProjectType!): [Project!]!
552 projects(filter: FilterInput): [Project!]!
553 }
554
555 type Mutation {
556 deleteProject(selector: ProjectSelectorInput!): DeleteProjectPayload!
557 }
558
559 input ProjectSelectorInput {
560 organization: ID!
561 project: ID!
562 }
563
564 input FilterInput {
565 type: ProjectType
566 pagination: PaginationInput
567 }
568
569 input PaginationInput {
570 limit: Int
571 offset: Int
572 }
573
574 type ProjectSelector {
575 organization: ID!
576 project: ID!
577 }
578
579 type DeleteProjectPayload {
580 selector: ProjectSelector!
581 deletedProject: Project!
582 }
583
584 type Project {
585 id: ID!
586 cleanId: ID!
587 name: String!
588 type: ProjectType!
589 buildUrl: String
590 validationUrl: String
591 }
592
593 enum ProjectType {
594 FEDERATION
595 STITCHING
596 SINGLE
597 CUSTOM
598 }
599 "#,
600 )?;
601
602 let op: graphql_tools::static_graphql::query::Document = parse_query(
603 r#"
604 mutation deleteProject($selector: ProjectSelectorInput!) {
605 deleteProject(selector: $selector) {
606 selector {
607 organization
608 project
609 }
610 deletedProject {
611 ...ProjectFields
612 }
613 }
614 }
615
616 fragment ProjectFields on Project {
617 id
618 cleanId
619 name
620 type
621 }
622 "#,
623 )?;
624
625 {
627 let usage_agent = UsageAgent::builder()
628 .token(token.into())
629 .endpoint(format!("{}/200", server_url))
630 .user_agent(user_agent.into())
631 .build()?;
632
633 let request = http::Request::builder()
634 .method(Method::POST)
635 .uri("http://localhost/graphql")
636 .body(())
637 .unwrap();
638
639 usage_agent
640 .add_report_with_request(
641 ExecutionReport {
642 schema: Arc::new(schema),
643 operation_body: op.to_string(),
644 operation_name: Some("deleteProject".to_string()),
645 operation_type: Some(OperationType::Mutation),
646 client_name: Some(GRAPHQL_CLIENT_NAME.to_string()),
647 client_version: Some(GRAPHQL_CLIENT_VERSION.to_string()),
648 timestamp,
649 duration,
650 ok: true,
651 errors: 0,
652 persisted_document_hash: None,
653 },
654 Some((&request).into()),
655 )
656 .await?;
657 }
658
659 mock.assert_async().await;
660
661 Ok(())
662 }
663
664 fn make_test_report(
665 operation_name: Option<&str>,
666 operation_type: OperationType,
667 operation_body: &str,
668 ) -> ExecutionReport {
669 let schema: graphql_tools::static_graphql::schema::Document =
670 parse_schema("type Query { hello: String }").unwrap();
671
672 ExecutionReport {
673 schema: Arc::new(schema),
674 operation_body: operation_body.to_string(),
675 operation_name: operation_name.map(|s| s.to_string()),
676 operation_type: Some(operation_type),
677 client_name: Some("test-client".to_string()),
678 client_version: Some("1.0.0".to_string()),
679 timestamp: 1625247600,
680 duration: Duration::from_millis(10),
681 ok: true,
682 errors: 0,
683 persisted_document_hash: None,
684 }
685 }
686
687 fn make_simple_report(
688 operation_name: Option<&str>,
689 operation_type: OperationType,
690 ) -> ExecutionReport {
691 make_test_report(operation_name, operation_type, "query { hello }")
692 }
693
694 fn make_simple_request() -> http::Request<()> {
695 http::Request::builder()
696 .method(Method::GET)
697 .uri("http://localhost/graphql")
698 .body(())
699 .unwrap()
700 }
701
702 #[test]
703 fn vrl_value_contains_operation_name() {
704 let report = make_simple_report(Some("MyQuery"), OperationType::Query);
705 let request = make_simple_request();
706 let value =
707 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
708
709 let name = vrl_get(&value, &["request", "operation", "name"]);
710 assert_eq!(name, &VrlValue::from("MyQuery"));
711 }
712
713 #[test]
714 fn vrl_value_contains_operation_type_query() {
715 let report = make_simple_report(Some("Q"), OperationType::Query);
716 let request = make_simple_request();
717 let value =
718 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
719
720 let op_type = vrl_get(&value, &["request", "operation", "type"]);
721 assert_eq!(op_type, &VrlValue::from("query"));
722 }
723
724 #[test]
725 fn vrl_value_contains_operation_type_mutation() {
726 let report = make_simple_report(Some("M"), OperationType::Mutation);
727 let request = make_simple_request();
728 let value =
729 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
730
731 let op_type = vrl_get(&value, &["request", "operation", "type"]);
732 assert_eq!(op_type, &VrlValue::from("mutation"));
733 }
734
735 #[test]
736 fn vrl_value_contains_operation_type_subscription() {
737 let report = make_simple_report(Some("S"), OperationType::Subscription);
738 let request = make_simple_request();
739 let value =
740 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
741
742 let op_type = vrl_get(&value, &["request", "operation", "type"]);
743 assert_eq!(op_type, &VrlValue::from("subscription"));
744 }
745
746 #[test]
747 fn vrl_value_contains_operation_body() {
748 let report = make_simple_report(Some("Q"), OperationType::Query);
749 let request = make_simple_request();
750 let value =
751 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
752
753 let query = vrl_get(&value, &["request", "operation", "query"]);
754 assert_eq!(query, &VrlValue::from("query { hello }"));
755 }
756
757 #[test]
758 fn vrl_value_contains_request_method() {
759 let report = make_test_report(Some("Q"), OperationType::Query, "query { hello }");
760 let request = http::Request::builder()
761 .method(Method::GET)
762 .uri("http://localhost/graphql")
763 .body(())
764 .unwrap();
765 let value =
766 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
767
768 let method = vrl_get(&value, &["request", "method"]);
769 assert_eq!(method, &VrlValue::from("GET"));
770 }
771
772 #[test]
773 fn vrl_value_contains_url_details() {
774 let report = make_test_report(Some("Q"), OperationType::Query, "query { hello }");
775 let request = http::Request::builder()
776 .method(Method::POST)
777 .uri("http://api.example.com:8080/v1/graphql")
778 .body(())
779 .unwrap();
780 let value =
781 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
782
783 assert_eq!(
784 vrl_get(&value, &["request", "url", "host"]),
785 &VrlValue::from("api.example.com")
786 );
787 assert_eq!(
788 vrl_get(&value, &["request", "url", "port"]),
789 &VrlValue::Integer(8080)
790 );
791 assert_eq!(
792 vrl_get(&value, &["request", "url", "path"]),
793 &VrlValue::from("/v1/graphql")
794 );
795 }
796
797 #[test]
798 fn vrl_value_contains_headers() {
799 let request = http::Request::builder()
800 .method(Method::POST)
801 .uri("http://localhost/graphql")
802 .header("x-custom-header", "custom-value")
803 .header("authorization", "Bearer token123")
804 .body(())
805 .unwrap();
806 let report = make_test_report(Some("Q"), OperationType::Query, "query { hello }");
807 let value =
808 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
809
810 assert_eq!(
811 vrl_get(&value, &["request", "headers", "x-custom-header"]),
812 &VrlValue::from("custom-value")
813 );
814 assert_eq!(
815 vrl_get(&value, &["request", "headers", "authorization"]),
816 &VrlValue::from("Bearer token123")
817 );
818 }
819
820 #[test]
821 fn vrl_value_joins_duplicate_header_values() {
822 let mut request = http::Request::builder()
823 .method(Method::POST)
824 .uri("http://localhost/graphql")
825 .body(())
826 .unwrap();
827
828 request
829 .headers_mut()
830 .append("x-scope", http::HeaderValue::from_static("one"));
831 request
832 .headers_mut()
833 .append("x-scope", http::HeaderValue::from_static("two"));
834
835 let report = make_test_report(Some("Q"), OperationType::Query, "query { hello }");
836 let value =
837 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
838
839 assert_eq!(
840 vrl_get(&value, &["request", "headers", "x-scope"]),
841 &VrlValue::from("one, two")
842 );
843 }
844
845 #[test]
846 fn vrl_value_anonymous_operation_has_empty_name() {
847 let report = make_simple_report(None, OperationType::Query);
848 let request = make_simple_request();
849 let value =
850 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
851
852 let name = vrl_get(&value, &["request", "operation", "name"]);
853 assert_eq!(name, &VrlValue::from(""));
854 }
855
856 #[test]
857 fn vrl_value_has_default_false() {
858 let report = make_simple_report(Some("Q"), OperationType::Query);
859 let request = make_simple_request();
860 let value =
861 get_vrl_value_from_execution_report_and_request(&report, Some((&request).into()));
862
863 let default_val = vrl_get(&value, &["default"]);
864 assert_eq!(default_val, &VrlValue::Boolean(false));
865 }
866
867 #[tokio::test(flavor = "multi_thread")]
868 async fn exclude_expression_filters_by_operation_name() -> Result<(), Box<dyn std::error::Error>>
869 {
870 let token = "Token";
871 let mut server = mockito::Server::new_async().await;
872 let server_url = server.url();
873
874 let mock = server
876 .mock("POST", "/200")
877 .expect(0)
878 .with_status(200)
879 .create_async()
880 .await;
881
882 {
883 let usage_agent = UsageAgent::builder()
884 .token(token.into())
885 .endpoint(format!("{}/200", server_url))
886 .buffer_size(1) .exclude_expression(r#".request.operation.name == "ExcludeMe""#.to_string())
888 .build()?;
889
890 let report = make_simple_report(Some("ExcludeMe"), OperationType::Query);
892 let request = make_simple_request();
893 usage_agent
894 .add_report_with_request(report, Some((&request).into()))
895 .await?;
896 }
897
898 mock.assert_async().await;
899 Ok(())
900 }
901
902 #[tokio::test(flavor = "multi_thread")]
903 async fn exclude_expression_allows_non_matching_operations(
904 ) -> Result<(), Box<dyn std::error::Error>> {
905 let token = "Token";
906 let mut server = mockito::Server::new_async().await;
907 let server_url = server.url();
908
909 let mock = server
911 .mock("POST", "/200")
912 .expect(1)
913 .with_status(200)
914 .create_async()
915 .await;
916
917 {
918 let usage_agent = UsageAgent::builder()
919 .token(token.into())
920 .endpoint(format!("{}/200", server_url))
921 .exclude_expression(r#".request.operation.name == "ExcludeMe""#.to_string())
922 .build()?;
923
924 let report = make_simple_report(Some("KeepMe"), OperationType::Query);
925 let request = make_simple_request();
926 usage_agent
927 .add_report_with_request(report, Some((&request).into()))
928 .await?;
929 }
930
931 mock.assert_async().await;
932 Ok(())
933 }
934
935 #[tokio::test(flavor = "multi_thread")]
936 async fn exclude_expression_filters_by_operation_type() -> Result<(), Box<dyn std::error::Error>>
937 {
938 let token = "Token";
939 let mut server = mockito::Server::new_async().await;
940 let server_url = server.url();
941
942 let mock = server
943 .mock("POST", "/200")
944 .expect(0)
945 .with_status(200)
946 .create_async()
947 .await;
948
949 {
950 let usage_agent = UsageAgent::builder()
951 .token(token.into())
952 .endpoint(format!("{}/200", server_url))
953 .buffer_size(1)
954 .exclude_expression(r#".request.operation.type == "subscription""#.to_string())
955 .build()?;
956
957 let report = make_simple_report(Some("OnMessage"), OperationType::Subscription);
958 let request = make_simple_request();
959 usage_agent
960 .add_report_with_request(report, Some((&request).into()))
961 .await?;
962 }
963
964 mock.assert_async().await;
965 Ok(())
966 }
967
968 #[tokio::test(flavor = "multi_thread")]
969 async fn exclude_expression_filters_by_header() -> Result<(), Box<dyn std::error::Error>> {
970 let token = "Token";
971 let mut server = mockito::Server::new_async().await;
972 let server_url = server.url();
973
974 let mock = server
975 .mock("POST", "/200")
976 .expect(0)
977 .with_status(200)
978 .create_async()
979 .await;
980
981 {
982 let usage_agent = UsageAgent::builder()
983 .token(token.into())
984 .endpoint(format!("{}/200", server_url))
985 .buffer_size(1)
986 .exclude_expression(r#".request.headers."x-internal" == "true""#.to_string())
987 .build()?;
988
989 let request = http::Request::builder()
990 .method(Method::POST)
991 .uri("http://localhost/graphql")
992 .header("x-internal", "true")
993 .body(())
994 .unwrap();
995
996 let report = make_test_report(Some("Q"), OperationType::Query, "query { hello }");
997 usage_agent
998 .add_report_with_request(report, Some((&request).into()))
999 .await?;
1000 }
1001
1002 mock.assert_async().await;
1003 Ok(())
1004 }
1005
1006 #[tokio::test(flavor = "multi_thread")]
1007 async fn exclude_expression_complex_conditional() -> Result<(), Box<dyn std::error::Error>> {
1008 let token = "Token";
1009 let mut server = mockito::Server::new_async().await;
1010 let server_url = server.url();
1011
1012 let exclude_expr = r#"
1014 if (.request.operation.name == "IntrospectionQuery") {
1015 true
1016 } else if (.request.operation.type == "mutation") {
1017 true
1018 } else {
1019 false
1020 }
1021 "#;
1022
1023 let mock = server
1024 .mock("POST", "/200")
1025 .expect(0)
1026 .with_status(200)
1027 .create_async()
1028 .await;
1029
1030 {
1031 let usage_agent = UsageAgent::builder()
1032 .token(token.into())
1033 .endpoint(format!("{}/200", server_url))
1034 .buffer_size(1)
1035 .exclude_expression(exclude_expr.to_string())
1036 .build()?;
1037
1038 let request = make_simple_request();
1039
1040 let report = make_simple_report(Some("IntrospectionQuery"), OperationType::Query);
1042 usage_agent
1043 .add_report_with_request(report, Some((&request).into()))
1044 .await?;
1045
1046 let report = make_simple_report(Some("CreateUser"), OperationType::Mutation);
1048 usage_agent
1049 .add_report_with_request(report, Some((&request).into()))
1050 .await?;
1051 }
1052
1053 mock.assert_async().await;
1054 Ok(())
1055 }
1056
1057 #[tokio::test(flavor = "multi_thread")]
1058 async fn exclude_expression_allows_through_complex_conditional(
1059 ) -> Result<(), Box<dyn std::error::Error>> {
1060 let token = "Token";
1061 let mut server = mockito::Server::new_async().await;
1062 let server_url = server.url();
1063
1064 let exclude_expr = r#"
1065 if (.request.operation.name == "IntrospectionQuery") {
1066 true
1067 } else if (.request.operation.type == "mutation") {
1068 true
1069 } else {
1070 false
1071 }
1072 "#;
1073
1074 let mock = server
1076 .mock("POST", "/200")
1077 .expect(1)
1078 .with_status(200)
1079 .create_async()
1080 .await;
1081
1082 {
1083 let usage_agent = UsageAgent::builder()
1084 .token(token.into())
1085 .endpoint(format!("{}/200", server_url))
1086 .exclude_expression(exclude_expr.to_string())
1087 .build()?;
1088
1089 let request = make_simple_request();
1090
1091 let report = make_simple_report(Some("GetUsers"), OperationType::Query);
1092 usage_agent
1093 .add_report_with_request(report, Some((&request).into()))
1094 .await?;
1095 }
1096
1097 mock.assert_async().await;
1098 Ok(())
1099 }
1100
1101 #[tokio::test(flavor = "multi_thread")]
1102 async fn exclude_expression_filters_by_url_path() -> Result<(), Box<dyn std::error::Error>> {
1103 let token = "Token";
1104 let mut server = mockito::Server::new_async().await;
1105 let server_url = server.url();
1106
1107 let mock = server
1108 .mock("POST", "/200")
1109 .expect(0)
1110 .with_status(200)
1111 .create_async()
1112 .await;
1113
1114 {
1115 let usage_agent = UsageAgent::builder()
1116 .token(token.into())
1117 .endpoint(format!("{}/200", server_url))
1118 .buffer_size(1)
1119 .exclude_expression(r#".request.url.path == "/internal/graphql""#.to_string())
1120 .build()?;
1121
1122 let request = http::Request::builder()
1123 .method(Method::POST)
1124 .uri("http://localhost/internal/graphql")
1125 .body(())
1126 .unwrap();
1127
1128 let report = make_test_report(Some("Q"), OperationType::Query, "query { hello }");
1129 usage_agent
1130 .add_report_with_request(report, Some((&request).into()))
1131 .await?;
1132 }
1133
1134 mock.assert_async().await;
1135 Ok(())
1136 }
1137
1138 #[tokio::test(flavor = "multi_thread")]
1139 async fn no_exclude_expression_sends_all_reports() -> Result<(), Box<dyn std::error::Error>> {
1140 let token = "Token";
1141 let mut server = mockito::Server::new_async().await;
1142 let server_url = server.url();
1143
1144 let mock = server
1145 .mock("POST", "/200")
1146 .expect(1)
1147 .with_status(200)
1148 .create_async()
1149 .await;
1150
1151 {
1152 let usage_agent = UsageAgent::builder()
1153 .token(token.into())
1154 .endpoint(format!("{}/200", server_url))
1155 .build()?;
1156
1157 let report = make_simple_report(Some("AnyOp"), OperationType::Query);
1158 let request = make_simple_request();
1159 usage_agent
1160 .add_report_with_request(report, Some((&request).into()))
1161 .await?;
1162 }
1163
1164 mock.assert_async().await;
1165 Ok(())
1166 }
1167
1168 #[test]
1169 fn builder_rejects_invalid_exclude_expression() {
1170 let result = UsageAgent::builder()
1171 .token("Token".into())
1172 .exclude_expression("this is not valid VRL }{".to_string())
1173 .build();
1174
1175 assert!(result.is_err());
1176 }
1177
1178 #[tokio::test(flavor = "multi_thread")]
1179 async fn builder_ignores_empty_exclude_expression() {
1180 let result = UsageAgent::builder()
1181 .token("Token".into())
1182 .exclude_expression("".to_string())
1183 .build();
1184
1185 assert!(result.is_ok());
1186 }
1187}