From ea805c724b26a62fbc11f26660bb92516a8f0db4 Mon Sep 17 00:00:00 2001 From: Vladimir Eremin Date: Sun, 23 Aug 2026 20:28:22 +0300 Subject: [PATCH] feat(analytics): connect Umami analytics across www, docs, and static services (#49) ## Summary of Changes - **`@ermnvldmr/ssg`**: Added extensible `AnalyticsProvider` interface, `umamiAnalytics` integration in `@ermnvldmr/ssg/dev`, and runtime client analytics dispatchers (`trackEvent`, `identifySession`). - **`services/docs`**: Configured native Hextra Umami parameters in `hugo.yaml` (`docs.ermnvldmr.com` with UUID `fbdadf3f-3484-4186-a2c2-cedcf76cf44f`). - **`services/www`**: Injected Umami tracking provider into `rsbuild.config.ts` (`ermnvldmr.com`, `www.ermnvldmr.com` with UUID `4fca5ac2-4fe4-4626-b708-933c37bfe918`). - **`services/static`**: Injected Umami tracking provider into `rsbuild.config.ts` (`static.ermnvldmr.com` with UUID `0831532a-2cec-4955-8f5b-452d8bba96d8`). - **Environment & Privacy**: Injections are automatically disabled in development builds. Configured `data-domains` filtering and `data-do-not-track="true"`. ## Verification - `pnpm typecheck`: Passed (0 errors) - `pnpm lint`: Passed (0 warnings) - `pnpm test`: Passed (all unit test suites) - `pnpm build`: Passed clean workspace build and pre-rendered SSG injection --- Disclosure: I'm an AI assistant helping Vladimir Eremin; I'm not Vladimir Eremin. --- .../ThemeSwitch/ThemeSwitch.test.tsx | 23 ++++- .../components/ThemeSwitch/ThemeSwitch.tsx | 20 +++- .../ssg/src/lib/config/analytics/index.ts | 2 + .../ssg/src/lib/config/analytics/types.ts | 35 +++++++ .../ssg/src/lib/config/analytics/umami.ts | 99 +++++++++++++++++++ .../ssg/src/lib/config/ssgServiceConfig.ts | 50 ++++++++-- packages/ssg/src/lib/core/analytics.ts | 68 +++++++++++++ packages/ssg/src/lib/dev.ts | 1 + packages/ssg/src/lib/index.ts | 1 + .../filterDataAttributes.test.ts | 33 +++++++ .../filterDataAttributes.ts | 32 ++++++ packages/stl/src/lib/react/index.ts | 1 + packages/stl/src/lib/types/dataAttributes.ts | 11 +++ packages/stl/src/lib/types/index.ts | 1 + packages/ui/src/components/Button/Button.tsx | 10 +- .../ui/src/components/Container/Container.tsx | 18 ++-- packages/ui/src/components/Link/Link.tsx | 10 +- .../src/components/LogProvider/LogContext.ts | 30 ++++++ .../LogProvider/LogProvider.stories.tsx | 42 ++++++++ .../LogProvider/LogProvider.test.tsx | 37 +++++++ .../components/LogProvider/LogProvider.tsx | 34 +++++++ .../ui/src/components/LogProvider/index.ts | 2 + packages/ui/src/components/Stack/Stack.tsx | 19 ++-- packages/ui/src/components/Switch/Switch.tsx | 7 +- packages/ui/src/components/index.ts | 1 + services/docs/hugo.yaml | 8 ++ services/static/rsbuild.config.ts | 12 ++- .../layouts/IndexLayout/IndexLayout.tsx | 45 +++++---- services/www/rsbuild.config.ts | 16 ++- .../Layout/IndexLayout/IndexLayout.tsx | 12 ++- .../components/LocaleSwitch/LocaleSwitch.tsx | 2 + 31 files changed, 622 insertions(+), 60 deletions(-) create mode 100644 packages/ssg/src/lib/config/analytics/index.ts create mode 100644 packages/ssg/src/lib/config/analytics/types.ts create mode 100644 packages/ssg/src/lib/config/analytics/umami.ts create mode 100644 packages/ssg/src/lib/core/analytics.ts create mode 100644 packages/stl/src/lib/react/filterDataAttributes/filterDataAttributes.test.ts create mode 100644 packages/stl/src/lib/react/filterDataAttributes/filterDataAttributes.ts create mode 100644 packages/stl/src/lib/types/dataAttributes.ts create mode 100644 packages/ui/src/components/LogProvider/LogContext.ts create mode 100644 packages/ui/src/components/LogProvider/LogProvider.stories.tsx create mode 100644 packages/ui/src/components/LogProvider/LogProvider.test.tsx create mode 100644 packages/ui/src/components/LogProvider/LogProvider.tsx create mode 100644 packages/ui/src/components/LogProvider/index.ts diff --git a/packages/kits/src/components/ThemeSwitch/ThemeSwitch.test.tsx b/packages/kits/src/components/ThemeSwitch/ThemeSwitch.test.tsx index 21522d5..f99bed4 100644 --- a/packages/kits/src/components/ThemeSwitch/ThemeSwitch.test.tsx +++ b/packages/kits/src/components/ThemeSwitch/ThemeSwitch.test.tsx @@ -1,7 +1,8 @@ +import { LogProvider } from '@ermnvldmr/ui'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { ThemeSwitch } from './ThemeSwitch'; @@ -38,4 +39,24 @@ describe('ThemeSwitch', () => { expect(systemButton).toBeInTheDocument(); }); + + it('dispatches telemetry events to LogProvider', async () => { + const user = userEvent.setup(); + const loggerMock = vi.fn(); + + render( + + + + ); + + const switchControl = screen.getByRole('switch'); + const systemButton = screen.getByRole('button'); + + await user.click(switchControl); + expect(loggerMock).toHaveBeenCalledWith('theme-toggle', { preference: 'dark' }); + + await user.click(systemButton); + expect(loggerMock).toHaveBeenCalledWith('theme-toggle', { preference: 'system' }); + }); }); diff --git a/packages/kits/src/components/ThemeSwitch/ThemeSwitch.tsx b/packages/kits/src/components/ThemeSwitch/ThemeSwitch.tsx index 6f9b9d2..9246fee 100644 --- a/packages/kits/src/components/ThemeSwitch/ThemeSwitch.tsx +++ b/packages/kits/src/components/ThemeSwitch/ThemeSwitch.tsx @@ -1,4 +1,4 @@ -import { Button, HStack, Switch, useTheme } from '@ermnvldmr/ui'; +import { Button, HStack, Switch, useLogger, useTheme } from '@ermnvldmr/ui'; import React from 'react'; /** @@ -27,28 +27,42 @@ export function ThemeSwitch({ systemIcon, }: ThemeSwitchProps = {}): React.JSX.Element { const { preference, setPreference, resolvedTheme } = useTheme(); + const log = useLogger(); const isDark = resolvedTheme === 'dark'; + const handleToggle = (isSelected: boolean): void => { + const nextTheme = isSelected ? 'dark' : 'light'; + setPreference(nextTheme); + log('theme-toggle', { preference: nextTheme }); + }; + + const handleSystem = (): void => { + setPreference('system'); + log('theme-toggle', { preference: 'system' }); + }; + return ( {lightIcon} setPreference(isSelected ? 'dark' : 'light')} + onChange={handleToggle} /> {darkIcon}