{"record":{"id":"23a5bf9a3882af6c","repo":"Hmbown/CodeWhale","slug":"invalid-duration-component-num-buf","errorCode":null,"errorMessage":"invalid duration component: {num_buf:?}","messagePattern":"invalid duration component: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/cli/src/metrics.rs","lineNumber":80,"sourceCode":"pub fn parse_since(s: &str) -> Result<DateTime<Utc>> {\n    let s = s.trim().to_ascii_lowercase();\n    let s = s.strip_prefix(\"now-\").unwrap_or(&s);\n    let secs = parse_duration_secs(s)?;\n    Ok(Utc::now() - Duration::seconds(secs))\n}\n\nfn parse_duration_secs(s: &str) -> Result<i64> {\n    // Walk through the string accumulating numbers and consuming unit suffixes.\n    let mut total: i64 = 0;\n    let mut num_buf = String::new();\n\n    for ch in s.chars() {\n        match ch {\n            '0'..='9' => num_buf.push(ch),\n            'd' | 'h' | 'm' | 's' => {\n                let n: i64 = num_buf\n                    .parse()\n                    .map_err(|_| anyhow::anyhow!(\"invalid duration component: {num_buf:?}\"))?;\n                num_buf.clear();\n                let factor = match ch {\n                    'd' => 86_400,\n                    'h' => 3_600,\n                    'm' => 60,\n                    's' => 1,\n                    _ => unreachable!(),\n                };\n                total += n * factor;\n            }\n            _ => anyhow::bail!(\"unrecognised character {ch:?} in duration {s:?}\"),\n        }\n    }\n\n    if !num_buf.is_empty() {\n        // Trailing bare number — treat as seconds.\n        let n: i64 = num_buf.parse()?;\n        total += n;","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/cli/src/metrics.rs#L62-L98","documentation":"parse_duration_secs walks the string as runs of digits terminated by unit letters d/h/m/s. When a unit letter arrives but the digit buffer is empty (or the accumulated number fails i64 parsing, i.e. overflow), this error names the offending buffer. Notably there is no millisecond unit: \"100ms\" fails here because the trailing s follows an empty buffer.","triggerScenarios":"Passing \"100ms\" or \"1500ms\" to a --since-style flag; a bare unit like \"d\" or \"h\" with no number; a huge component like \"99999999999999999999s\" overflowing i64.","commonSituations":"Assuming humantime/ms support; copy-pasting latency-style durations into a retention window flag; scripting with empty variables producing a lone unit.","solutions":["Express the duration with supported units only: d/h/m/s (e.g. \"100ms\" -> \"0s\" is invalid, use \"1s\" or drop it)","Ensure every unit letter is preceded by digits — no stray or doubled units","Keep each component under i64 range; split into multiple components instead","Use the accepted \"now-2h\" prefix form where the flag documents it"],"exampleFix":"# before\n codewhale metrics --since 100ms   # error: invalid duration component: \"\"\n\n# after\n codewhale metrics --since 1s","handlingStrategy":"validation","validationCode":"fn valid_duration(s: &str) -> bool {\n    let s = s.trim().to_ascii_lowercase();\n    let s = s.strip_prefix(\"now-\").unwrap_or(&s);\n    !s.is_empty() && s.chars().all(|c| c.is_ascii_digit() || matches!(c, 'd' | 'h' | 'm' | 's')) && !s.ends_with(|c: char| matches!(c, 'd' | 'h' | 'm' | 's')) == false\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Remember there is no ms unit — convert milliseconds to seconds before passing","Always precede a unit letter with digits; reject strings like \"ms\" or lone \"h\" in wrappers","Prefer simple single-unit forms (\"7d\", \"24h\", \"90m\") in scripts"],"tags":["cli","metrics","duration","parsing","rust"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}