Show the analytics preview in telemetry settings

This commit is contained in:
Josh Hawkins
2026-10-29 16:01:45 -05:00
parent 3fe51fbbe2
commit a50a8bace5
10 changed files with 137 additions and 4 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
/**
* Telemetry settings: the analytics preview shows the report Frigate would
* send, fetched only when opened.
*/
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { test, expect } from "../../fixtures/frigate-test";
const __dirname = dirname(fileURLToPath(import.meta.url));
const CONFIG_SCHEMA = JSON.parse(
readFileSync(
resolve(__dirname, "../../fixtures/mock-data/config-schema.json"),
"utf-8",
),
);
const PREVIEW = {
schema_version: 1,
install_id: "0".repeat(32),
install: { version: "0.19.0-test" },
};
test.describe("telemetry settings: analytics preview @medium", () => {
test("shows the report on demand", async ({ frigateApp }) => {
let requests = 0;
const page = frigateApp.page;
await page.route("**/api/config/schema.json", (route) =>
route.fulfill({ json: CONFIG_SCHEMA }),
);
await page.route("**/api/config/raw_paths", (route) =>
route.fulfill({ json: { telemetry: {} } }),
);
await page.route("**/api/analytics/preview", (route) => {
requests += 1;
return route.fulfill({ json: PREVIEW });
});
await frigateApp.goto("/settings?page=systemTelemetry");
const button = page.getByRole("button", { name: "Preview the report" });
await expect(button).toBeVisible();
expect(requests).toBe(0);
await button.click();
await expect(page.getByTestId("analytics-preview")).toContainText(
'"schema_version": 1',
);
expect(requests).toBe(1);
});
});
+4
View File
@@ -222,6 +222,10 @@
"telemetry": {
"label": "Telemetry",
"description": "System telemetry and stats options including GPU and network bandwidth monitoring.",
"analytics": {
"label": "Share anonymous analytics",
"description": "Send one anonymous usage report a day to help the Frigate maintainers decide what to support. Nothing is sent until this is on."
},
"network_interfaces": {
"label": "Network interfaces",
"description": "List of network interface name prefixes to monitor for bandwidth statistics."
@@ -1,4 +1,9 @@
{
"analyticsPreview": {
"show": "Preview the report",
"desc": "Frigate sends this report once a day while sharing is on. It never includes camera names, zones, labels, or addresses.",
"error": "Unable to build the preview"
},
"documentTitle": {
"default": "Settings - Frigate",
"authentication": "Authentication Settings - Frigate",
+2 -1
View File
@@ -56,7 +56,8 @@
"object_processing_behind": "Tracked object processing could not keep up, so camera frames were dropped. Check CPU utilization, and whether CPU limits or pinning on the container or VM are leaving Frigate too little CPU",
"failed_login_one": "Failed login attempt for {{user}}",
"failed_login_other": "Failed login attempts for {{user}}",
"update_available": "Frigate {{version}} is available"
"update_available": "Frigate {{version}} is available",
"analytics_prompt": "Share anonymous usage statistics to help improve Frigate"
},
"streamPrefix": "Stream {{index}}: {{message}}",
"streamPrefixRestream": "Stream {{index}} (via go2rtc): {{message}}",
@@ -3,9 +3,17 @@ import type { SectionConfigOverrides } from "./types";
const telemetry: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/advanced/reference",
fieldDocs: {
analytics: "/configuration/advanced/analytics",
},
restartRequired: ["version_check"],
fieldOrder: ["network_interfaces", "stats", "version_check"],
fieldOrder: ["analytics", "network_interfaces", "stats", "version_check"],
advancedFields: [],
uiSchema: {
analytics: {
"ui:after": { render: "AnalyticsPreview" },
},
},
},
};
@@ -0,0 +1,56 @@
import { useState } from "react";
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
export default function AnalyticsPreview() {
const { t } = useTranslation("views/settings");
const [open, setOpen] = useState(false);
// built on demand, since building runs every collector on the server
const { data, error } = useSWR<Record<string, unknown>>(
open ? "analytics/preview" : null,
{ revalidateOnFocus: false },
);
return (
<Collapsible open={open} onOpenChange={setOpen} className="mt-2">
<CollapsibleTrigger asChild>
<Button variant="outline" size="sm" className="gap-1">
{open ? (
<LuChevronDown className="size-4" />
) : (
<LuChevronRight className="size-4" />
)}
{t("analyticsPreview.show")}
</Button>
</CollapsibleTrigger>
<div className="mt-1 text-xs text-muted-foreground">
{t("analyticsPreview.desc")}
</div>
<CollapsibleContent className="mt-2">
{error ? (
<div className="text-sm text-danger">
{t("analyticsPreview.error")}
</div>
) : data ? (
<pre
data-testid="analytics-preview"
className="max-h-96 overflow-auto rounded-md bg-secondary p-3 text-xs"
>
{JSON.stringify(data, null, 2)}
</pre>
) : (
<ActivityIndicator className="size-5" />
)}
</CollapsibleContent>
</Collapsible>
);
}
@@ -1,4 +1,5 @@
import type { ComponentType } from "react";
import AnalyticsPreview from "./AnalyticsPreview";
import SemanticSearchReindex from "./SemanticSearchReindex.tsx";
import CameraReviewStatusToggles from "./CameraReviewStatusToggles";
import ProxyRoleMap from "./ProxyRoleMap";
@@ -56,6 +57,9 @@ export const sectionRenderers: SectionRenderers = {
birdseye: {
BirdseyeCameraReorder,
},
telemetry: {
AnalyticsPreview,
},
};
export default sectionRenderers;
+1
View File
@@ -662,6 +662,7 @@ export interface FrigateConfig {
};
telemetry: {
analytics: boolean;
network_interfaces: string[];
stats: {
amd_gpu_stats: boolean;