diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml index 9563b1cbf4..d09c3a77c2 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -10,6 +10,25 @@ servers: - url: https://demo.frigate.video/api - url: http://localhost:5001/api paths: + /analytics/preview: + get: + tags: + - Analytics + summary: Get Analytics Preview + description: |- + **Access:** Admin role required. + + Get the analytics report Frigate would send next, without sending it. + operationId: get_analytics_preview_analytics_preview_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin /auth/first_time_login: get: tags: diff --git a/frigate/api/analytics.py b/frigate/api/analytics.py new file mode 100644 index 0000000000..a05daca436 --- /dev/null +++ b/frigate/api/analytics.py @@ -0,0 +1,25 @@ +"""Analytics APIs.""" + +import logging + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse + +from frigate.analytics.report import preview_report +from frigate.api.auth import require_role +from frigate.api.defs.tags import Tags + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.analytics]) + + +@router.get("/analytics/preview", dependencies=[Depends(require_role(["admin"]))]) +def get_analytics_preview(request: Request) -> JSONResponse: + """Get the analytics report Frigate would send next, without sending it.""" + report = preview_report( + request.app.frigate_config, + request.app.stats_emitter, + request.app.notice_registry, + ) + return JSONResponse(content=report.model_dump(mode="json")) diff --git a/frigate/api/defs/tags.py b/frigate/api/defs/tags.py index 9ce733329c..d2d2e2af7a 100644 --- a/frigate/api/defs/tags.py +++ b/frigate/api/defs/tags.py @@ -2,6 +2,7 @@ from enum import Enum class Tags(Enum): + analytics = "Analytics" app = "App" auth = "Auth" camera = "Camera" diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 3b712dae6d..db64bdb893 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -12,8 +12,8 @@ from slowapi.middleware import SlowAPIMiddleware from starlette_context import middleware, plugins from starlette_context.plugins import Plugin -from frigate.api import app as main_app from frigate.api import ( + analytics, auth, camera, chat, @@ -30,6 +30,7 @@ from frigate.api import ( record, review, ) +from frigate.api import app as main_app from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default from frigate.comms.dispatcher import Dispatcher from frigate.comms.event_metadata_updater import ( @@ -140,6 +141,7 @@ def create_fastapi_app( # Routes # Order of include_router matters: https://fastapi.tiangolo.com/tutorial/path-params/#order-matters + app.include_router(analytics.router) app.include_router(auth.router) app.include_router(camera.router) app.include_router(chat.router) diff --git a/frigate/test/http_api/test_http_analytics.py b/frigate/test/http_api/test_http_analytics.py new file mode 100644 index 0000000000..84e540b15a --- /dev/null +++ b/frigate/test/http_api/test_http_analytics.py @@ -0,0 +1,49 @@ +"""Tests for the analytics preview API.""" + +from unittest.mock import Mock, patch + +from frigate.analytics import report +from frigate.analytics.collectors import hardware +from frigate.models import Notice, NoticeStats, User +from frigate.notices.registry import NoticeRegistry +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpAnalytics(BaseTestHttp): + def setUp(self): + super().setUp([Notice, NoticeStats, User]) + stats = Mock() + stats.get_latest_stats.return_value = self.test_stats + self.app = self.create_app(stats=stats, notice_registry=NoticeRegistry()) + + for target, name, value in ( + (report, "load_state", None), + (hardware.hardware_prober, "probe", []), + (hardware, "hwaccel_options", ("", [])), + ): + patcher = patch.object(target, name, return_value=value) + patcher.start() + self.addCleanup(patcher.stop) + + def test_preview_returns_the_report_without_sending_it(self): + with ( + patch("frigate.analytics.transport.requests.post") as post, + AuthTestClient(self.app) as client, + ): + response = client.get("/analytics/preview") + + self.assertEqual(response.status_code, 200) + body = response.json() + self.assertEqual(body["install_id"], report.PREVIEW_INSTALL_ID) + self.assertEqual(body["schema_version"], 1) + self.assertEqual(body["cameras"]["total"], 1) + post.assert_not_called() + + def test_preview_is_admin_only(self): + with AuthTestClient(self.app) as client: + response = client.get( + "/analytics/preview", + headers={"remote-user": "viewer", "remote-role": "viewer"}, + ) + + self.assertEqual(response.status_code, 403) diff --git a/generate_api_auth_spec.py b/generate_api_auth_spec.py index 3193836927..58d19ba6ff 100644 --- a/generate_api_auth_spec.py +++ b/generate_api_auth_spec.py @@ -47,8 +47,8 @@ from fastapi.routing import APIRoute from ruamel.yaml import YAML from ruamel.yaml.scalarstring import LiteralScalarString -from frigate.api import app as main_app from frigate.api import ( + analytics, auth, camera, chat, @@ -65,6 +65,7 @@ from frigate.api import ( record, review, ) +from frigate.api import app as main_app from frigate.api.auth import require_admin_by_default logging.basicConfig(level=logging.INFO, format="%(message)s") @@ -145,6 +146,7 @@ def build_app() -> FastAPI: """ app = FastAPI() routers = [ + analytics.router, auth.router, camera.router, chat.router,