{"record":{"id":"aabfa526b9b0d580","repo":"JuliusBrussee/caveman","slug":"invalid-since-q-want-rfc3339-w","errorCode":null,"errorMessage":"invalid --since %q (want RFC3339): %w","messagePattern":"invalid --since %q \\(want RFC3339\\): %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"proxy/internal/store/store.go","lineNumber":741,"sourceCode":"\tout.Basis = \"inferred\"\n\treturn out, nil\n}\n\n// ObserveSummarySince returns the compact observe-estimate object, optionally\n// filtered to rows at or after an RFC3339 instant (the wrap session start). It\n// mixes the observe-only would-have-saved numbers with the real compression cut so\n// one call serves both the \"would have cut\" (observe) and \"cut\" (compress) session\n// lines. Everything it reports is `inferred`; savings_usd is whatever was truly\n// booked (0 in observe mode), and would_save_usd stays nil unless a priced row\n// produced one.\nfunc (s *Store) ObserveSummarySince(since string) (ObserveSummary, error) {\n\tout := ObserveSummary{Basis: \"inferred\", TokenAccounting: map[string]int64{}}\n\twhere := \"\"\n\tvar args []any\n\tif trimmed := strings.TrimSpace(since); trimmed != \"\" {\n\t\tt, err := time.Parse(time.RFC3339, trimmed)\n\t\tif err != nil {\n\t\t\treturn out, fmt.Errorf(\"invalid --since %q (want RFC3339): %w\", trimmed, err)\n\t\t}\n\t\twhere = \" WHERE ts >= ?\"\n\t\targs = append(args, t.UTC().Format(storeTSLayout))\n\t}\n\tvar usdCount int64\n\tvar usdSum float64\n\trow := s.db.QueryRow(`SELECT COUNT(*), COALESCE(SUM(input_tokens),0), COALESCE(SUM(would_save_tokens),0),\n\t\tCOALESCE(SUM(compression_tokens_before),0), COALESCE(SUM(compression_tokens_after),0),\n\t\tCOALESCE(SUM(compression_tokens_before - compression_tokens_after),0),\n\t\tCOALESCE(SUM(savings_usd),0), COUNT(would_save_usd), COALESCE(SUM(would_save_usd),0),\n\t\tCOALESCE(SUM(cached_input_tokens),0), COALESCE(SUM(cache_creation_input_tokens),0),\n\t\tCOALESCE(SUM(CASE WHEN cache_bust <> 0 THEN 1 ELSE 0 END),0),\n\t\tCOALESCE(SUM(CASE WHEN compression_eligible <> 0 THEN 1 ELSE 0 END),0)\n\t\tFROM requests`+where, args...)\n\tif err := row.Scan(&out.Spans, &out.TokensIn, &out.WouldSaveTokens,\n\t\t&out.CompressionTokensBefore, &out.CompressionTokensAfter, &out.CompressionTokensSaved,\n\t\t&out.SavingsUSD, &usdCount, &usdSum,\n\t\t&out.CachedInputTokens, &out.CacheCreationInputTokens,","sourceCodeStart":723,"sourceCodeEnd":759,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/proxy/internal/store/store.go#L723-L759","documentation":"Returned by Store.ObserveSummarySince when the optional --since value is non-empty but does not parse as RFC3339. The store timestamps rows in UTC using RFC3339-derived formatting, so the cutoff must be an exact RFC3339 timestamp like 2026-01-02T15:04:05Z or with an explicit offset (2026-01-02T10:04:05-05:00).","triggerScenarios":"Passing a date-only string ('2026-01-02'), a legacy timestamp format ('2026-01-02 15:04:05'), a missing timezone, or garbage into the --since flag consumed by ObserveSummarySince. Empty/whitespace strings are fine (no filter); anything else must be strict RFC3339.","commonSituations":"Users habitually typing 'YYYY-MM-DD' from other tools' date flags; passing Unix epoch seconds or ISO-with-space from scripts; a timezone-less local timestamp like '2026-01-02T15:04:05' (RFC3339 requires an offset); copy-pasting a timestamp that contains a trailing space or quote.","solutions":["Use a full RFC3339 timestamp with timezone: 2026-01-02T15:04:05Z or 2026-01-02T15:04:05+02:00","In scripts, generate it: date -u +%Y-%m-%dT%H:%M:%SZ","If you only want date granularity, expand it explicitly: 2026-01-02T00:00:00Z","Catch the error and show the offending value plus the expected format to the user"],"exampleFix":"# before\ncaveman-proxy stats --since 2026-01-02\ninvalid --since \"2026-01-02\" (want RFC3339)\n\n# after\ncaveman-proxy stats --since 2026-01-02T00:00:00Z\n# or generated: --since \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"","handlingStrategy":"validation","validationCode":"if since != \"\" {\n    if _, err := time.Parse(time.RFC3339, strings.TrimSpace(since)); err != nil {\n        return fmt.Errorf(\"--since must be RFC3339, e.g. %s\", time.Now().UTC().Add(-24*time.Hour).Format(time.RFC3339))\n    }\n}","typeGuard":"func isRFC3339(s string) bool {\n    s = strings.TrimSpace(s)\n    if s == \"\" { return true } // optional flag\n    _, err := time.Parse(time.RFC3339, s)\n    return err == nil\n}","tryCatchPattern":"sum, err := st.ObserveSummarySince(since)\nif err != nil && strings.Contains(err.Error(), \"invalid --since\") {\n    return fmt.Errorf(\"bad --since: pass an RFC3339 timestamp like 2026-01-02T00:00:00Z\")\n}","preventionTips":["Generate --since in scripts with date -u +%Y-%m-%dT%H:%M:%SZ","Remember RFC3339 requires a timezone (Z or ±hh:mm); date-only and space-separated forms fail","Date-granularity needs T00:00:00Z appended explicitly"],"tags":["validation","timestamp","cli","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}