Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions grafana/dashboards/ai-usage.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions grafana/dashboards/github-prs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:<owner> 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" },
Expand Down
2 changes: 1 addition & 1 deletion grafana/dashboards/maintainer-reviews.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
59 changes: 59 additions & 0 deletions test/unit/selfhost-grafana-variable-query-text.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
Loading