Voeg self-hosted Umami analytics + UptimeRobot uptime dashboard toe
Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Nav } from './Nav';
|
||||
import { Footer } from './Footer';
|
||||
import { UmamiAnalytics } from './UmamiAnalytics';
|
||||
|
||||
export function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="font-sans min-h-screen bg-bg text-text">
|
||||
<UmamiAnalytics />
|
||||
<Nav />
|
||||
{children}
|
||||
<Footer />
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SCRIPT_ELEMENT_ID = 'umami-analytics-script';
|
||||
|
||||
/**
|
||||
* Injects the self-hosted Umami tracking script (see
|
||||
* aidlc-docs/features/react-frontend/operations/monitoring/umami-setup.md) when both
|
||||
* VITE_UMAMI_SCRIPT_URL and VITE_UMAMI_WEBSITE_ID are configured at build time.
|
||||
*
|
||||
* Silently does nothing if either variable is missing (e.g. local development, or a
|
||||
* build for which the Umami instance/website has not been set up yet) — safe default,
|
||||
* no crash on missing config, mirroring the existing Sentry DSN pattern.
|
||||
*
|
||||
* Skipped during local development (`pnpm dev`) by default so local testing does not
|
||||
* pollute production/test visitor analytics.
|
||||
*/
|
||||
export function UmamiAnalytics() {
|
||||
useEffect(() => {
|
||||
const scriptUrl = import.meta.env.VITE_UMAMI_SCRIPT_URL;
|
||||
const websiteId = import.meta.env.VITE_UMAMI_WEBSITE_ID;
|
||||
|
||||
if (import.meta.env.DEV || !scriptUrl || !websiteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.getElementById(SCRIPT_ELEMENT_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.id = SCRIPT_ELEMENT_ID;
|
||||
script.src = scriptUrl;
|
||||
script.defer = true;
|
||||
script.setAttribute('data-website-id', websiteId);
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.getElementById(SCRIPT_ELEMENT_ID)?.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { render, cleanup } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { UmamiAnalytics } from '../UmamiAnalytics';
|
||||
|
||||
const SCRIPT_ID = 'umami-analytics-script';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
document.getElementById(SCRIPT_ID)?.remove();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe('UmamiAnalytics', () => {
|
||||
it('does not inject a script tag when the env vars are missing', () => {
|
||||
vi.stubEnv('VITE_UMAMI_SCRIPT_URL', '');
|
||||
vi.stubEnv('VITE_UMAMI_WEBSITE_ID', '');
|
||||
|
||||
render(<UmamiAnalytics />);
|
||||
|
||||
expect(document.getElementById(SCRIPT_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it('does not inject a script tag during local development, even if configured', () => {
|
||||
vi.stubEnv('DEV', true);
|
||||
vi.stubEnv('VITE_UMAMI_SCRIPT_URL', 'https://analytics.slpsoftware.nl/script.js');
|
||||
vi.stubEnv('VITE_UMAMI_WEBSITE_ID', 'test-website-id');
|
||||
|
||||
render(<UmamiAnalytics />);
|
||||
|
||||
expect(document.getElementById(SCRIPT_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it('injects the tracking script with the configured URL and website id when built for test/production', () => {
|
||||
vi.stubEnv('DEV', false);
|
||||
vi.stubEnv('VITE_UMAMI_SCRIPT_URL', 'https://analytics.slpsoftware.nl/script.js');
|
||||
vi.stubEnv('VITE_UMAMI_WEBSITE_ID', 'test-website-id');
|
||||
|
||||
render(<UmamiAnalytics />);
|
||||
|
||||
const script = document.getElementById(SCRIPT_ID) as HTMLScriptElement | null;
|
||||
expect(script).not.toBeNull();
|
||||
expect(script?.src).toBe('https://analytics.slpsoftware.nl/script.js');
|
||||
expect(script?.getAttribute('data-website-id')).toBe('test-website-id');
|
||||
});
|
||||
|
||||
it('renders nothing visible', () => {
|
||||
const { container } = render(<UmamiAnalytics />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
/** Umami tracking script URL (self-hosted), e.g. https://analytics.slpsoftware.nl/script.js */
|
||||
readonly VITE_UMAMI_SCRIPT_URL?: string;
|
||||
/** Umami website ID, created manually in the Umami dashboard for this site. */
|
||||
readonly VITE_UMAMI_WEBSITE_ID?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Reference in New Issue
Block a user