From 593e6985fb5e464c77799f0ac22e98461b580316 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 12 Jul 2026 07:32:28 -0700 Subject: [PATCH] fix(observability): every Grafana query-variable dropdown was silently empty in production Every "query"-type template variable across every dashboard ($repo, $scope, $provider/$feature/$model) resolved to zero options in the live Grafana instance -- confirmed empirically against a running frser-sqlite-datasource via POST /api/ds/query -- because the variable's `query` object only carried `rawQueryText` (a display/round-trip field the plugin's variable-query path never actually executes), never `queryText` (the field the plugin's query builder requires). Panel targets were never affected -- they already set both fields at the top level -- which is exactly why every table/timeseries panel rendered real data while every $variable dropdown showed a red error icon in the UI, cascading into "No data"/zeroed panels wherever a panel's own WHERE clause referenced one of the broken variables. Adds a cross-dashboard regression test scanning every file in grafana/dashboards/ for this exact shape, so a future dashboard can't reintroduce it by copying the variable pattern without the fix. --- grafana/dashboards/ai-usage.json | 5 +- grafana/dashboards/github-prs.json | 1 + grafana/dashboards/maintainer-reviews.json | 2 +- ...lfhost-grafana-variable-query-text.test.ts | 59 +++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 test/unit/selfhost-grafana-variable-query-text.test.ts diff --git a/grafana/dashboards/ai-usage.json b/grafana/dashboards/ai-usage.json index 9059255d0..2f97495a7 100644 --- a/grafana/dashboards/ai-usage.json +++ b/grafana/dashboards/ai-usage.json @@ -15,7 +15,7 @@ "label": "Provider", "type": "query", "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, - "query": { "queryType": "table", "rawQueryText": "SELECT DISTINCT provider FROM ai_usage_events WHERE provider IS NOT NULL ORDER BY provider" }, + "query": { "queryType": "table", "queryText": "SELECT DISTINCT provider FROM ai_usage_events WHERE provider IS NOT NULL ORDER BY provider", "rawQueryText": "SELECT DISTINCT provider FROM ai_usage_events WHERE provider IS NOT NULL ORDER BY provider" }, "includeAll": true, "allValue": "$__all", "multi": false, @@ -28,7 +28,7 @@ "label": "Feature", "type": "query", "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, - "query": { "queryType": "table", "rawQueryText": "SELECT DISTINCT feature FROM ai_usage_events ORDER BY feature" }, + "query": { "queryType": "table", "queryText": "SELECT DISTINCT feature FROM ai_usage_events ORDER BY feature", "rawQueryText": "SELECT DISTINCT feature FROM ai_usage_events ORDER BY feature" }, "includeAll": true, "allValue": "$__all", "multi": false, @@ -43,6 +43,7 @@ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "query": { "queryType": "table", + "queryText": "SELECT DISTINCT model FROM ai_usage_events WHERE ('$provider' = '$__all' OR provider = '$provider') ORDER BY model", "rawQueryText": "SELECT DISTINCT model FROM ai_usage_events WHERE ('$provider' = '$__all' OR provider = '$provider') ORDER BY model" }, "includeAll": true, diff --git a/grafana/dashboards/github-prs.json b/grafana/dashboards/github-prs.json index 9e4c40ee0..5833ecf0c 100644 --- a/grafana/dashboards/github-prs.json +++ b/grafana/dashboards/github-prs.json @@ -18,6 +18,7 @@ "description": "Dynamic, never hardcoded: built from whichever repos are actually tracked in this self-hoster's own reporting DB. 'All repos' assumes a single-owner setup (the same assumption the dashboard's own GitHub-search-syntax scope always required, since GitHub search can't OR multiple repo: qualifiers in one query) — it widens to org: of the first tracked repo. A self-hoster tracking repos across multiple owners should pick a specific repo instead of 'All repos'.", "query": { "queryType": "table", + "queryText": "SELECT * FROM (SELECT 0 AS ord, 'All repos' AS \"__text\", (SELECT 'org:' || substr(repo, 1, instr(repo, '/') - 1) FROM review_targets LIMIT 1) AS \"__value\" UNION ALL SELECT 1 AS ord, repo AS \"__text\", 'repo:' || repo AS \"__value\" FROM (SELECT DISTINCT repo FROM review_targets ORDER BY repo)) ORDER BY ord, \"__text\"", "rawQueryText": "SELECT * FROM (SELECT 0 AS ord, 'All repos' AS \"__text\", (SELECT 'org:' || substr(repo, 1, instr(repo, '/') - 1) FROM review_targets LIMIT 1) AS \"__value\" UNION ALL SELECT 1 AS ord, repo AS \"__text\", 'repo:' || repo AS \"__value\" FROM (SELECT DISTINCT repo FROM review_targets ORDER BY repo)) ORDER BY ord, \"__text\"" }, "current": { "text": "All repos", "value": "org:JSONbored" }, diff --git a/grafana/dashboards/maintainer-reviews.json b/grafana/dashboards/maintainer-reviews.json index e698d83e7..a8892e0b0 100644 --- a/grafana/dashboards/maintainer-reviews.json +++ b/grafana/dashboards/maintainer-reviews.json @@ -16,7 +16,7 @@ "label": "Repo", "type": "query", "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, - "query": { "queryType": "table", "rawQueryText": "SELECT DISTINCT repo FROM review_targets ORDER BY repo" }, + "query": { "queryType": "table", "queryText": "SELECT DISTINCT repo FROM review_targets ORDER BY repo", "rawQueryText": "SELECT DISTINCT repo FROM review_targets ORDER BY repo" }, "includeAll": true, "allValue": "$__all", "multi": false, diff --git a/test/unit/selfhost-grafana-variable-query-text.test.ts b/test/unit/selfhost-grafana-variable-query-text.test.ts new file mode 100644 index 000000000..fb03e0851 --- /dev/null +++ b/test/unit/selfhost-grafana-variable-query-text.test.ts @@ -0,0 +1,59 @@ +import { readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +// Regression guard (2026-07 fix): every "query"-type template variable across every dashboard silently +// returned ZERO options in the live Grafana instance -- confirmed empirically against a running +// frser-sqlite-datasource via POST /api/ds/query -- because the variable's `query` object only carried +// `rawQueryText` (a display/round-trip field), never the `queryText` field the plugin actually needs to +// execute the query. Panel targets were never affected: they already set BOTH fields at the top level (see +// the `rawQueryText === queryText` check on panel targets elsewhere in this suite), which is exactly why +// every table/timeseries panel rendered real data while every $variable dropdown showed a red error icon and +// resolved to nothing -- cascading into "No data"/zeroed panels wherever a panel's WHERE clause referenced +// one of those variables. This test scans every dashboard file, not just the ones fixed today, so a future +// dashboard can never reintroduce this by copying the variable shape without the fix. +const dashboardsDir = join(process.cwd(), "grafana/dashboards"); + +type TemplateVar = { + name: string; + type: string; + datasource?: { type?: string }; + query?: { queryText?: string; rawQueryText?: string } | string; +}; + +function readDashboardFiles(): Array<{ file: string; vars: TemplateVar[] }> { + return readdirSync(dashboardsDir) + .filter((f) => f.endsWith(".json")) + .map((file) => { + const dashboard = JSON.parse(readFileSync(join(dashboardsDir, file), "utf8")) as { + templating?: { list?: TemplateVar[] }; + }; + return { file, vars: dashboard.templating?.list ?? [] }; + }); +} + +describe("Grafana dashboards: every query-type template variable actually executes (2026-07 fix)", () => { + it("every SQL-datasource query variable's `query` object carries queryText, matching rawQueryText", () => { + const dashboards = readDashboardFiles(); + expect(dashboards.length).toBeGreaterThan(3); // sanity: the scan found real dashboard files + + const violations: string[] = []; + for (const { file, vars } of dashboards) { + for (const v of vars) { + if (v.type !== "query") continue; + if (typeof v.query !== "object" || v.query === null) continue; + // Only the frser-sqlite-datasource plugin exhibits this specific missing-queryText bug (confirmed + // empirically); a Prometheus/other query variable's `query` field is a plain string, not this shape, + // and is unaffected -- skip anything that isn't this plugin. + if (v.datasource?.type !== "frser-sqlite-datasource") continue; + const { queryText, rawQueryText } = v.query; + if (!queryText) { + violations.push(`${file}: $${v.name} is missing "queryText" in its query object (rawQueryText alone silently returns zero rows)`); + } else if (queryText !== rawQueryText) { + violations.push(`${file}: $${v.name}'s queryText and rawQueryText have diverged ("${queryText}" vs "${rawQueryText}")`); + } + } + } + expect(violations).toEqual([]); + }); +});