1use serde::{Deserialize, Serialize};
2
3use crate::logging::RouteAttemptLog;
4use crate::policy_actions::{
5 PolicyAction, PolicyActionKind, PolicyActionOwner, PolicyActionRecoveryState,
6};
7use crate::pricing::CostBreakdown;
8use crate::provider_signals::{
9 ProviderSignal, ProviderSignalConfidence, ProviderSignalKind, ProviderSignalSource,
10 ProviderSignalTarget,
11};
12use crate::state::{FinishedRequest, RequestObservability, SessionIdentitySource};
13use crate::usage::UsageMetrics;
14
15pub const REQUEST_CHAIN_EXPORT_API_VERSION: u32 = 1;
16pub const REQUEST_CHAIN_EXPORT_DEFAULT_LIMIT: usize = 20;
17pub const REQUEST_CHAIN_EXPORT_MAX_LIMIT: usize = 100;
18pub const REQUEST_CHAIN_ATTEMPT_MAX: usize = 16;
19pub const REQUEST_CHAIN_SIGNALS_MAX: usize = 16;
20pub const REQUEST_CHAIN_ACTIONS_MAX: usize = 16;
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
23pub struct RequestChainSelector {
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub trace_id: Option<String>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub request_id: Option<u64>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub session_id: Option<String>,
30}
31
32impl RequestChainSelector {
33 pub fn has_identity(&self) -> bool {
34 self.trace_id
35 .as_deref()
36 .is_some_and(|value| !value.trim().is_empty())
37 || self.request_id.is_some()
38 || self
39 .session_id
40 .as_deref()
41 .is_some_and(|value| !value.trim().is_empty())
42 }
43
44 pub fn normalized(mut self) -> Self {
45 self.trace_id = clean_identity(self.trace_id);
46 self.session_id = clean_identity(self.session_id);
47 self
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
52pub struct RequestChainExport {
53 pub api_version: u32,
54 pub selector: RequestChainSelector,
55 pub limit: usize,
56 pub truncated: bool,
57 pub requests: Vec<RequestChainRequest>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct RequestChainRequest {
62 pub request_id: u64,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub trace_id: Option<String>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub session_id: Option<String>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub session_identity_source: Option<SessionIdentitySource>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub client_name: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub model: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub reasoning_effort: Option<String>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub service_tier: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub station_name: Option<String>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub provider_id: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub usage: Option<UsageMetrics>,
83 #[serde(default, skip_serializing_if = "CostBreakdown::is_unknown")]
84 pub cost: CostBreakdown,
85 pub observability: RequestObservability,
86 pub service: String,
87 pub method: String,
88 pub path: String,
89 pub status_code: u16,
90 pub duration_ms: u64,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub ttfb_ms: Option<u64>,
93 pub streaming: bool,
94 pub ended_at_ms: u64,
95 pub attempts_truncated: bool,
96 pub provider_signals_truncated: bool,
97 pub policy_actions_truncated: bool,
98 pub route_attempts: Vec<RequestChainRouteAttempt>,
99 pub provider_signals: Vec<RequestChainProviderSignal>,
100 pub policy_actions: Vec<RequestChainPolicyAction>,
101 pub timeline: Vec<RequestChainTimelineEvent>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
105pub struct RequestChainRouteAttempt {
106 pub attempt_index: u32,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub provider_id: Option<String>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub endpoint_id: Option<String>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub provider_endpoint_key: Option<String>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub station_name: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub preference_group: Option<u32>,
117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
118 pub route_path: Vec<String>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub provider_attempt: Option<u32>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub upstream_attempt: Option<u32>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub provider_max_attempts: Option<u32>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub upstream_max_attempts: Option<u32>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub avoided_total: Option<usize>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub total_upstreams: Option<usize>,
131 pub decision: String,
132 pub code: String,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub status_code: Option<u16>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub error_class: Option<String>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub model: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub upstream_headers_ms: Option<u64>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub duration_ms: Option<u64>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub cooldown_secs: Option<u64>,
145 #[serde(default, skip_serializing_if = "bool_is_false")]
146 pub skipped: bool,
147 pub provider_signals: Vec<RequestChainProviderSignal>,
148 pub policy_actions: Vec<RequestChainPolicyAction>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
152pub struct RequestChainProviderSignal {
153 pub kind: ProviderSignalKind,
154 pub code: String,
155 pub source: ProviderSignalSource,
156 pub target: ProviderSignalTarget,
157 pub confidence: ProviderSignalConfidence,
158 pub observed_at_ms: u64,
159 #[serde(default, skip_serializing_if = "bool_is_false")]
160 pub route_facing: bool,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub retry_after_secs: Option<u64>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub reset_after_secs: Option<u64>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub error_class: Option<String>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub trace_id: Option<String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
172pub struct RequestChainPolicyAction {
173 pub id: String,
174 pub kind: PolicyActionKind,
175 pub code: String,
176 pub owner: PolicyActionOwner,
177 pub provider_endpoint_key: String,
178 pub source_signal: RequestChainProviderSignal,
179 pub confidence: ProviderSignalConfidence,
180 pub created_at_ms: u64,
181 pub expires_at_ms: u64,
182 pub recovery_state: PolicyActionRecoveryState,
183 pub generation: u64,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
187pub struct RequestChainTimelineEvent {
188 pub order: u32,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub at_ms: Option<u64>,
191 pub kind: String,
192 pub code: String,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub attempt_index: Option<u32>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub provider_id: Option<String>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub endpoint_id: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub provider_endpoint_key: Option<String>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub status_code: Option<u16>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub model: Option<String>,
205}
206
207impl RequestChainExport {
208 pub fn from_finished_requests(
209 selector: RequestChainSelector,
210 limit: usize,
211 truncated: bool,
212 mut requests: Vec<FinishedRequest>,
213 ) -> Self {
214 requests.sort_by_key(|request| (request.ended_at_ms, request.id));
215 Self {
216 api_version: REQUEST_CHAIN_EXPORT_API_VERSION,
217 selector: selector.normalized(),
218 limit: limit.clamp(1, REQUEST_CHAIN_EXPORT_MAX_LIMIT),
219 truncated,
220 requests: requests
221 .iter()
222 .map(RequestChainRequest::from_finished_request)
223 .collect(),
224 }
225 }
226}
227
228impl RequestChainRequest {
229 pub fn from_finished_request(request: &FinishedRequest) -> Self {
230 let route_attempts = request
231 .retry
232 .as_ref()
233 .map(|retry| retry.route_attempts_or_derived())
234 .unwrap_or_default();
235 let attempts_truncated = route_attempts.len() > REQUEST_CHAIN_ATTEMPT_MAX;
236 let route_attempts = route_attempts
237 .iter()
238 .take(REQUEST_CHAIN_ATTEMPT_MAX)
239 .map(RequestChainRouteAttempt::from_attempt)
240 .collect::<Vec<_>>();
241
242 let provider_signals_truncated = request.provider_signals.len() > REQUEST_CHAIN_SIGNALS_MAX;
243 let provider_signals = request
244 .provider_signals
245 .iter()
246 .take(REQUEST_CHAIN_SIGNALS_MAX)
247 .map(RequestChainProviderSignal::from_signal)
248 .collect::<Vec<_>>();
249
250 let policy_actions_truncated = request.policy_actions.len() > REQUEST_CHAIN_ACTIONS_MAX;
251 let policy_actions = request
252 .policy_actions
253 .iter()
254 .take(REQUEST_CHAIN_ACTIONS_MAX)
255 .map(RequestChainPolicyAction::from_action)
256 .collect::<Vec<_>>();
257
258 let timeline =
259 request_chain_timeline(request, &route_attempts, &provider_signals, &policy_actions);
260
261 Self {
262 request_id: request.id,
263 trace_id: request.trace_id.clone(),
264 session_id: request.session_id.clone(),
265 session_identity_source: request.session_identity_source,
266 client_name: request.client_name.clone(),
267 model: request.model.clone(),
268 reasoning_effort: request.reasoning_effort.clone(),
269 service_tier: request.service_tier.clone(),
270 station_name: request.station_name.clone(),
271 provider_id: request.provider_id.clone(),
272 usage: request.usage.clone(),
273 cost: request.cost.clone(),
274 observability: request.observability_view(),
275 service: request.service.clone(),
276 method: request.method.clone(),
277 path: request.path.clone(),
278 status_code: request.status_code,
279 duration_ms: request.duration_ms,
280 ttfb_ms: request.ttfb_ms,
281 streaming: request.streaming,
282 ended_at_ms: request.ended_at_ms,
283 attempts_truncated,
284 provider_signals_truncated,
285 policy_actions_truncated,
286 route_attempts,
287 provider_signals,
288 policy_actions,
289 timeline,
290 }
291 }
292}
293
294impl RequestChainRouteAttempt {
295 pub fn from_attempt(attempt: &RouteAttemptLog) -> Self {
296 let provider_signals = attempt
297 .provider_signals
298 .iter()
299 .take(REQUEST_CHAIN_SIGNALS_MAX)
300 .map(RequestChainProviderSignal::from_signal)
301 .collect();
302 let policy_actions = attempt
303 .policy_actions
304 .iter()
305 .take(REQUEST_CHAIN_ACTIONS_MAX)
306 .map(RequestChainPolicyAction::from_action)
307 .collect();
308 Self {
309 attempt_index: attempt.attempt_index,
310 provider_id: attempt.provider_id.clone(),
311 endpoint_id: attempt.endpoint_id.clone(),
312 provider_endpoint_key: attempt.provider_endpoint_key.clone(),
313 station_name: attempt.station_name.clone(),
314 preference_group: attempt.preference_group,
315 route_path: attempt.route_path.clone(),
316 provider_attempt: attempt.provider_attempt,
317 upstream_attempt: attempt.upstream_attempt,
318 provider_max_attempts: attempt.provider_max_attempts,
319 upstream_max_attempts: attempt.upstream_max_attempts,
320 avoided_total: attempt.avoided_total,
321 total_upstreams: attempt.total_upstreams,
322 decision: attempt.decision.clone(),
323 code: attempt.stable_code().to_string(),
324 status_code: attempt.status_code,
325 error_class: attempt.error_class.clone(),
326 model: attempt.model.clone(),
327 upstream_headers_ms: attempt.upstream_headers_ms,
328 duration_ms: attempt.duration_ms,
329 cooldown_secs: attempt.cooldown_secs,
330 skipped: attempt.skipped,
331 provider_signals,
332 policy_actions,
333 }
334 }
335}
336
337impl RequestChainProviderSignal {
338 pub fn from_signal(signal: &ProviderSignal) -> Self {
339 Self {
340 kind: signal.kind.clone(),
341 code: signal.stable_code().to_string(),
342 source: signal.source.clone(),
343 target: signal.target.clone(),
344 confidence: signal.confidence,
345 observed_at_ms: signal.observed_at_ms,
346 route_facing: signal.route_facing,
347 retry_after_secs: signal.retry_after_secs,
348 reset_after_secs: signal.reset_after_secs,
349 error_class: signal.error_class.clone(),
350 trace_id: signal.trace.trace_id.clone(),
351 }
352 }
353}
354
355impl RequestChainPolicyAction {
356 pub fn from_action(action: &PolicyAction) -> Self {
357 Self {
358 id: action.id.clone(),
359 kind: action.kind.clone(),
360 code: action.stable_code().to_string(),
361 owner: action.owner.clone(),
362 provider_endpoint_key: action.provider_endpoint_key.stable_key(),
363 source_signal: RequestChainProviderSignal::from_signal(&action.source_signal),
364 confidence: action.confidence,
365 created_at_ms: action.created_at_ms,
366 expires_at_ms: action.expires_at_ms,
367 recovery_state: action.recovery_state.clone(),
368 generation: action.generation,
369 }
370 }
371}
372
373fn request_chain_timeline(
374 request: &FinishedRequest,
375 route_attempts: &[RequestChainRouteAttempt],
376 provider_signals: &[RequestChainProviderSignal],
377 policy_actions: &[RequestChainPolicyAction],
378) -> Vec<RequestChainTimelineEvent> {
379 let mut events = vec![RequestChainTimelineEvent {
380 order: 0,
381 at_ms: Some(request.ended_at_ms),
382 kind: "request".to_string(),
383 code: if request.status_code >= 400 {
384 "request_failed".to_string()
385 } else {
386 "request_completed".to_string()
387 },
388 attempt_index: None,
389 provider_id: request.provider_id.clone(),
390 endpoint_id: None,
391 provider_endpoint_key: None,
392 status_code: Some(request.status_code),
393 model: request.model.clone(),
394 }];
395
396 events.extend(
397 route_attempts
398 .iter()
399 .map(|attempt| RequestChainTimelineEvent {
400 order: 1_000 + attempt.attempt_index,
401 at_ms: None,
402 kind: "route_attempt".to_string(),
403 code: attempt.code.clone(),
404 attempt_index: Some(attempt.attempt_index),
405 provider_id: attempt.provider_id.clone(),
406 endpoint_id: attempt.endpoint_id.clone(),
407 provider_endpoint_key: attempt.provider_endpoint_key.clone(),
408 status_code: attempt.status_code,
409 model: attempt.model.clone(),
410 }),
411 );
412
413 events.extend(provider_signals.iter().enumerate().map(|(index, signal)| {
414 let (provider_id, provider_endpoint_key) = signal_target_identity(&signal.target);
415 RequestChainTimelineEvent {
416 order: 2_000 + u32::try_from(index).unwrap_or(u32::MAX),
417 at_ms: Some(signal.observed_at_ms),
418 kind: "provider_signal".to_string(),
419 code: signal.code.clone(),
420 attempt_index: None,
421 provider_id,
422 endpoint_id: None,
423 provider_endpoint_key,
424 status_code: None,
425 model: None,
426 }
427 }));
428
429 events.extend(policy_actions.iter().enumerate().map(|(index, action)| {
430 RequestChainTimelineEvent {
431 order: 3_000 + u32::try_from(index).unwrap_or(u32::MAX),
432 at_ms: Some(action.created_at_ms),
433 kind: "policy_action".to_string(),
434 code: action.code.clone(),
435 attempt_index: None,
436 provider_id: Some(action.source_signal_target_provider_id()),
437 endpoint_id: None,
438 provider_endpoint_key: Some(action.provider_endpoint_key.clone()),
439 status_code: None,
440 model: None,
441 }
442 }));
443
444 events.sort_by_key(|event| (event.at_ms.unwrap_or(request.ended_at_ms), event.order));
445 events
446}
447
448impl RequestChainPolicyAction {
449 fn source_signal_target_provider_id(&self) -> String {
450 match &self.source_signal.target {
451 ProviderSignalTarget::ProviderEndpoint {
452 provider_endpoint_key,
453 } => provider_endpoint_key.provider_id.clone(),
454 ProviderSignalTarget::Provider { provider_id, .. } => provider_id.clone(),
455 ProviderSignalTarget::Service { service } => service.clone(),
456 }
457 }
458}
459
460fn signal_target_identity(target: &ProviderSignalTarget) -> (Option<String>, Option<String>) {
461 match target {
462 ProviderSignalTarget::ProviderEndpoint {
463 provider_endpoint_key,
464 } => (
465 Some(provider_endpoint_key.provider_id.clone()),
466 Some(provider_endpoint_key.stable_key()),
467 ),
468 ProviderSignalTarget::Provider { provider_id, .. } => (Some(provider_id.clone()), None),
469 ProviderSignalTarget::Service { .. } => (None, None),
470 }
471}
472
473fn clean_identity(value: Option<String>) -> Option<String> {
474 value
475 .map(|value| value.trim().to_string())
476 .filter(|value| !value.is_empty())
477}
478
479fn bool_is_false(value: &bool) -> bool {
480 !*value
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use crate::logging::{RetryInfo, RouteAttemptLog};
487 use crate::policy_actions::PolicyActionRecoveryState;
488 use crate::provider_signals::{
489 ProviderSignalSource, ProviderSignalTarget, ProviderSignalTrace,
490 };
491 use crate::runtime_identity::ProviderEndpointKey;
492
493 fn endpoint_key() -> ProviderEndpointKey {
494 ProviderEndpointKey::new("codex", "relay", "default")
495 }
496
497 fn signal(observed_at_ms: u64) -> ProviderSignal {
498 ProviderSignal {
499 kind: ProviderSignalKind::RateLimit,
500 code: Some("rate_limit".to_string()),
501 source: ProviderSignalSource::UpstreamResponse,
502 target: ProviderSignalTarget::ProviderEndpoint {
503 provider_endpoint_key: endpoint_key(),
504 },
505 confidence: ProviderSignalConfidence::High,
506 observed_at_ms,
507 route_facing: true,
508 retry_after_secs: Some(30),
509 reset_after_secs: None,
510 reason: Some("raw upstream says secret-token".to_string()),
511 error_class: Some("http_429".to_string()),
512 trace: ProviderSignalTrace {
513 trace_id: Some("trace-a".to_string()),
514 cf_ray: Some("cf-ray-secret".to_string()),
515 upstream_request_id: Some("upstream-secret".to_string()),
516 },
517 }
518 }
519
520 fn action(source_signal: ProviderSignal) -> PolicyAction {
521 PolicyAction {
522 id: "codex-helper:relay:100".to_string(),
523 kind: PolicyActionKind::Cooldown,
524 code: Some("cooldown".to_string()),
525 owner: PolicyActionOwner::CodexHelper,
526 provider_endpoint_key: endpoint_key(),
527 source_signal,
528 reason: "raw policy reason secret-token".to_string(),
529 confidence: ProviderSignalConfidence::High,
530 created_at_ms: 120,
531 expires_at_ms: 30_120,
532 recovery_state: PolicyActionRecoveryState::Active,
533 generation: 7,
534 }
535 }
536
537 fn finished_request() -> FinishedRequest {
538 let provider_signal = signal(110);
539 let policy_action = action(provider_signal.clone());
540 let mut request = FinishedRequest {
541 id: 42,
542 trace_id: Some("trace-a".to_string()),
543 session_id: Some("session-a".to_string()),
544 session_identity_source: None,
545 client_name: Some("codex".to_string()),
546 client_addr: Some("127.0.0.1:5555".to_string()),
547 cwd: Some("C:/Users/Frankorz/private-project".to_string()),
548 model: Some("gpt-5.6".to_string()),
549 reasoning_effort: Some("high".to_string()),
550 service_tier: Some("priority".to_string()),
551 station_name: Some("primary".to_string()),
552 provider_id: Some("relay".to_string()),
553 upstream_base_url: Some("https://relay.example/v1?token=secret".to_string()),
554 route_decision: None,
555 usage: Some(UsageMetrics {
556 input_tokens: 10,
557 output_tokens: 20,
558 total_tokens: 30,
559 ..UsageMetrics::default()
560 }),
561 cost: CostBreakdown::unknown(),
562 retry: Some(RetryInfo {
563 attempts: 1,
564 upstream_chain: vec!["raw upstream chain secret-token".to_string()],
565 route_attempts: vec![RouteAttemptLog {
566 attempt_index: 0,
567 provider_id: Some("relay".to_string()),
568 endpoint_id: Some("default".to_string()),
569 provider_endpoint_key: Some(endpoint_key().stable_key()),
570 decision: "failed_status".to_string(),
571 code: Some("failed_status".to_string()),
572 status_code: Some(429),
573 error_class: Some("http_429".to_string()),
574 model: Some("gpt-5.6".to_string()),
575 raw: "raw route attempt secret-token".to_string(),
576 provider_signals: vec![provider_signal.clone()],
577 policy_actions: vec![policy_action.clone()],
578 ..RouteAttemptLog::default()
579 }],
580 }),
581 provider_signals: vec![provider_signal],
582 policy_actions: vec![policy_action],
583 observability: RequestObservability::default(),
584 service: "codex".to_string(),
585 method: "POST".to_string(),
586 path: "/v1/responses".to_string(),
587 status_code: 429,
588 duration_ms: 1000,
589 ttfb_ms: Some(250),
590 streaming: true,
591 ended_at_ms: 200,
592 };
593 request.refresh_observability();
594 request
595 }
596
597 #[test]
598 fn request_chain_export_sanitizes_sensitive_finished_request_fields() {
599 let export = RequestChainExport::from_finished_requests(
600 RequestChainSelector {
601 trace_id: Some(" trace-a ".to_string()),
602 request_id: None,
603 session_id: None,
604 },
605 20,
606 false,
607 vec![finished_request()],
608 );
609 let text = serde_json::to_string(&export).expect("serialize request chain export");
610
611 assert!(text.contains("\"trace_id\":\"trace-a\""));
612 assert!(text.contains("\"route_attempts\""));
613 assert!(text.contains("\"code\":\"failed_status\""));
614 assert!(!text.contains("client_addr"));
615 assert!(!text.contains("cwd"));
616 assert!(!text.contains("upstream_base_url"));
617 assert!(!text.contains("raw"));
618 assert!(!text.contains("secret-token"));
619 assert!(!text.contains("cf-ray-secret"));
620 assert!(!text.contains("upstream-secret"));
621 }
622
623 #[test]
624 fn request_chain_export_orders_session_requests_by_completion_time() {
625 let mut newer = finished_request();
626 newer.id = 2;
627 newer.ended_at_ms = 200;
628 let mut older = finished_request();
629 older.id = 1;
630 older.ended_at_ms = 100;
631
632 let export = RequestChainExport::from_finished_requests(
633 RequestChainSelector {
634 session_id: Some("session-a".to_string()),
635 ..RequestChainSelector::default()
636 },
637 20,
638 false,
639 vec![newer, older],
640 );
641
642 assert_eq!(
643 export
644 .requests
645 .iter()
646 .map(|request| request.request_id)
647 .collect::<Vec<_>>(),
648 vec![1, 2]
649 );
650 }
651}