Skip to main content

tibba_middleware/
tracker.rs

1// Copyright 2026 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::LOG_TARGET;
16use axum::extract::Request;
17use axum::extract::State;
18use axum::middleware::Next;
19use axum::response::Response;
20use tibba_error::Error;
21use tibba_state::CTX;
22use tracing::{error, info};
23
24type Result<T> = std::result::Result<T, Error>;
25
26#[derive(Clone, Copy)]
27pub struct TrackerParams {
28    pub name: &'static str,
29    pub step: &'static str,
30}
31
32impl From<(&'static str, &'static str)> for TrackerParams {
33    fn from((name, step): (&'static str, &'static str)) -> Self {
34        Self { name, step }
35    }
36}
37
38/// Middleware that records user behavior events for audit and analytics.
39///
40/// Each event captures:
41/// - Identity context: device_id, trace_id, account
42/// - Business context: operation name and step label
43/// - Outcome: HTTP status, success/failure result, elapsed time
44/// - Failure detail: error message, category, sub-category, and whether it
45///   was an infrastructure exception (vs. a normal business error)
46pub async fn user_tracker(
47    State(params): State<TrackerParams>,
48    req: Request,
49    next: Next,
50) -> Result<Response> {
51    let res = next.run(req).await;
52
53    let ctx = CTX.get();
54    // Milliseconds elapsed since the request entered the middleware stack
55    let elapsed = ctx.elapsed_ms();
56    let device_id = &ctx.device_id;
57    let trace_id = &ctx.trace_id;
58    // Authenticated account name; empty string when the user is not logged in
59    let account = ctx.get_account();
60    // HTTP status code — useful for correlating with access logs
61    let status = res.status().as_u16();
62
63    if status < 400 {
64        info!(
65            target: LOG_TARGET,
66            device_id,
67            trace_id,
68            name = params.name,   // Logical operation name (e.g. "user_login")
69            account = %account,
70            step = params.step,   // Fine-grained step within the operation
71            status,
72            elapsed,
73            result = "success",
74            "user tracker",
75        );
76        return Ok(res);
77    }
78
79    // Extract structured error details from the response extensions.
80    // If no Error is attached (e.g. the handler panicked), treat as an
81    // infrastructure exception so on-call alerts fire correctly.
82    let (error, error_category, error_sub_category, error_exception) = res
83        .extensions()
84        .get::<Error>()
85        .map(|err| {
86            (
87                Some(err.message.clone()),
88                Some(err.category.clone()),
89                err.sub_category.clone(),
90                // true when the error originated from infrastructure
91                // (network timeout, downstream failure, etc.)
92                err.exception.unwrap_or_default(),
93            )
94        })
95        .unwrap_or((None, None, None, true));
96
97    error!(
98        target: LOG_TARGET,
99        device_id,
100        trace_id,
101        name = params.name,
102        account = %account,
103        step = params.step,
104        status,
105        error,
106        error_category,
107        error_sub_category,
108        // Distinguishes infrastructure exceptions from normal business errors
109        error_exception,
110        elapsed,
111        result = "failure",
112        "user tracker",
113    );
114    Ok(res)
115}