refactor: CLI agent-mode hardening + cross-cutting LOC reduction#365
Conversation
Replaces 25+ inlined BWConfig + sessionAndProject + ask AuthContext sequences across Pages/* with a single mkPageCtx helper that returns (Session, Project, BWConfig) seeded with the always-set fields (sessM, currProject, config). Net: -102 LoC.
Collapses the recurrent 'addErrorToast msg Nothing >> addRespHeaders payload' shape across Pages/Projects.hs and Pages/Dashboards.hs. Net: -5 LoC plus +5 LoC for the helper and re-export = breakeven, but eliminates a duplicated two-line pattern at every site.
Collapses the duplicated outer flex-wrapper + optional widget header + flex-1 fill + surface-raised card shell shared between renderLogsWidget, renderTraceTable, and renderTable into a single helper.
Replaces manual ToJSON/FromJSON instances and the parseFieldTypes, parseFieldCategoryEnum, fieldTypeToText helper functions with a single deriving-via clause that piggybacks on the existing WrappedEnumSC wrapper. Net: -50 LoC.
Replace ad-hoc AE.Value field-name probing in the CLI with typed parsing against the server's existing response types so wire-shape changes surface as compile errors: - filterSchema now takes Models.Telemetry.Schema.Schema - normalizeList decodes via Web.ApiTypes.Paged - metrics renderers/assertion take Pages.Charts.Types.MetricsData Also: factor validateDurationOrDie/validateAndNormalizeKind helpers, fold three case-of-OutputMode triplets through renderWith, derive ServiceRow/ConfigGetJson/AuthStatusJson/TraceSummary JSON via CustomJSON.
Show the discover -> search -> context -> triage chain with the stable JSON envelope each command exposes, so a reader scanning the README sees why the CLI is agent-friendly before getting to the Claude Code Skills section.
Code ReviewOverviewLarge, well-motivated refactor that ships three distinct improvements together: (1) agentic-CLI hardening (stable JSON envelopes, Issues1. Breaking env-var rename with no migration path 2. Duration validation rejects the defaults it ships 3. 4. svcIdx = Map.lookup "service" idxMapIf 5. monoscope issues list --status open -o json | jq '.items[] | {id,title,service,severity}'This will break after the normalisation is shipped. 6. Double-parsing in Nits / minor suggestions
Positives
|
Drops 2 errors and 7 warnings from `hlint cli` to 0: - Resource.hs: `>> pure n` -> `$> n` for the apply-batch counter - Commands.hs: `ExceptT . pure` -> `hoistEither`; manual `maybe ... Right` -> `maybeToRight` - Config.hs: `either (const x) id` -> `fromRight x` - UI.hs: `T.unlines` -> Relude's `unlines` - Validate.hs: drop redundant `void` Plus higher-impact suggestions: - `data` -> `newtype` for ServicesCommand, ConfigGetOpts, ServicesListOpts, FormEditorName - `fmap (fmap f) x` -> `f <<$>> x` in 6 list/schema/facets handlers - `Set.singleton` -> Relude's `one` - `pure ()` -> `pass` in UI event handlers - `\_ -> action` -> `const action` - `(\c -> Map.lookup c idxMap)` -> `(`Map.lookup` idxMap)` Build green: `All good (8 modules)`. The two remaining hlint hints (`Data.Set as Set` alias, `universe` import) belong in `.hlint.yaml` rather than the code.
Code Review: Refactor/loc reduction cross cuttingGreat PR overall — significant boilerplate reduction, cleaner JSON derivations, better agentic ergonomics. A few issues worth addressing before merge. Potential bugs1. -- aggregateServices
svcIdx = Map.lookup "service" idxMap -- may not exist
tsIdx = Map.lookup "timestamp" idxMap
-- withTraceSummary
cell "service" r
cell "severity" rThe server's 2. Default duration strings bypass the lowercase-only validator
Breaking changes without migration path3. Existing CI scripts / docker-compose files using 4. List response envelope is a breaking change The docs note Performance concern5. let params = [("since", fromMaybe "24H" opts.since), ("limit", "10000")]Now that Fragility6. getField = toText . map toLower . drop 2 . showThis relies on every constructor being named Dead bindings7. Leftover
Minor nits
What's well done
|
IssueL is a list-view wrapper around Issue (`{ base :: Issue, ... }`),
so the `id` field is reached through `.base`. Four sites in
ErrorPatternsSpec called `issue.id` directly, which type-errored after
IssueL gained its current shape. Matches the pattern used in
src/Pages/Anomalies.hs (`errL.base.id`).
The platform's TimePicker emits uppercase ("1H", "24H") — the same
strings the CLI itself injects as defaults via buildSearchParams /
runServicesList — but the validator only accepted lowercase, so a user
who copy-pasted the displayed default got rejected client-side. Fold
the suffix to lowercase before checking the whitelist; preserve
original casing in the returned value.
Add a regression test in CLIE2ESpec asserting `--since 1H` is accepted.
Code Review: Refactor/LOC Reduction Cross-CuttingOverviewThis PR is a large cross-cutting audit and refactor focused on: (1) hardening the CLI for agentic use (stable JSON envelopes, client-side validation, KQL operator fixes, new Overall this is solid, purposeful work. Most of the reduction is genuine. Comments below focus on correctness concerns and remaining LOC opportunities. Issues
getField = toText . map toLower . drop 2 . showThis relies on constructor names having exactly a 2-character prefix ("FT") and on
agg = Map.map (\ts -> (length ts, maximum ts))
$ Map.fromListWith (<>) [(s, [t]) | (s, t) <- bumps]Double blank lines in CLI/Commands.hs — several top-level declarations have two blank lines between them (after
capped = maybe filtered (\n -> Map.fromList (take n (Map.toList filtered))) opts.schemaLimit
capped = maybe filtered (`Map.take` filtered) opts.schemaLimitMinor suggestions
validateAndNormalizeKind = mapM (fmap normalizeKind . validateOrDie . validateKind)
README "Agentic incident workflow" — steps 1 and 5 still use Positives
|
…, doc/comment fixes
1. validateDurationOrDie now takes the flag name, so --window failures
read "--window must match …" instead of "--since must match …".
Three call sites updated (--since x2, --window x1).
2. aggregateServices keeps its `Map.lookup "service"` but with a comment
pinning the contract: the server aliases resource___service___name to
the short "service" column in colIdxMap (Pages.LogExplorer.Log.allCols).
Added an e2e regression in CLIE2ESpec that asserts services-list still
returns {services, count} so a column rename can't silently zero the
output.
3. docs/cli.md agentic-incident-workflow: replace .items[] with .data[]
in the issues-list and log-patterns-list jq pipes — they'd break
against the now-normalised list envelope.
4. runEventsContext --summary used to walk logsData twice (once in
normalizeEventsResponse, once in withTraceSummary). Introduce a
`DecodedEvents` record that carries idxMap/rows/raw, decode once at
the call site, and thread it through both functions.
…t scan
Previously runServicesList fetched up to 10,000 events and aggregated
client-side because /api/v1/metrics returned empty for `summarize ... by`.
The facets endpoint (precomputed in apis.facet_summaries via a background
job) is one indexed lookup — sub-millisecond regardless of project size.
- Drop the events-scan fallback; switch to GET /api/v1/facets?field=resource.service.name
- ServiceRow loses the `last_seen` field (facets don't track per-value
recency). The remaining {name, events} is what the use-case actually
needs ("what services exist? how busy are they?").
- e2e regression in CLIE2ESpec already covers the {services, count}
envelope; comment updated to pin the facets-backed contract.
- docs/cli.md and monoscope-skills/README.md output-shape tables drop
last_seen.
Code Review — Refactor/LOC reduction cross-cuttingOverviewGood, well-motivated PR bundling three real improvements: agentic-CLI hardening (stable JSON envelopes, Bugs1. CI docs example uses
monoscope issues list --status open -o json | jq '.items | length'After 2. renderWith mode enriched (renderEventsTable val Nothing)The JSON/YAML path correctly emits Performance nit
AE.toJSON (take 1 (toList arr)) -- arr :: V.Vector AE.Value
Corrections to the existing reviewThree of the five findings in the prior review are not accurate:
Issue #1 (breaking env-var rename Minor
|
…ing, debug-comment Server side: - dashboardsGetH: drop the appCtx ask; bw.config.liveReloadDashboards carries the flag now that mkPageCtx threads config through. - teamAlertsGetH: drop appCtx + sess + project, all genuinely unused bindings. (dashboardGetH / dashboardTabGetH / dashboardTabContentGetH / unifiedMonitorOverviewH still bind appCtx — they read appCtx.env.* which isn't on bw.config, so the binding is required.) CLI: - takeFirstEvent: V.take 1 arr instead of take 1 (toList arr); avoids the Vector→List→Vector round-trip. - Resource.normalizeList split into normalizeListE returning Either, with Left carrying the raw payload on shape mismatch. runList now flags the unrecognised case via printDebug so contributors see when the server envelope drifts. - Core.renderParams: comment pinning the "debug-only, NOT URL-encoded, redact if you ever pass auth via query params" contract.
Code Review - Refactor/LOC reduction cross-cuttingGood cross-cutting cleanup. The Bug - doc example contradicts migration note In Performance - unnecessary full Vector->List conversion In Stale/orphaned Haddock comment Around the Fragile
String-literal matching on The non-agent branch matches Minor -
Breaking change - env var rename needs callout
Nit -
Overall the direction is right - |
- docs/cli.md CI/Automation example said `jq '.items | length'` — inconsistent with the agentic-workflow section above and broken since normalizeList moved everything to `.data`. - emitFirstEventId now uses `arr V.!? 0` instead of `head (toList arr)` — same V.take optimisation as takeFirstEvent, no intermediate list allocation. Bug #2 from the review (renderEventsTable in runEventsContext receiving raw `val`) is intentional: the table renderer reads `cols`/`logsData` out of the raw envelope to produce a dense table; the JSON path uses the named-field `enriched` form. Passing `enriched` to the table renderer would render an empty table because the normalised shape doesn't carry `cols`/`logsData`. The split is by design — table mode is column-oriented, JSON mode is record-oriented.
Code Review — Refactor/loc-reduction cross-cuttingOverall this is a solid, well-motivated PR. The KQL fix ( Breaking changes that need explicit call-outs1. Any existing CI pipeline or dotfile using 2. List JSON shape migration — The migration note in the docs is good, but the PR template checklist item "You are NOT deprecating/removing a feature" is unchecked (and unchecked in the boilerplate, so it may just be the template). Worth confirming intentional deprecation is OK per project policy. Bugs / correctness3. isAgentMode = any isJust <$> mapM Env.lookupEnv ["MONOSCOPE_AGENT_MODE", "CLAUDE_CODE", "CI"]
isAgentMode = any (\v -> isJust v && v /= Just "") <$> mapM Env.lookupEnv [...]
-- or simpler with extras: any (maybe False (not . null)) <$> ...4. In (False, _) -> renderWith mode sliced (renderEventsTable val validatedOpts.fields)
5. Double Haddock block In -- | Project the server's positional row representation into named-field
-- objects so agents/jq consumers don't need to thread 'colIdxMap'...
-- | Single decode of the events envelope. Sharing the parsed @colIdxMap@...
data DecodedEvents = ...Only the second one is actually attached to Performance / efficiency6.
withAPIResult cfg "/api/v1/events" params $ \val -> do
let d = decodeEvents val
normalized = maybe val (`normalizeDecoded` validatedOpts.fields) d
sliced = if validatedOpts.firstOnly || validatedOpts.idOnly then takeFirstEvent normalized else normalized
case (validatedOpts.idOnly, mode) of
(True, _) -> emitFirstEventId sliced
(False, _) -> renderWith mode sliced (renderEventsTable val validatedOpts.fields)Style / succinctness (GHC extensions available)7. A small helper type eliminates the hand-rolled data FacetRow = FacetRow {value :: Text, count :: Int}
deriving stock Generic
deriving AE.FromJSON via DAE.CustomJSON '[DAE.FieldLabelModifier '[DAE.CamelToSnake]] FacetRow
parseServiceFacets :: AE.Value -> [ServiceRow]
parseServiceFacets v = case AE.fromJSON v of
AE.Success (obj :: Map Text [FacetRow]) ->
[ ServiceRow f.value f.count
| f <- fold (Map.lookup "resource.service.name" obj) ]
_ -> []Shorter, type-safe, and consistent with how 8. AE.Error _ -> onFailThe AE.Error msg -> printDebug ("metrics decode failed: " <> toText msg) >> onFailBut this requires threading 9. -- new code:
idsOpt = T.splitOn "," <$> strOption (...)
-- vs existing in int64IdsOpt (same PR):
mapMaybe (readMaybe . toString) . T.splitOn "," <$> strOption (...)Both are fine; just noting consistency is already there. 10. Double blank lines Several spots introduce two blank lines between top-level definitions (after Positive highlights
|
Code Review — PR #365: Refactor/LOC reduction cross-cuttingOverall: Solid, well-motivated refactor. The direction is right — less boilerplate, stable agent-facing JSON shapes, and cleaner error paths. A few specific issues worth addressing before merge. Positives
Issues1. -- src/Models/Apis/Fields.hs
instance HasField "toText" FieldTypes Text where
getField = toText . map toLower . drop 2 . showThis relies on 2. toRow (AE.Object o) = ServiceRow <$> txt o "value" <*> int o "count"
txt o k = case KM.lookup k o of Just (AE.String s) -> Just s; _ -> Nothing
int o k = case KM.lookup k o of Just (AE.Number n) -> Just (round n); _ -> NothingThe server's 3. fromMaybe "" (Map.lookup k d.idxMap >>= \i -> listToMaybe (drop i r))If 4. -- cli/Main.hs
when global.debugFlag $ setEnv "MONOSCOPE_DEBUG" "1"
5. The comment says "if you ever add a credential to a query param, redact it here" — but there's no enforcement. 6. emitCompletion shell = case shell of
"bash" -> emit "--bash-completion-script"
...
other -> liftIO $ do -- only the error branch is in liftIO directly
...
where
emit shellArg = liftIO $ ...This is fine but slightly inconsistent — both branches end up in 7. Missing The PR adds 8. -- src/Pages/Dashboards.hs
dashboardGetH ... = do
(_, project, bw) <- mkPageCtx pid
appCtx <- ask @AuthContext -- ← still here
Minor / Style
|
…arity, single decode, derived facets, forall pinning 3. isAgentMode no longer fires on `CI=` (empty) or `CI=false` — many dev shells export CI unconditionally; treat empty/"false"/"False"/"FALSE"/"0" as off. 4. --first parity in table mode: takeFirstRow trims the raw envelope's logsData to one row, so table output matches the JSON path's takeFirstEvent slice instead of silently dumping all rows. 5. Free-floating Haddock above DecodedEvents merged into the actual docstring (the orphan would have triggered a -Wmissing-signatures-ish warning eventually). 6. runEventsSearch now decodes once and threads DecodedEvents through normalizeDecoded, matching the pattern in runEventsContext. No more double walk of logsData on the JSON path. 7. parseServiceFacets decodes via Models.Apis.Fields.FacetValue (FromJSON via DAE.CustomJSON) — drops 6 lines of hand-rolled KM.lookup/AE.String/AE.Number unpacking and surfaces server wire changes at compile time. 8. withMetricsData now forwards aeson's Error message through printDebug so a server envelope drift is visible under --debug instead of vanishing into the raw-JSON fallback. Plus: explicit `forall es a b` on apiGetJson/apiPostJson/apiPutJson/ apiPatchJson so TypeApplications at call sites (`apiPostJson @_ @_ @AE.Value`) survive future constraint reordering by fourmolu/hlint. Without this, alphabetical resort of `(AE.FromJSON b, AE.ToJSON a, Environment :> es, ...)` flips the implicit type-var order and the third `@AE.Value` lands on `es` instead of the body type. Run fourmolu over the touched files; #10 (double blank lines) is auto-fixed.
Code Review — PR #365: Refactor/loc reduction cross cuttingGood overall direction. This PR fixes real bugs (KQL operators, broken Potential Bug —
|
…ge, dead code
3. normalizeDecoded now emits `null` for missing fields (row shorter
than colIdxMap) instead of silently coercing to "". Agents can now
tell "field present but empty" from "field missing".
4. Comment on the --debug setEnv site explaining the single-shot, pre-
thread-spawn placement (so future readers know it's not a race).
6. emitCompletion lifts to IO once at the entry instead of per-branch.
7. CLIBinarySpec helpCases extended with `facets`, `schema`, `services
list`, `events {search,context,tail}`, `metrics {query,chart}`,
`auth login`, `completion` — every top-level subcommand now has a
--help smoke test.
8. dashboardGetH/dashboardTabGetH: replace appCtx.config.hostUrl with
bw.config.hostUrl now that mkPageCtx threads config through; appCtx
binding stays because appCtx.env.apiKeyEncryptionSecretKey isn't on
bw. dashboardTabContentGetH untouched — no mkPageCtx, no bw.
Style:
- Drop dead extractNumericRows from Pkg.CLIFormat (no callers since the
metrics path moved to MetricsData).
- Export validateDurationFor so the doctest examples (which reference it)
resolve at doctest runtime.
Re #2 (parseServiceFacets via FromJSON) and #10 (double blank lines):
already landed in commit 2a54bd0.
Re #5 (renderParams allowlist): keeping the comment-only enforcement.
The CLI doesn't put credentials in query params today (Bearer + project
go via headers), and a type-level distinction would touch every API
helper for a hypothetical leak. The doc note is sufficient guard for
contributor review.
Code Review – Refactor/loc reduction cross cuttingOverviewGood LOC-reduction pass across the CLI and server-side page handlers. The dominant gains are Breaking change – env var rename
|
Core bug:
- withTraceSummary used `cell "severity"` to count errors per trace, but
the events colIdxMap (Pages.LogExplorer.Log.allCols) doesn't expose a
"severity" column — only the boolean "errors" flag. Result: every
trace summary reported error_count=0 regardless of traffic. Now reads
cell "errors" and treats "true" as 1. Also added a defensive fallback
for service ("service" <|> "resource.service.name") in case a future
column-set rename hides the alias.
KQL safety:
- runEventsGet escapes `\` and `"` in opts.eventId before splicing into
the query string. Event IDs aren't UUIDs (synthetic-… span ids,
log_pattern integers, …) so format validation is wrong; escaping
defends against injection regardless of shape.
Small wins:
- isAgentMode: fold suffix to lowercase once via `map toLower v` rather
than enumerating "false"/"False"/"FALSE".
- filterSchema: `Map.take n filtered` instead of
`Map.fromList (take n (Map.toList filtered))` (containers ≥ 0.6.3).
Doctests note:
- The earlier "doctest comment" the review flagged was already removed
in a prior commit. Pure validators are still covered end-to-end via
CLIE2ESpec ("--since 1xyz", "--kind banana") which exercise the real
binary. Wiring CLI/* into the lib doctest target would need moving
the modules into src/ — out of scope for this round.
Pushed back:
- AuthStatus method ADT: the strings "env"/"token" appear in two
adjacent lines of one function. A two-constructor ADT + custom ToJSON
is more boilerplate than the leak it prevents (no other consumer).
- Map merge tuple readability: a record would cost two lines and one
data declaration to save zero compile errors. The merge fits on one
line; leaving it.
…ss comment - Pages/Projects.hs: replace seven `toastError "…" ""` and three `addRespHeaders ""` with `mempty`. Reads as "no body" rather than "empty string body" — the underlying type is `Html ()` whose Monoid identity is the empty Html, same wire effect. - cli/Main.hs `capFacets`: comment on the trailing wildcard clause so the (Just n) + non-Object fallthrough is explicitly documented vs the Nothing case the first clause already handles. Re facets in CLIBinarySpec helpCases: already added in 76ebf71.
|
Code Review PR 365 - see full review below |
|
Code Review PR 365 - Bugs/Correctness
In src/Models/Apis/Fields.hs: getField = toText . map toLower . drop 2 . show Works for all current constructors, but this is an implicit contract on Show. If a constructor is added with <=2-char suffix or renamed without the FT prefix, it silently produces wrong output at runtime. The original explicit fieldTypeToText was safer. At minimum add a doctest asserting the output; better: delegate to WrappedEnumSC which already owns the wire encoding.
validateDurationFor accepts '1H', '24H' (case-insensitive), but parseDurationMs only matches lowercase suffixes. parseDurationMs '5M' falls through to readMaybe and returns 5000ms instead of 300000ms. Only affects --watch on metrics chart, but validateDurationOrDie will silently accept '5M' and pass it to parseDurationMs which misreads it. Fix: T.toLower the input at the start of parseDurationMs.
The .~ is split across three lines making it look like a prefix to the next & chain. Formatter artifact worth overriding.
In dashboardGetH/dashboardTabGetH: appCtx <- ask @authcontext is still present alongside (_, project, bw) <- mkPageCtx pid, with bw.config.hostUrl used in one place and appCtx.env.apiKeyEncryptionSecretKey in another. ask is still needed for the encryption key, but the asymmetry is surprising; a comment explaining why would help. |
Code Review — PR #365 (Refactor/LOC reduction cross-cutting)Good overall direction. The agentic-pipeline additions are well-motivated and the agent-mode detection fix ( Improvements worth making before merge1. cell field r = Map.lookup field d.idxMap >>= \i -> listToMaybe (drop i r)
2. T.toLower suffix `notElem` ["ms", "s", "m", "h", "d"]This is fine — 3. If the server adds a field to its 4. httpExToError (HttpExceptionRequest req (StatusCodeException resp body)) =
5. _ -> printError "no events matched" >> liftIO exitFailureThis is fine — 6. truthy = maybe False (\v -> not (null v) && map toLower v `notElem` ["false", "0"])
Minor / style
Security / correctness
What's good
|
BackgroundJobs.hs: 6× displayException + 2× fromException were resolving to Control.Exception.* (because the file did `import Control.Exception (ErrorCall (..))` which exposed the Control.Exception namespace to GHC's resolver and tripped .hlint.yaml's "use Relude version" rule). Drop the unqualified import; ErrorCall now goes through `Control.Exception qualified as CE` so the only `Control.Exception` reference is the explicit qualifier and unqualified displayException/fromException resolve through Relude. Pages.Anomalies.hs: - NE.head -> head (Relude) - two whenJust lambdas -> point-free (toHtml composition) - when (... `notElem` ...) -> unless (... `elem` ...) Pages.Settings.hs: sortOn fst -> sortWith fst (no caching needed for projection on first component). Pages.Telemetry.hs: MapS.singleton k v -> one (k, v) (Relude polymorphic container constructor). Pages.LogExplorer.Log.hs: two `\j -> (j, x)` lambdas -> tuple sections (TupleSections is part of GHC2024, already on). Verified clean: `hlint src/BackgroundJobs.hs src/Pages/Anomalies.hs src/Pages/Settings.hs src/Pages/Telemetry.hs src/Pages/LogExplorer/Log.hs` reports "No hints".
Code Review: Refactor/LOC Reduction Cross-CuttingGood PR overall — clear intent, consistent direction, real bug fixes bundled in. Notes below, prioritised by impact. Bugs / Correctness
-- Fields.hs
getField = toText . map toLower . drop 2 . showThis relies on
withTraceSummary _ v = v -- catchallThis is correct, but the Code Quality / Succinctness
AE.Object _
| AE.Success (p :: Paged AE.Value) <- AE.fromJSON v -> ...This means any
case AE.fromJSON val of
AE.Success md -> onOk md
AE.Error msg -> printDebug ... >> onFailis fine, but this is exactly what
-- current
AE.Object obj
| Just (AE.Array arr) <- KM.lookup "events" obj
, Just (AE.Object e) <- arr V.!? 0
, Just (AE.String i) <- KM.lookup "id" e -> putTextLn i
-- alternative (with lens already in scope)
val ^? key "events" . nth 0 . key "id" . _String
| T.null t = Left ... -- checks raw t
| otherwise = let trimmed = T.strip t -- strips after the null check
Performance
Breaking Change / Migration Risk
Naming / Style
Test CoverageThe docs describe three test layers (doctests, binary smoke tests, e2e). The new
Minor
|
…s null 3. normalizeListE now returns Either (rawValue, reason) — when an Object fails @PageD@ decoding the aeson Error message is captured and surfaced through runList's --debug log. Contributors see *what* shape changed instead of a generic "did not match envelope". normalizeList stays pure with `either fst id . normalizeListE`. 4. httpExToError now uses TE.decodeUtf8With lenientDecode for both the status reason and the response body. The body field of StatusCodeException is the truncated raw bytes (≤1024B) and may be gzip leftovers, HTML, or arbitrary upstream junk; strict decodeUtf8 would throw at the error-handling boundary, masking the real HTTP failure with a UnicodeException. 6. isAgentMode: replace `not (null v)` with `v /= ""` to dodge the Prelude.null vs Relude container-null ambiguity. v is String here. Re prior style notes: - Double blank lines between top-level bindings: produced by fourmolu format pass; project convention is enforced by the formatter. - AuthStatusJson / ConfigGetJson: kept as records — the CamelToSnake derivation pins the wire shape (an inline AE.object [...] silently drifts when fields are added). - runConfigGet OutputMode asymmetry vs init/set: noted; init takes no output path (interactive form) and set has no payload to format. Follow-up if it surfaces in user feedback. - normalizeList = either fst id . normalizeListE (was either id id; shape changed when Left started carrying the reason). - CLI/UI.hs `currentValue = T.strip . unlines . BE.getEditContents`: verified — `unlines` here is the Relude re-export, which is polymorphic over the IsList/Foldable instance. Compiles clean.
Code Review — Refactor/LOC Reduction Cross-CuttingGood overall direction. The PR tightens the agentic surface (stable JSON envelopes, client-side validation, Bugs / Correctness
-- current (wrong — & associates left, so .~ sees the wrong LHS)
W.defaults
& W.header "Accept"
.~ ["application/json"]
& addAuth cfg.apiKeyThe W.defaults
& W.header "Accept" .~ ["application/json"]
& addAuth cfg.apiKey
& addProjectId cfg.projectId
& addParams params
Succinctness / Use Existing Packages
T.toLower suffix `notElem` ["ms","s","m","h","d"]
-- →
T.toLower suffix `Set.notMember` Set.fromList ["ms","s","m","h","d"]Or define a top-level
withMetricsData val onFail onOk = case AE.fromJSON val of
AE.Success md -> onOk md
AE.Error msg -> printDebug ("metrics decode failed: " <> toText msg) >> onFailThis is already what the PR does, so fine; but the helper could simply be: withMetricsData :: … => AE.Value -> Eff es () -> (MetricsData -> Eff es ()) -> Eff es ()
withMetricsData = either (const …) … . AE.fromJSON -- not meaningful without the debug lineKeep as-is.
emitFirstEventId v = maybe notFound putTextLn (v ^? key "events" . nth 0 . key "id" . _String)
where notFound = printError "no events matched" >> liftIO exitFailure
takeFirstIn :: AK.Key -> AE.Value -> AE.Value
takeFirstIn k (AE.Object obj) = AE.Object $ KM.adjust (\case AE.Array a -> AE.Array (V.take 1 a); x -> x) k obj
takeFirstIn _ v = v
Design / API SurfaceBreaking env var rename without a compatibility shim:
Test Coverage
Minor
Summary: The direction is right and the agent-mode story is significantly better. Fix the |
Same pattern as the ErrorPatternsSpec fix in f0ab87d: IssueL wraps Issue in `base`, so the issueType field lives at `c.base.issueType`, not `c.issueType`.
Code ReviewOverviewSolid LOC-reduction pass. The three new abstractions ( Good
Issues1. Incomplete Four call sites were missed: -- still unconsolidated in Anomalies.hs
Nothing -> addErrorToast "Error not found" Nothing >> addRespHeaders mempty
| err.projectId /= pid -> addErrorToast "Error not found for this project" Nothing >> addRespHeaders memptyThese should become 2. -- before
deduped = map NE.head $ NE.groupBy ((==) `on` dedupKey) $ sortOn dedupKey raw
-- after
deduped = map head $ NE.groupBy ((==) `on` dedupKey) $ sortOn dedupKey raw
3. defaultFrom = fromMaybe (addUTCTime (-86400) now) fromTThe The function returns 4. findBin :: IO FilePath
findBin = T.unpack . T.strip . toText <$> readProcOut "cabal" ["list-bin", "exe:monoscope"]
5. KM.member (fromString (toString k)) obj
6. withCardFrame :: Bool -> Widget -> Maybe (Text, Text) -> Html () -> Html ()
7. Repeated This idiom appears ~10 times. A small helper: assertSuccess :: (ExitCode, String, String) -> IO ()
assertSuccess (ExitSuccess, _, _) = pure ()
assertSuccess (code, _, err) = expectationFailure $ "exit=" <> show code <> " stderr=" <> errwould make the tests more readable and the duplication disappears. Minor nits
|
CLIE2ESpec imports Network.HTTP.Types.Status (statusCode) for the reachability probe but http-types wasn't on the test target's deps. The library has it (line 173), the test suite didn't inherit it.
Code Review — PR #365: Refactor/loc reduction cross cuttingOverviewThis is a large, high-quality refactor touching the CLI, server pages, and shared models. The primary themes are:
The net LOC reduction is substantial and the code is generally more correct than what it replaces. Observations below are ordered by impact. Good changes worth calling out
Issues and suggestions1.
|
Code Review: Refactor/loc reduction cross cuttingGood overall direction — the agent-mode hardening,
|
Build break: UnliftIO.Exception.try's forall is (m, e, a); the @someexception TypeApplication landed on m and tripped a kind error. Switch to tryAny (MonadUnliftIO m => m a -> m (Either SomeException a)) — same semantics, no type-arg juggling. Drops the redundant Data.List (isInfixOf) import along the way (Relude already has it). Review items: - AuthStatus: extract printStatus helper for the env/token branches — 3 lines repeated → 1 helper. - apiFacets: replace addUTCTime (-86400) with negate nominalDay (Data.Time re-export). No more 86400-second magic. - capFacets: drop the now-redundant `Nothing` clause; the wildcard at the end already covers it (and `Just n` + non-Object). - parseDurationMs: T.toLower the input via a view pattern so --watch 5M is treated like --watch 5m, matching validateDurationFor's case-insensitive suffix check. - reqOpts: collapse the broken multi-line `& W.header "Accept" .~ [...]` formatter artifact into a single parenthesised line so subsequent `&` chain elements align under W.defaults. - Resource.runList: route the unrecognised-envelope reason through printError (always-on stderr) instead of printDebug — agents expecting {data, pagination} need to see when the server returns something else. - DecodedEvents: decode count / has_more / cursor at construction instead of carrying the raw KeyMap. Three lookups eliminated, intent is local to decodeEvents. Re minor nits noted: - isAgentMode/isDebugMode re-read env on each call: kept; called at most a handful of times per invocation, no hot path. - Data.Char (toLower): kept the explicit import — Relude does re-export it but the explicit form documents which `toLower` we mean (vs Data.Text.toLower which operates on Text).
Code Review: Refactor/loc-reduction cross-cuttingOverviewThis is a substantial PR adding agentic-mode improvements to the CLI (structured JSON output, pagination, Potential bugs1.
2.
3. The body is now included verbatim in error messages — great for KQL parse errors, but a WAF/CDN HTML error page could produce kilobytes on stderr. Add Package / extension opportunities4. UUID validation reimplements
validateUuid t
| T.null t = Left "expected UUID; got empty string"
| isNothing (UUID.fromText t) = Left $ "expected UUID (8-4-4-4-12 hex); got '" <> t <> "'"
| otherwise = Right t5.
6. Three
Resource.normalizeList <<$>> apiGetJson ... -- issues, endpoints, log-patternsThese should go through Conciseness / style7. Double blank lines add ~100 lines of noise The PR introduces double blank lines between most top-level declarations in 8. Audit-finding codes in comments will rot Codes like 9.
Migration note (breaking change)The What's good
|
Code Review — Refactor/loc reduction cross cuttingGood overall direction: adding agent-mode capabilities, normalising JSON envelopes, and cleaning up cross-cutting concerns. Several concrete issues worth addressing before merge. Breaking change (no migration path)
Bug: backslash not escaped in
|
…demo project
CI failures:
1. CLIBinarySpec "auth status exits and shows status message" — when CI
is set, isAgentMode auto-enables and `auth status` emits the JSON
envelope ({authenticated, method, api_url, project}). The test
predicate only matched plain-text "Not authenticated" / "Authenticated".
Extend it to also accept the JSON shape ("authenticated" key).
2. CLIBinarySpec "completion --help" exited 1. The completion subparser
declared a positional SHELL argument but didn't attach `<**> helper`,
so `--help` got consumed as the SHELL value and fell through to the
"unknown shell" branch. Adding helper makes optparse intercept --help
and emit usage with exit 0.
Default e2e suite to prod demo project:
CLIE2ESpec.getE2EConfig now defaults @MONOSCOPE_API_URL@ to
https://api.monoscope.tech and @MONOSCOPE_PROJECT@ to the public demo
00000000-0000-0000-0000-000000000000. Only @MONOSCOPE_API_KEY@ has no
safe default (read keys for other projects would leak). The pendingWith
message names the missing variable specifically so it's obvious how to
unlock the suite.
Wired up in CI: pullrequest.yml's integration-tests step now reads
MONOSCOPE_API_KEY from secrets.MONOSCOPE_DEMO_API_KEY. Repo owner needs
to mint a read-only key on the demo project and store it in repo
secrets — then every PR/push runs the suite against prod. Forks (no
secret) still get the CLIBinarySpec smoke tests.
docs/cli.md updated with the new default-driven workflow.
Code Review — PR #365 (Refactor/LOC reduction cross cutting)Overall this is a solid, well-motivated refactor with real functional improvements bundled in. The LOC reduction is genuine and the agent-mode additions are thoughtfully designed. Below are specific findings, roughly prioritised. Bugs / correctness issues
getField = toText . map toLower . drop 2 . show
withTraceSummary _ v = vIf the events call returns something other than an Object (e.g. unexpected server shape change), Code quality / conciseness opportunities
-- current
let (digits, suffix) = T.span isDigit trimmed
-- same, more idiomatic with ReludeThis is fine, but
addErrorToast msg Nothing >> addRespHeaders (SomeError msg)The new
-- current
sinceParam = case opts.since of
Just s -> s
Nothing -> if isJust opts.from || isJust opts.to then "" else "1H"
-- more point-free
sinceParam = fromMaybe (bool "1H" "" (isJust opts.from || isJust opts.to)) opts.sinceModule export list style — inconsistent trailing comma style Blank lines between top-level definitions Performance
SecurityKQL injection in
Test coverageThe new
Minor
SummaryThe core refactors ( |
ErrorPatternsSpec test 12 ("Full LLM pipeline") asserted
`length mergedTypeErrors == 1` after merging one (childId, canonId)
pair. The query counted ALL TypeError patterns with canonical_id set
across the project, so any earlier test that merged a TypeError made
the count drift up. CI now sees 2 because some upstream change in the
LLM/notify pipeline produced an additional pre-merged pattern by the
time test 12 runs.
Switched to a behaviour-level assertion: query the specific childId's
canonical_id and require it equals canonId. This tests what
`assignErrorsToCanonical` actually does, with no dependence on the
global state earlier tests left behind.
Code Review — Refactor/LOC reduction cross-cuttingGood overall direction. The PR eliminates a lot of boilerplate ( Bugs / correctness
groups = Map.fromListWith merge
[ (tid, …)
| r <- d.rows
, let tid = fromMaybe "" (cell "context.trace_id" r <|> cell "trace_id" r)
, not (T.null tid) -- ← this guard already removes them
]The guard is present, so this is fine — but the comment on L648 says "Empty
truthy = maybe False (\v -> v /= "" && map toLower v `notElem` ["false", "0"])
Succinctness / use existing packages
Both functions pattern-match on -- before (14 lines for two functions)
takeFirstEvent (AE.Object obj) = case KM.lookup "events" obj of
Just (AE.Array arr) -> AE.Object (KM.insert "events" (AE.Array (V.take 1 arr)) obj)
_ -> AE.Object obj
takeFirstEvent v = v
-- after (4 lines, same semantics)
takeFirstEvent = key "events" . _Array %~ V.take 1
takeFirstRow = key "logsData" . _Array %~ V.take 1
-- current
case (T.null prefix, T.null trimmed) of
(True, _) -> trimmed
(_, True) -> prefix
_ -> prefix <> " and (" <> trimmed <> ")"
-- more idiomatic with Relude's `guarded` or just pattern guards
foldFiltersIntoQuery query mService mLevel
| T.null prefix = trimmed
| T.null trimmed = prefix
| otherwise = prefix <> " and (" <> trimmed <> ")"
where …
Map.fromList [(AK.toText k, round n) | (k, AE.Number n) <- KM.toList obj]
parseDurationMs (T.toLower -> t)Good use of
cell :: Text -> [Text] -> Maybe Text
cell field r = Map.lookup field d.idxMap >>= \i -> listToMaybe (drop i r)
svcIdx = Map.lookup "service" d.idxMap <|> Map.lookup "resource.service.name" d.idxMap
errIdx = Map.lookup "errors" d.idxMap
tidIdx = Map.lookup "context.trace_id" d.idxMap <|> Map.lookup "trace_id" d.idxMap
at idx r = idx >>= \i -> listToMaybe (drop i r)Saves a
|
- takeFirstEvent / takeFirstRow rewritten with Data.Aeson.Lens (`AL.key … . AL._Array %~ V.take 1`). 14 lines of pattern-match-and- reinsert collapsed to two one-liners. AL is qualified to avoid collision with the `key` record fields on ConfigSet/GetOpts. - foldFiltersIntoQuery: case (T.null …, T.null …) → pattern guards. Reads top-down instead of as a tuple-match puzzle. - normalizeListE: AEKM.member → has (AL.key …) lens accessor; the branch matches on the value directly, no need to destructure AE.Object obj first. - extractColIdxMap: round → floor on the Scientific. Column indices are already-integral non-negative ints; floor documents that intent and drops banker's rounding semantics that don't apply here. - parseDurationMs: drop the bare-number-as-seconds fallback. validateDurationFor rejects unsuffixed values upstream, so the only paths that reach parseDurationMs have a recognised suffix; a typo now returns the 5s default loudly instead of silently treating "5" as five seconds. - runMetricsChart: validateDurationOrDie "--watch" before entering the forever loop, so a bad --watch string fails at flag parse rather than burning HTTP requests every 5s. - withTraceSummary: precompute svcIdx / errIdx / tidIdx once instead of re-Map.lookup'ing the same column path per row. Saves one Map lookup per cell per row — measurable on large `events context --summary` payloads. Build deps: cli exe gains `lens-aeson` (already in lib deps for the server's Data.Aeson.Lens usage). hpack regenerated monoscope.cabal.
Code Review: CLI agent-mode hardening + cross-cutting LOC reductionOverviewSolid PR. The KQL operator fix ( Potential bugs / issues1. q v = "\"" <> T.replace "\"" "\\\"" v <> "\""A service name containing a backslash (e.g. let eid = T.replace "\"" "\\\"" (T.replace "\\" "\\\\" opts.eventId)Apply the same pattern in q v = "\"" <> T.replace "\\" "\\\\" (T.replace "\"" "\\\"" v) <> "\""2. instance HasField "toText" FieldTypes Text where
getField = toText . map toLower . drop 2 . showThis silently breaks if any constructor is renamed to one without a 2-char prefix (e.g. 3. "(id==\"" <> eid <> "\") or (context.trace_id==\"" <> eid <> "\")"When Minor4. Double blank lines between top-level definitions The PR adds two blank lines after each top-level definition in 5. Pagination shape for bare arrays is not fully parallel to the
6.
What's good
|
Two threads landed together because the CLI work uncovered several places where the server code could be DRY'd up or had subtle bugs the agent-facing tests caught.
CLI: agent-mode hardening
Audit + repair of the
monoscopeCLI so an LLM agent can drive incident response end-to-end.Critical bugs fixed
--service/--leveland any positional KQL emittedfield:value(Lucene), which the server's parser rejects with HTTP 400. Now emitsfield=="value". Three callers (events search,events tail,events context) were affected.events get <id>usedtrace_id:<id>(wrong operator + wrong column name) → 400 on every call. Nowid==<id>(with--treeswitching tocontext.trace_id==<id>); event IDs are escaped before splicing into KQL.Core.httpExToErrordiscarded the response body, so KQL parser errors (which include line/column markers) surfaced as a genericHTTP 400. Now forwards the body verbatim with lenient UTF-8 decoding.events context --summarywalkedlogsDatatwice; single decode viaDecodedEventsshared betweennormalizeDecodedandwithTraceSummary. The summary'serror_countwas always 0 (looked upseveritywhich isn't on the eventscolIdxMap); now reads theerrorscolumn.services listused to scan up to 10k events and aggregate client-side; backed by/api/v1/facets?field=resource.service.namenow (one indexed lookup).--firstworked in JSON mode but ignored in table mode;takeFirstRowtrims the raw envelope to match.validateDurationrejected1H/24H(the strings the CLI itself displays as defaults). Suffix check is now case-insensitive;validateDurationOrDietakes the flag name so--windowerrors say--window.isAgentModefired onCI=(empty) orCI=false— many dev shells exportCIunconditionally. Now treats empty/false/0(case-insensitive) as off.Agentic UX additions
--debugflag (andMONOSCOPE_DEBUG) prints every outgoing request URL+params to stderr.--cursor,--first,--id-onlyfor chain-friendly pagination (events search --first --id-only | xargs monoscope events get).events context --summaryadds a per-trace breakdown ({traces: [{trace_id, services, span_count, error_count}, …]}).schema --search/--limitto fetch a focused slice of the 600-line schema.facetssubcommand for the precomputed top-N values per field.--agentmode emits stable JSON forauth status({authenticated, method, api_url, project}).auth loginrefuses in agent mode (would otherwise hang on the device-code poll).Stable JSON envelope
events search/logs search/traces search/events get/events context:{events: [{id, timestamp, service, summary, trace_id, kind, …}], count, has_more, cursor}.services list:{services: [{name, events}], count}.issues,monitors,dashboards,api-keys,teams,members,endpoints,log-patterns):{data: […], pagination: {has_more, total, cursor, page, per_page}}.Validation + safety
CLI.Validatemodule:validateDuration,validateUuid,validateKind,normalizeKind— fail-fast with actionable messages instead of relying on the server.events get(\+"escaped).Env-var rename
MONO_*→MONOSCOPE_*across CLI, install script, docs, skills, and tests.Derived JSON instances
Replaced manual
instance ToJSONand inlineAE.objectcalls withderiving-aeson(CamelToSnake) records:ServiceRow,AuthStatusJson,ConfigGetJson,TraceSummary. Adding a field can't drift between Haskell and the wire.CLI: testability story
test/integration/CLI/CLIE2ESpec.hsruns the actualmonoscopebinary against a real server. Every audit finding has at least one regression test (KQL operator, kind→source mapping, error-body surfacing, list envelope, agent-mode JSON, validation messages).https://api.monoscope.tech, project00000000-…); onlyMONOSCOPE_API_KEYhas no safe default. CI reads it fromsecrets.MONOSCOPE_DEMO_API_KEY(one-time setup needed: mint a read-only key and add to repo secrets).CLIBinarySpecsmoke-tests--helpfor every top-level subcommand including the newfacets,schema,services list,events {search,context,tail},metrics {query,chart},auth login,completion.Cross-cutting LOC reduction (server-side)
mkPageCtxextracted from page handlers — collapses the recurrent(sess, project) <- … ; appCtx <- ask … ; bw = def{…}preamble.toastErrorhelper for validation/error response paths inPages/Projects.hs; replaced 7toastError "…" ""and 3addRespHeaders ""withmempty.withCardFrameextracted from the widget renderer for the shared card-shell HTML.Models.Apis.Fields: deriveFieldTypes/FieldCategoryEnumJSON viaWrappedEnumSC(drops 60-line manual instance block).Schema.Schema,Web.ApiTypes.Paged,Pages.Charts.Types.MetricsData— wire-shape changes break the CLI at compile time now.Documentation
docs/cli.mdrewritten end-to-end with valid KQL examples, the stable JSON envelope,--cursorpagination, and a new "Agentic incident workflow" worked example.monoscope-tech/skills(separate repo) updated to reference the new flags, env vars, and output shapes; pinned via integration tests in this repo.getting-started.mdenv-var rename touch-up.hlint cleanup
Cleared all hlint warnings across
src/BackgroundJobs.hs,Pages/Anomalies.hs,Pages/Settings.hs,Pages/Telemetry.hs,Pages/LogExplorer/Log.hs(displayException/fromException re-exports,NE.head→head,sortOn→sortWith,Set.singleton→one, tuple sections, etc.).hlint cli/andhlint src/<touched>both green except the two project-config-level hints (Setalias,universeimport) that need.hlint.yamlchanges.How to test
Checklist
CLIBinarySpec(binary smoke),CLIE2ESpec(real-server e2e).docs/cli.md,getting-started.md,monoscope-tech/skills.