From ca0ebc91c37a1f6100787f1535aed0f9b63c000a Mon Sep 17 00:00:00 2001 From: gromdimon Date: Fri, 3 Nov 2023 19:33:24 +0100 Subject: [PATCH] feat: implementation of matomo settings --- backend/app/api/internal/api.py | 12 +++++++++ backend/app/core/config.py | 5 ++++ backend/tests/test_main.py | 20 +++++++++++++++ frontend/package-lock.json | 10 ++++++++ frontend/package.json | 1 + frontend/src/api/__tests__/settings.spec.ts | 23 +++++++++++++++++ frontend/src/api/settings.ts | 20 +++++++++++++++ frontend/src/main.ts | 10 +++++--- frontend/src/plugins/index.ts | 6 ++++- frontend/src/plugins/matomo.ts | 28 +++++++++++++++++++++ 10 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 frontend/src/api/__tests__/settings.spec.ts create mode 100644 frontend/src/api/settings.ts create mode 100644 frontend/src/plugins/matomo.ts diff --git a/backend/app/api/internal/api.py b/backend/app/api/internal/api.py index 33ee4a54..23e2a5b5 100644 --- a/backend/app/api/internal/api.py +++ b/backend/app/api/internal/api.py @@ -1,6 +1,7 @@ import subprocess from fastapi import APIRouter, Response +from fastapi.responses import JSONResponse from app.api.internal.endpoints import proxy, remote from app.core.config import settings @@ -20,3 +21,14 @@ async def version(): else: version = subprocess.check_output(["git", "describe", "--tags", "--dirty"]).strip() return Response(content=version) + + +@api_router.get("/frontend-settings") +@api_router.post("/frontend-settings") +async def matomo(): + """Return Frontend settings""" + frontend_settings = { + "matomo_host": settings.MATOMO_HOST, + "matomo_site_id": settings.MATOMO_SITE_ID, + } + return JSONResponse(content=frontend_settings) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 5a5b6073..c905b924 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -44,6 +44,11 @@ def assemble_reev_version(cls, v: str | None, info: ValidationInfo) -> str | Non else: return None + #: Matomo host + MATOMO_HOST: str | None = None + #: Matomo site ID + MATOMO_SITE_ID: int | None = None + # == API-related settings == #: URL prefix for internal API diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index d11b2311..8d11119a 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -33,3 +33,23 @@ async def test_favicon(client: TestClient): response = client.get("/favicon.ico") assert response.status_code == 200 assert response.headers["content-type"] == "image/vnd.microsoft.icon" + + +@pytest.mark.asyncio +async def test_frontend_settings(monkeypatch: MonkeyPatch, client: TestClient): + """Test frontend settings endpoint.""" + monkeypatch.setattr(settings, "MATOMO_HOST", "matomo.example.com") + monkeypatch.setattr(settings, "MATOMO_SITE_ID", 42) + response = client.get("/internal/frontend-settings") + assert response.status_code == 200 + assert response.json() == {"matomo_host": "matomo.example.com", "matomo_site_id": 42} + + +@pytest.mark.asyncio +async def test_frontend_settings_no_matomo(monkeypatch: MonkeyPatch, client: TestClient): + """Test frontend settings endpoint with no matomo.""" + monkeypatch.setattr(settings, "MATOMO_HOST", None) + monkeypatch.setattr(settings, "MATOMO_SITE_ID", None) + response = client.get("/internal/frontend-settings") + assert response.status_code == 200 + assert response.json() == {"matomo_host": None, "matomo_site_id": None} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3cd7a29c..83ceac92 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "vega": "^5.25.0", "vega-embed": "^6.22.2", "vue": "^3.3.4", + "vue-matomo": "^4.2.0", "vue-router": "^4.2.5", "vue3-easy-data-table": "^1.5.47", "vuetify": "^3.3.19" @@ -11690,6 +11691,15 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/vue-matomo": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/vue-matomo/-/vue-matomo-4.2.0.tgz", + "integrity": "sha512-m5hCw7LH3wPDcERaF4sp/ojR9sEx7Rl8TpOyH/4jjQxMF2DuY/q5pO+i9o5Dx+BXLSa9+IQ0qhAbWYRyESQXmA==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/vue-router": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index cd7e776a..28270381 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "vega": "^5.25.0", "vega-embed": "^6.22.2", "vue": "^3.3.4", + "vue-matomo": "^4.2.0", "vue-router": "^4.2.5", "vue3-easy-data-table": "^1.5.47", "vuetify": "^3.3.19" diff --git a/frontend/src/api/__tests__/settings.spec.ts b/frontend/src/api/__tests__/settings.spec.ts new file mode 100644 index 00000000..6351e854 --- /dev/null +++ b/frontend/src/api/__tests__/settings.spec.ts @@ -0,0 +1,23 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import createFetchMock from 'vitest-fetch-mock' + +import { SettingsClient } from '@/api/settings' + +const fetchMocker = createFetchMock(vi) + +describe.concurrent('Settings Client', () => { + beforeEach(() => { + fetchMocker.enableMocks() + fetchMocker.resetMocks() + }) + + it('fetches version info correctly', async () => { + fetchMocker.mockResponseOnce( + JSON.stringify({ matomo_host: 'https://matomo.example.com/', matomo_site_id: '1' }) + ) + + const client = new SettingsClient() + const result = await client.fetchFrontendSettings() + expect(result).toEqual({ matomo_host: 'https://matomo.example.com/', matomo_site_id: '1' }) + }) +}) diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts new file mode 100644 index 00000000..7b622f15 --- /dev/null +++ b/frontend/src/api/settings.ts @@ -0,0 +1,20 @@ +import { API_INTERNAL_BASE_PREFIX } from '@/api/common' + +const API_BASE_URL = API_INTERNAL_BASE_PREFIX + +export class SettingsClient { + private apiBaseUrl: string + private csrfToken: string | null + + constructor(apiBaseUrl?: string, csrfToken?: string) { + this.apiBaseUrl = apiBaseUrl ?? API_BASE_URL + this.csrfToken = csrfToken ?? null + } + + async fetchFrontendSettings(): Promise { + const response = await fetch(`${this.apiBaseUrl}frontend-settings`, { + method: 'GET' + }) + return await response.json() + } +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 569c2d08..f20881a3 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -12,8 +12,12 @@ import { registerPlugins } from '@/plugins' import App from './App.vue' -const app = createApp(App) +async function bootstrap() { + const app = createApp(App) -registerPlugins(app) + await registerPlugins(app) -app.mount('#app') + app.mount('#app') +} + +bootstrap() diff --git a/frontend/src/plugins/index.ts b/frontend/src/plugins/index.ts index a02104cf..378c42fa 100644 --- a/frontend/src/plugins/index.ts +++ b/frontend/src/plugins/index.ts @@ -9,8 +9,12 @@ import type { App } from 'vue' import router from '../router' import pinia from '../stores' +import setupMatomo from './matomo' import vuetify from './vuetify' -export function registerPlugins(app: App) { +export async function registerPlugins(app: App) { app.use(vuetify).use(router).use(pinia) + + // Initialize Matomo + await setupMatomo(app, router) } diff --git a/frontend/src/plugins/matomo.ts b/frontend/src/plugins/matomo.ts new file mode 100644 index 00000000..f88be84a --- /dev/null +++ b/frontend/src/plugins/matomo.ts @@ -0,0 +1,28 @@ +/** + * plugins/matomo.ts + * + * Matomo documentation: https://developer.matomo.org/guides/spa-tracking + */ +import { type App } from 'vue' +import VueMatomo from 'vue-matomo' + +import { SettingsClient } from '@/api/settings' + +async function setupMatomo(app: App, router: any) { + try { + const client = new SettingsClient() + const response = await client.fetchFrontendSettings() + + app.use(VueMatomo, { + host: response['matomo_host'], + siteId: response['matomo_site_id'], + router: router, + requireConsent: true, + disableCookies: true + }) + } catch (error) { + console.error('Failed to initialize Matomo:', error) + } +} + +export default setupMatomo