Skip to main content

codex_helper_core/
request_chain.rs

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}