Skip to content

feat(dashboards): add Streamlit agent analytics dashboard - #480

Draft
whuang602 wants to merge 5 commits into
GoogleCloudPlatform:mainfrom
whuang602:streamlit
Draft

feat(dashboards): add Streamlit agent analytics dashboard#480
whuang602 wants to merge 5 commits into
GoogleCloudPlatform:mainfrom
whuang602:streamlit

Conversation

@whuang602

@whuang602 whuang602 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What: Python-native alternative to the Grafana dashboard, backed by the same typed views and filter semantics, essentially extending existing Grafana features into Streamlit. Includes scan-cap guardrails and query caching. Intentionally leaving existing Streamlit content intact.

Why: Addresses the optional dashboard integrations mentioned in #339. This is intentionally paired with PR #481, which is working to consolidate 3P dashboard integration into a dedicated dashboards/ directory.

Reason this is in draft: Anti-drift check script & tests are in review.

image image image image

@caohy1988 caohy1988 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: 1 / P1: 3 / P2: 1 — REQUEST_CHANGES

Reviewed HEAD 08d42ac55e3dd8641d16502f0c1aaa24b01296e0. The PR is still OPEN + DRAFT. The acknowledged anti-drift work remains a readiness requirement; this is not LGTM pending only an undraft.

  1. P0 — Report billed bytes accurately. dashboards/streamlit/queries.py:790 returns job.total_bytes_processed, which dashboards/streamlit/app.py:575-580 totals and labels billed. These are different BigQuery statistics. A synthetic successful job with 1,024 bytes processed and 10,485,760 bytes billed renders “1.0 KB billed”. This silently understates the dashboard's FinOps accounting. Carry total_bytes_billed separately for on-demand billing, or label this measure “processed” and remove the billing claim. Preserve zero incremental usage for cache hits. The BigQuery job contract distinguishes the fields and qualifies billed bytes under capacity pricing.

  2. P1 — Preserve selected filters when options expire or fall outside the cap. dashboards/streamlit/app.py:90-91,192-194 removes selections absent from refreshed options; dashboards/streamlit/models.py:242 then turns an empty selection into ___ALL___. Streamlit AppTest reproduces selected agent alpha / session s1 becoming all agents / all sessions when those options disappear, without an explicit filter-clear action. This can happen after narrowing the window or after newer sessions displace the selected session from the 1,000-option list. Preserve the selected values in the widget's option set so queries retain their scope and return no matching data when appropriate.

  3. P1 — Keep truncated IDs reachable, as Grafana does. dashboards/streamlit/app.py:197-201,213-217 uses multiselects without accept_new_options or another exact-ID input, while dashboards/streamlit/queries.py:146-148 caps each option list at 1,000. Streamlit 1.49 defaults custom values to disabled. In a busy window, an older known session/user cannot be selected even by typing its exact ID; the shortest available window can still contain more than 1,000 IDs. grafana/README.md:192-194 explicitly guarantees custom values for Agent, User ID and Session. Add the equivalent parameterized input path and retain those values across option refreshes.

  4. P1 — Land the promised Streamlit contract tests and CI before readiness. dashboards/streamlit/queries.py:82-113,639-669,726-838 introduces filter, cap and cache contracts, but this nine-file diff contains no tests or anti-drift checker. The existing .github/workflows/grafana-sync-check.yml:20-31 watches Grafana paths only, and the current checker verifies Grafana SQL/JSON, not these Python builders. Land the acknowledged draft follow-up with explicit panel/filter mappings, whole-session rollups, missing-data behavior, bound parameters, real-job byte caps, cache-scope cases and filter-retention regressions; ensure Streamlit changes trigger it.

  5. P2 — Document typed-view setup before the run command. dashboards/streamlit/README.md:17-42 goes directly from environment configuration to launch. An existing raw events table alone is insufficient for the overview latency, LLM and tool panels. Include the views create-all prerequisite with matching project/dataset/table and view-prefix guidance, plus the query identity's jobUser/dataViewer requirements. The missing-view error handler already provides a recovery command, but setup should establish this dependency first.

Validation: all 15 builders parsed as single SELECT/UNION queries; identifier-injection rejection, bound filter parameters, cache misses for changed filter/project/dataset/window/cap, repeated-query cache hits, and over-cap refusal passed in offline probes. Each real-job configuration retained maximum_bytes_billed. A synthetic four-tab AppTest passed on Streamlit 1.49.0 / Plotly 6.0.0. Existing Grafana sync + typed-view tests: 37 passed; the standalone checker verified 37 Grafana queries. Wheel metadata and a .[dashboards] dependency-resolution dry run passed. No customer BigQuery queries were executed; no source fixes were made.

Pairing with #481: links resolve at this HEAD. When Grafana moves, reconcile dashboards/README.md:10 and the old grafana/queries/README.md references in app.py:210 / queries.py:86, along with the new anti-drift CI paths. This is merge-order coordination, not an additional current-path defect.

@caohy1988 caohy1988 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW_VERDICT=REQUEST_CHANGES
P0=0 / P1=2 / P2=1

Astra re-review of exact HEAD 11b2624560615d3b6dedc0c2097e9155e5b7ef8e, including the complete delta from 08d42ac55e3dd8641d16502f0c1aaa24b01296e0. Counts are deduplicated by root cause: the two prior filter findings now share one demonstrated widget-state defect. The PR remains OPEN + DRAFT.

Prior finding Disposition at this HEAD Evidence
P0: processed bytes labeled billed CLOSED Actual fetch() -> scan_log -> footer path reports 10.0 MB billed (1.0 KB processed) for a synthetic job with those distinct statistics. Repeated local-cache reads add zero bytes; a BigQuery cache hit reports zero billed bytes.
P1: expired selections silently become ALL PARTIAL; blocker remains Explicit pruning is removed and selected values are added to options, but real Streamlit 1.49.0 widgets still reset their selections when the options change. See R1.
P1: capped-out exact IDs unreachable PARTIAL; blocker remains Agent/User/Session now enable custom input, but entering a new ID and clicking Apply resets it to ALL. See R1.
P1: missing Streamlit contract tests/CI OPEN No tests, checker, or workflow changes in the four-file follow-up. See R2.
P2: typed-view setup before launch PARTIAL Setup order, query IAM roles, and the create-all command are added; the custom-prefix flag is invalid. See R3.

R1 — P1: Preserve filter values across widget identity changes. dashboards/streamlit/app.py:221-252, particularly the _merge_widget_options() calls. On the declared minimum streamlit==1.49.0, options participate in widget identity even with a key. Appending the active values keeps them available but does not restore the selection when Streamlit registers the replacement widget.

Actual AppTest reproduction: select agent alpha, user u1, event LLM_RESPONSE, and session s1; submit; refresh options so those values disappear. All four returned selections and the resulting BigQuery array parameters become ['___ALL___'], with no explicit clear. The same reset occurs when an unselected option is merely added, or the list is reordered. Separately, enter custom-agent, custom-user, and custom-session and click Apply: merging these newly entered values changes the widget identities immediately, and all three submitted filters become ALL. These probes exercise the actual sidebar and query-parameter builder, not just the helper in isolation. Preserve submitted selections independently of widget recreation and test this through Apply and subsequent refreshes on the supported minimum version. The 1.49 custom-input contract permits new values without inserting them into the options list.

R2 — P1: Land the promised Streamlit contract coverage and run it in CI. There are still no Streamlit tests or anti-drift checks in this PR. .github/workflows/grafana-sync-check.yml:20-31 only watches Grafana/checker paths; general CI installs .[dev], which does not include the Streamlit extra. Add the acknowledged panel/filter mappings, whole-session and missing-data cases, bound parameters, real-job byte caps, billing/cache accounting, and widget retention/custom-ID regressions, with Streamlit dependencies and triggers. The reproduced R1 failure is exactly what the current passing Grafana tests do not exercise.

R3 — P2: Use the supported custom-prefix CLI flag. dashboards/streamlit/README.md:58 names --view-prefix, but views create-all exposes --prefix (src/bigquery_agent_analytics/cli.py:1848). CLI parsing of the documented flag exits 2 with “No such option”; the same invocation with --prefix custom_ reaches the mocked view manager and exits 0. Change the flag and keep BQ_VIEW_PREFIX aligned with it.

Delta assessment: no independent additional P0/P1 found beyond these unresolved contracts. The newly enabled custom-input path's immediate silent-ALL behavior is included in R1, not counted again. The billing fix is credited as closed.

Pairing with #481: it is still OPEN at 47620b8f05926568ba4f94968a248f6935f3fd79; Grafana has not moved in #480. Current paths resolve. Combining the branches has a verified .gitignore three-way conflict. Whichever lands second must preserve both integrations' ignore rules and reconcile the legacy-path protection identified in #481. After the move, update dashboards/README.md:10 to grafana/README.md, plus app.py:243, queries.py:86, and models.py:17 to the new repository paths; target the new locations in anti-drift coverage. This remains merge-order coordination, not an extra current-path finding.

Validation: 106 existing Grafana-sync/local-runner/typed-view tests passed; standalone checker verified 37 Grafana queries. All 15 Streamlit builders parsed as single read-only queries; synthetic cap/cache/billing probes passed; four-tab AppTest smoke passed with Streamlit 1.49.0 / Plotly 6.0.0. Four refresh scenarios and custom-ID submission reproduce R1. No live BigQuery jobs or source implementation changes. No undraft or merge; Haiyuan remains the gate.

@caohy1988

Copy link
Copy Markdown
Collaborator

@whuang602 Astra re-review comments for HEAD 11b2624 (also filed as REQUEST_CHANGES):

REVIEW_VERDICT=REQUEST_CHANGES
P0=0 / P1=2 / P2=1

Astra re-review of exact HEAD 11b2624560615d3b6dedc0c2097e9155e5b7ef8e, including the complete delta from 08d42ac55e3dd8641d16502f0c1aaa24b01296e0. Counts are deduplicated by root cause: the two prior filter findings now share one demonstrated widget-state defect. The PR remains OPEN + DRAFT.

Prior finding Disposition at this HEAD Evidence
P0: processed bytes labeled billed CLOSED Actual fetch() -> scan_log -> footer path reports 10.0 MB billed (1.0 KB processed) for a synthetic job with those distinct statistics. Repeated local-cache reads add zero bytes; a BigQuery cache hit reports zero billed bytes.
P1: expired selections silently become ALL PARTIAL; blocker remains Explicit pruning is removed and selected values are added to options, but real Streamlit 1.49.0 widgets still reset their selections when the options change. See R1.
P1: capped-out exact IDs unreachable PARTIAL; blocker remains Agent/User/Session now enable custom input, but entering a new ID and clicking Apply resets it to ALL. See R1.
P1: missing Streamlit contract tests/CI OPEN No tests, checker, or workflow changes in the four-file follow-up. See R2.
P2: typed-view setup before launch PARTIAL Setup order, query IAM roles, and the create-all command are added; the custom-prefix flag is invalid. See R3.

R1 — P1: Preserve filter values across widget identity changes. dashboards/streamlit/app.py:221-252, particularly the _merge_widget_options() calls. On the declared minimum streamlit==1.49.0, options participate in widget identity even with a key. Appending the active values keeps them available but does not restore the selection when Streamlit registers the replacement widget.

Actual AppTest reproduction: select agent alpha, user u1, event LLM_RESPONSE, and session s1; submit; refresh options so those values disappear. All four returned selections and the resulting BigQuery array parameters become ['___ALL___'], with no explicit clear. The same reset occurs when an unselected option is merely added, or the list is reordered. Separately, enter custom-agent, custom-user, and custom-session and click Apply: merging these newly entered values changes the widget identities immediately, and all three submitted filters become ALL. These probes exercise the actual sidebar and query-parameter builder, not just the helper in isolation. Preserve submitted selections independently of widget recreation and test this through Apply and subsequent refreshes on the supported minimum version. The 1.49 custom-input contract permits new values without inserting them into the options list.

R2 — P1: Land the promised Streamlit contract coverage and run it in CI. There are still no Streamlit tests or anti-drift checks in this PR. .github/workflows/grafana-sync-check.yml:20-31 only watches Grafana/checker paths; general CI installs .[dev], which does not include the Streamlit extra. Add the acknowledged panel/filter mappings, whole-session and missing-data cases, bound parameters, real-job byte caps, billing/cache accounting, and widget retention/custom-ID regressions, with Streamlit dependencies and triggers. The reproduced R1 failure is exactly what the current passing Grafana tests do not exercise.

R3 — P2: Use the supported custom-prefix CLI flag. dashboards/streamlit/README.md:58 names --view-prefix, but views create-all exposes --prefix (src/bigquery_agent_analytics/cli.py:1848). CLI parsing of the documented flag exits 2 with “No such option”; the same invocation with --prefix custom_ reaches the mocked view manager and exits 0. Change the flag and keep BQ_VIEW_PREFIX aligned with it.

Delta assessment: no independent additional P0/P1 found beyond these unresolved contracts. The newly enabled custom-input path's immediate silent-ALL behavior is included in R1, not counted again. The billing fix is credited as closed.

Pairing with #481: it is still OPEN at 47620b8f05926568ba4f94968a248f6935f3fd79; Grafana has not moved in #480. Current paths resolve. Combining the branches has a verified .gitignore three-way conflict. Whichever lands second must preserve both integrations' ignore rules and reconcile the legacy-path protection identified in #481. After the move, update dashboards/README.md:10 to grafana/README.md, plus app.py:243, queries.py:86, and models.py:17 to the new repository paths; target the new locations in anti-drift coverage. This remains merge-order coordination, not an extra current-path finding.

Validation: 106 existing Grafana-sync/local-runner/typed-view tests passed; standalone checker verified 37 Grafana queries. All 15 Streamlit builders parsed as single read-only queries; synthetic cap/cache/billing probes passed; four-tab AppTest smoke passed with Streamlit 1.49.0 / Plotly 6.0.0. Four refresh scenarios and custom-ID submission reproduce R1. No live BigQuery jobs or source implementation changes. No undraft or merge; Haiyuan remains the gate.

Comment thread dashboards/streamlit/app.py Outdated
with st.sidebar.form("filters"):
agents = st.multiselect(
"Agent",
options=_merge_widget_options("flt_agent", options.get("agent", [])),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — R1: Preserve filter values across widget identity changes

_merge_widget_options() keeps selected values in the options list, but on Streamlit 1.49.0 options still participate in widget identity. When options change (refresh/reorder/new custom IDs), the multiselect recreates and submitted selections become ___ALL___.

Reproduced (AppTest): select agent alpha / user u1 / event LLM_RESPONSE / session s1 → refresh so those values leave the option set → all four filters and BigQuery array params become ['___ALL___']. Same reset when entering custom IDs and clicking Apply.

Preserve submitted selections independently of widget recreation; test through Apply + refreshes on the supported minimum. 1.49 custom-input allows new values without inserting them into options.

Shared root cause for prior “expired → ALL” and “custom ID → ALL” findings.

Comment thread dashboards/streamlit/README.md Outdated
--table-id YOUR_TABLE
```

> **Note:** Views default to the `adk_` prefix. If you specify a custom prefix with `--view-prefix`, set `BQ_VIEW_PREFIX` in `.env` (or in the dashboard sidebar) to match.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — R3: wrong CLI flag for custom prefix

Docs name --view-prefix, but views create-all exposes --prefix (cli.py). --view-prefix exits 2 (“No such option”); --prefix custom_ works. Please fix the documented flag and keep BQ_VIEW_PREFIX aligned.

Comment thread pyproject.toml
"pandas>=2.0.0",
"python-dotenv>=1.0.0",
]
streamlit = [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — R2: Streamlit contract tests + CI still missing

No Streamlit tests/checker/workflow landed in this follow-up. Existing Grafana sync CI only watches grafana/** + the Grafana checker (not in this PR's path set), and general CI installs .[dev] without the Streamlit extra.

Please add the acknowledged contracts (panel/filter mappings, whole-session + missing-data, bound params, byte caps, billing/cache, widget retention / custom-ID regressions for R1) and wire CI so Streamlit path changes run them with the Streamlit deps.

@caohy1988

Copy link
Copy Markdown
Collaborator

Rebase note after #481 merge (PE)

#481 (dashboards/grafana/ move) landed on main first, so this draft is now CONFLICTING.

Conflict (only file)

.gitignore three-way:

Side Lines
This PR (11b2624) dashboards/**/*.json
main post-#481 dashboards/grafana/*.json (+ dashboards/grafana/datasource.yaml, dashboards/grafana/.local/)

Recommended resolution: take main’s scoped dashboards/grafana/* ignores (do not keep dashboards/**/*.json — that would also ignore any committed Streamlit JSON under dashboards/streamlit/ if added later). Keep the legacy grafana/*.json lines for older checkouts.

Also ensure these post-#481 lines from main remain:

dashboards/grafana/datasource.yaml
dashboards/grafana/.local/
dashboards/grafana/*.json

Path leftovers still on this branch (from prior Astra review)

After resolving .gitignore, please also refresh Streamlit doc pointers that still say bare grafana/...:

  • dashboards/README.md → Grafana guide still links ../grafana/README.md (should be grafana/README.md under dashboards/ or ./grafana/README.md)
  • dashboards/streamlit/models.py, queries.py, app.py comments still cite grafana/queries... — prefer dashboards/grafana/queries... for clarity

CI path filters in .github/workflows/grafana-sync-check.yml already look updated to dashboards/grafana/** on the merge tree.

Suggested author steps

git fetch upstream main
git rebase upstream/main   # or merge
# resolve .gitignore as above
# fix the three Streamlit/README path strings
git push --force-with-lease   # if rebasing

Happy to re-review once the conflict is cleared. #481 is already on main @ a4b9464.

Python-native alternative to the Grafana dashboard, backed by the same typed views and filter semantics. Includes scan-cap guardrails and query caching. Intentionally leaving existing Streamlit content intact.
…parity

- Resolve billed vs processed bytes disclaimers
- Preserve active filter selections and enable exact-ID entry for truncated options
- Document typed-view prerequisites and IAM roles before launch instructions
- Add query anti-drift sync checker and comprehensive contract test suites
@whuang602
whuang602 marked this pull request as ready for review September 10, 2026 04:26
@whuang602

Copy link
Copy Markdown
Contributor Author

Summary of status:

  • Rebased & resolved merge conflict
  • Anti-drift check & CI workflow implemented (R2)
  • Tests for streamlit app & anti-drift check script (R2)
  • Addressed R1 & R3
  • Addressed minor errors and code polish

@caohy1988 caohy1988 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW_VERDICT: CHANGES
P0=0 / P1=2 / P2=2

Re-reviewed exact HEAD 9c71f08d0923e06ddb834e81f2e05494a76da545, including the follow-up from 11b2624 and the full Streamlit addition against rebased main. REQUEST_CHANGES.

Prior finding Disposition Evidence at this HEAD
R1 P1: filter values lost across widget identity changes PARTIAL Applied filters now survive option retirement, addition, reordering, and an empty refresh, including subsequent Apply. First custom-ID Apply still submits ALL; changing/clearing an existing selection on the next Apply is also ignored. Details below.
R2 P1: missing Streamlit contract tests/CI PARTIAL A dedicated workflow, 14-query SQL sync checker, mutation checks and dashboard unit tests are present. Runtime widget/cache contracts are mocked away, including the reproduced R1 failures.
R3 P2: README --view-prefix CLOSED dashboards/streamlit/README.md:58 now uses --prefix, matching views_create_all in src/bigquery_agent_analytics/cli.py:1848.
Prior billing P0: processed bytes labeled billed CLOSED Successful-job path reports 10.0 MB billed (1.0 KB processed) from distinct statistics; repeated local-cache reads add zero. The legacy tuple fallbacks are removed. R5 below is a separate exception-path defect.

R1 — P1 (confidence 10/10): Preserve the newly submitted values before widget recreation. dashboards/streamlit/app.py:233–267,296–303. The new default=_default_for(applied.agents) and equivalent arguments use the previous applied filters, while _seed_options() adds pending custom values before widget registration. On supported Streamlit 1.49.0, both options and defaults participate in identity.

Real AppTest, production sidebar and BigQuery parameter builder:

  • From ALL, enter custom-agent, custom-user, custom-session and click Apply once: all three submitted arrays are ['___ALL___']; the custom values are in the option lists but unselected.
  • Apply alpha/u1/LLM_RESPONSE/s1, then change to existing beta/u2/TOOL_END/s2 and click Apply again: all four arrays retain the first values. Clearing them on that next Apply likewise retains the first values. The changed defaults recreate the widgets before the pending edit is committed.

The applied-state anchor fixes refresh retention, but it must also preserve the current form submission before re-registration can overwrite it. Regress custom first-submit, successive edits and clearing through actual widget-to-query behavior. The 1.49 custom-input contract returns new values without adding them to the option menu.

R2 — P1 (confidence 10/10): Exercise real Streamlit behavior in the new CI tests. tests/test_dashboards_streamlit_app.py:37–44,153–160,1153–1195. Streamlit is unconditionally replaced with a mock, cache decorators become passthrough functions, and the sidebar test replaces multiselect with return state.get(key, []). Installing Streamlit in the workflow therefore does not exercise widget identity, reruns, or actual cache keys. As an independent mutation check, removing all four default= repair arguments in memory still passes all five filter-retention/custom-ID tests. Add real AppTest regressions on the supported minimum, asserting bound arrays after Apply/refresh; retain real cache-key variation coverage. Credit the new SQL mapping/checker and CI wiring as delivered.

R4 — new P2 (confidence 10/10): Keep the inspected trace selected when recent sessions refresh. dashboards/streamlit/app.py:605: chosen = st.selectbox("Session", options=ids, index=0). Real AppTest of row_sessions: select s2 from ['s1','s2']; refresh to ['s3','s1','s2']; the picker and actual bound session_ids silently switch to ['s3'] although s2 remains available. A newly arriving session can switch the trace being investigated. Preserve the chosen session across option changes while it remains eligible, and make any necessary reset explicit. This is the separate trace selector, not the shared sidebar filters.

R5 — new P2 (confidence 10/10): Retain billing statistics after result-download failure. dashboards/streamlit/queries.py:824–830 returns bytes_processed=0, bytes_billed=0 for every exception, including failure inside job.to_dataframe() after successful query execution. An offline probe using an actual SDK QueryJob with synthetic DONE/no-error metadata (1,024 processed; 10,485,760 billed), and an injected row-download ServiceUnavailable, produces “1 queries this run · 0 B billed (0 B processed)” through fetch → scan_log → footer. Preserve known completed-job statistics on materialization failure; show unknown when statistics are unavailable. The real-job cap remains set. R4/R5 are newly identified defects in the original addition, not claims that the latest commits introduced them.

CI evidence: the six green check results are five GitHub Actions security-scan jobs plus CLA. At this exact SHA, general CI and Streamlit query sync check both report action_required; neither establishes passing application CI.

Validation: 142 targeted dashboard/Streamlit-sync/Grafana-sync/local-runner tests passed locally; standalone checkers verified 14 Streamlit and 37 Grafana queries. All 15 Streamlit builders parsed as single read-only queries. Bound-parameter, real-job cap, cache-scope and successful billing probes passed; four-tab AppTest smoke passed (8 metrics). Pyink 25.12.0, isort and diff whitespace checks passed for the relevant changed files. Runtime probes used Streamlit 1.49.0 / Plotly 6.0.0 / BigQuery SDK 3.45.0. Synthetic data only; no live BigQuery jobs, source fixes, merge or undraft. #481 is now in the base; the migrated Grafana links/checker paths resolve.

@caohy1988 caohy1988 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW_VERDICT: CHANGES
P0=0 / P1=2 / P2=2

Independent review of exact HEAD 9c71f08d0923e06ddb834e81f2e05494a76da545. I did not take prior review claims at face value: I reproduced every runtime finding below from scratch with real Streamlit AppTest (no mocks of streamlit) in throwaway venvs at both the pinned minimum 1.49.0 and latest 1.63.0, plus unit-level probes with synthetic BigQuery job metadata. Where my repro diverges from the prior review, I say so.

P1-1 — Filter Apply is broken on the declared minimum Streamlit 1.49.0 (dashboards/streamlit/app.py:234-303, pyproject.toml:63).
AppTest driving the real sidebar_filters on streamlit 1.49.0:

  • From ALL, type custom IDs (custom-agent / custom-user / custom-session via accept_new_options) and click Apply once → all three bind ('___ALL___',); the custom values appear in the option lists but are not submitted.
  • Apply alpha/u1/LLM_RESPONSE/s1 (binds correctly), then change to beta/u2/TOOL_END/s2 and Apply again → all four arrays still bind the first values.
  • Mechanism: _seed_options merges the in-progress value into options, and default=_default_for(applied) changes with each apply; on 1.49.0 both participate in widget identity, so the submit rerun recreates the widget and resets it to the previous applied values before the pending edit is read.
  • The same script on streamlit 1.63.0 passes every step, so this is version-dependent — but streamlit>=1.49.0 makes 1.49.0 a supported configuration. Either restructure the seeding/default so options and default are stable across the submit rerun, or raise the version floor to a release where this works.
  • One divergence from the prior review, for honesty: in my repro, clearing an existing selection and applying DID bind the ___ALL___ sentinel on 1.49.0. I could not reproduce the "clearing is ignored" sub-claim; the custom-ID and successive-edit failures reproduced exactly.
  • What works on both versions: retention of applied filters across an empty/failed options refresh (widget and applied_filters both keep the selection).

P1-2 — The new CI tests never exercise real Streamlit, and no application CI has run on this SHA (tests/test_dashboards_streamlit_app.py:37-44, .github/workflows/streamlit-sync-check.yml).

  • streamlit is in _UNCONDITIONAL_MOCKS: even though the workflow installs .[dev,streamlit], the suite replaces Streamlit with a MagicMock, turns cache_data/cache_resource into passthroughs, and swaps multiselect for return state.get(key, []) — a fake that ignores default= entirely. The P1-1 defects are interactions between default, options, and widget identity, so they are structurally invisible to this suite: the tests assert the app's own merging logic, not widget behavior. Verified locally: the full 62-test suite passes on a machine without streamlit installed at all.
  • Add real AppTest regressions (minimum supported version) that assert the bound query-parameter arrays after Apply/refresh, and at least one test that exercises real st.cache_data key variation.
  • CI state verified via the Actions API at this SHA: the CI workflow and the new Streamlit query sync check workflow both report action_required — the only green checks are the zizmor security scans and CLA. So no test or sync-check signal exists for this HEAD yet.
  • Credit where due: the 14-query SQL sync checker is real and passes locally (All 14 Streamlit dashboard queries match canonical Grafana SQL), and the SQL-sync CI wiring itself is sound.

P2-1 — Session selectbox refresh silently switches the trace under investigation (dashboards/streamlit/app.py:605).
Real AppTest of row_sessions (reproduced on both 1.49.0 and 1.63.0, so not version-dependent): select s2 from [s1, s2] → trace query binds ('s2',); options refresh to ['s3', 's1', 's2'] (a new session arrives) → the picker and the bound session_ids silently switch to ('s3',) even though s2 is still eligible. Preserve the chosen session while it remains in the options; reset explicitly only when it leaves.

P2-2 — Billing statistics are dropped on result-download failure (dashboards/streamlit/queries.py:771-786,824-830).
Probe with a synthetic completed job (total_bytes_processed=1024, total_bytes_billed=10,485,760, cache_hit=False) whose to_dataframe() raises: run_query returns bytes_processed=0, bytes_billed=0, so fetch → scan_log → footer renders "1 queries this run · 0 B billed (0 B processed)" for a query that actually billed 10 MB — contradicting the footer's "what this rerun actually scanned" contract. On the materialization-failure path the job object and its statistics exist; report them (or say "unknown") instead of zero.

Verified as solid (no action needed):

  • Injection posture: table/view identifiers are regex-validated before interpolation, filter values go out as ArrayQueryParameters, LIMITs are int-cast, timestamp literals are built from owned datetimes.
  • Success-path billing now reports distinct billed vs processed stats correctly (10.0 MB billed / 1.0 KB processed for a synthetic job), and the run-id dedup keeps cached reruns from double-counting.
  • Dry-run preflight + maximum_bytes_billed cap is set on the real job; guardrail message path works.
  • 62 mock-based tests and the sync checker pass locally; README/.env.sample/.gitignore (SA JSON keys) are consistent.

Verdict: REQUEST_CHANGES — two P1s (broken Apply on the supported minimum Streamlit; mock-only widget/cache test coverage with no CI signal on this SHA) and two P2s (trace-selectbox hijack on refresh; billing stats lost on download failure), each reproduced independently against real Streamlit.

@whuang602
whuang602 marked this pull request as draft September 11, 2026 14:18

@caohy1988 caohy1988 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fresh review of exact head 295542a64e59eafe7a76b3a251388ff14057f6f3.

Requesting changes for one P1 compatibility regression and one P2 cost-reporting issue. I also left one non-blocking P3 CI-quality comment.

Prior-review disposition:

  • The active custom-value, successive-apply, and clear-selection cases now pass on current Streamlit.
  • R3 (--prefix) and R4 (session selector retention) are fixed.
  • The known-statistics half of R5 is fixed: a completed job whose stats are available preserves them after result-download failure.
  • R1 is not fully closed at the declared Streamlit floor, and R5 still conflates unavailable statistics with zero.

Verification:

  • Python 3.13 / Streamlit 1.63.0: all 102 focused tests pass.
  • Python 3.13 / Streamlit 1.49.0 (the declared minimum): 101 pass, 1 fails. The failing committed AppTest is test_filter_widget_options_change_preserves_applied_filters.
  • Manual AppTest continuation on 1.49.0: after option refresh, the widget value becomes empty while applied_filters still contains agent-a; the next Apply changes it to ___ALL___.
  • All 14 Streamlit queries pass the canonical Grafana SQL anti-drift checker.
  • compileall, git diff --check, and isort --check-only pass. pyink --check reports dashboards/streamlit/charts.py would be reformatted (missing final newline).
  • The PR's CI and Streamlit workflows are currently action_required for this fork, so there is no application CI result on this head.

I did not run a live BigQuery integration query in this pass.

Comment thread pyproject.toml
"python-dotenv>=1.0.0",
]
streamlit = [
"streamlit>=1.49.0",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: the declared Streamlit minimum silently broadens refreshed filters

This lower bound is not compatible with the state-retention implementation at this head. With Python 3.13 and Streamlit 1.49.0, the branch's own test_filter_widget_options_change_preserves_applied_filters fails: after agent-a disappears from the refreshed BigQuery options, the code re-adds it to the option list, but the widget value is [], not ['agent-a']. Continuing the real AppTest and clicking Apply changes applied_filters.agents from ('agent-a',) to ('___ALL___',), so every panel is silently broadened.

The same 102-test set passes on Streamlit 1.63.0, so current CI resolution masks a broken advertised floor. Please either make the widget/session-state pattern work on 1.49.0 or raise the minimum to the first version you verify, and add a minimum-dependency CI cell so this contract stays exercised.

bytes_billed = int(exc.bytes_billed)
cache_hit = bool(exc.cache_hit)
else:
bytes_processed, bytes_billed, cache_hit = 0, 0, False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: unavailable billing statistics are still presented as zero

This fallback makes an unknown outcome indistinguishable from a proven zero. A query can be accepted and billed before to_dataframe() or the metadata reload fails; this path then records (0, 0, False), fetch() appends it to the scan log, and the footer describes the total as what the rerun actually billed/processed.

There is a second instance in _extract_job_stats: if total_bytes_processed is populated but total_bytes_billed is None, it does not reload and returns billed bytes as zero. An exact-head probe produced (2048, 0, False) with no reload call.

Please preserve unknown explicitly (optional fields or a stats_known flag), attempt hydration whenever a required statistic is missing, and have the footer identify failed queries with unavailable cost data rather than summing zero.

run: pip install -e ".[dev,streamlit]"

- name: Validate Streamlit queries match canonical Grafana SQL
run: python3 scripts/check_streamlit_queries_sync.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 (non-blocking): dashboard Python bypasses the formatting gate

This workflow runs the SQL checker and tests, but no formatter. The repository's autoformat.sh and the main CI dirty-diff assertion cover only src/, tests/, examples/, and scripts/, so the new production Python under dashboards/streamlit/ is outside both paths. The gap is observable on this head: pyink --check dashboards/streamlit/charts.py wants to add the missing final newline.

Please add dashboards/streamlit to the shared format command and CI diff list, or run pyink --check and isort --check-only for it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants