feat(analytics): add Web Vitals tracking, GlitchTip user context, and funnel events

- Add web-vitals package with LCP/CLS/INP/FCP/TTFB → Umami tracking
- Set GlitchTip user context on login, clear on logout
- Add funnel events: first_content_created, user_return_visit,
  second_module_used, guest_converted
- Track first content via Dexie creating hook (fires once per user)
- Track module usage via route navigation effect
- Track guest→registered conversion on signup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 17:03:06 +02:00
parent 198720ca38
commit f2d6573fa7
9 changed files with 188 additions and 3 deletions

View file

@ -1,4 +1,5 @@
import { initErrorTracking, handleSvelteError } from '@manacore/shared-error-tracking/browser';
import { trackWebVitals } from '@manacore/shared-utils/web-vitals';
import type { HandleClientError } from '@sveltejs/kit';
initErrorTracking({
@ -7,6 +8,8 @@ initErrorTracking({
environment: import.meta.env.MODE,
});
trackWebVitals();
export const handleError: HandleClientError = ({ error }) => {
handleSvelteError(error);
};

View file

@ -8,6 +8,7 @@
*/
import Dexie, { type EntityTable } from 'dexie';
import { trackFirstContent } from '$lib/stores/funnel-tracking';
// ─── Database ──────────────────────────────────────────────
@ -344,6 +345,7 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
data: { ...obj },
createdAt: now,
});
trackFirstContent(appId);
});
table.hook('updating', function (modifications, primKey) {

View file

@ -0,0 +1,88 @@
/**
* Funnel Tracking fires one-time activation & retention events.
*
* Each event uses a localStorage flag so it fires at most once per user/device.
* These events power Umami funnels:
* signup onboarding_completed first_content_created second_module_used user_return_visit
*/
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
const KEYS = {
firstContent: 'mana_funnel_first_content',
returnVisit: 'mana_funnel_return_visit',
modulesUsed: 'mana_funnel_modules',
wasGuest: 'mana_funnel_was_guest',
} as const;
function flagged(key: string): boolean {
return localStorage.getItem(key) === '1';
}
function flag(key: string): void {
localStorage.setItem(key, '1');
}
/**
* Call on app init (authenticated users).
* Fires `user_return_visit` on the second session.
*/
export function trackReturnVisit(): void {
if (flagged(KEYS.returnVisit)) return;
const lastVisit = localStorage.getItem('mana_last_visit');
const now = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
if (lastVisit && lastVisit !== now) {
ManaCoreEvents.userReturnVisit();
flag(KEYS.returnVisit);
}
localStorage.setItem('mana_last_visit', now);
}
/**
* Call when a user creates content in any module.
* Fires `first_content_created` exactly once.
*/
export function trackFirstContent(appId: string): void {
if (flagged(KEYS.firstContent)) return;
ManaCoreEvents.firstContentCreated(appId);
flag(KEYS.firstContent);
}
/**
* Call when a user navigates to a module.
* Fires `second_module_used` when they visit a second distinct module.
*/
export function trackModuleUsed(appId: string): void {
const raw = localStorage.getItem(KEYS.modulesUsed);
const modules: string[] = raw ? JSON.parse(raw) : [];
if (modules.includes(appId)) return;
modules.push(appId);
localStorage.setItem(KEYS.modulesUsed, JSON.stringify(modules));
if (modules.length === 2) {
ManaCoreEvents.secondModuleUsed(appId);
}
}
/**
* Mark the current session as guest (call before auth).
*/
export function markAsGuest(): void {
if (!flagged(KEYS.wasGuest)) {
flag(KEYS.wasGuest);
}
}
/**
* Call after signup. Fires `guest_converted` if the user was previously a guest.
*/
export function trackGuestConversion(): void {
if (flagged(KEYS.wasGuest)) {
ManaCoreEvents.guestConverted();
localStorage.removeItem(KEYS.wasGuest);
}
}

View file

@ -40,6 +40,8 @@
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { ManaCoreEvents, AppEvents } from '@manacore/shared-utils/analytics';
import { setUser as setErrorTrackingUser } from '@manacore/shared-error-tracking/browser';
import { trackReturnVisit, trackModuleUsed, markAsGuest } from '$lib/stores/funnel-tracking';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
@ -196,6 +198,7 @@
async function handleSignOut() {
unifiedSync?.stopAll();
guestMode?.destroy();
setErrorTrackingUser(null);
await authStore.signOut();
goto('/login');
}
@ -239,6 +242,8 @@
// Phase B: Auth-dependent — sync, settings, onboarding
if (authStore.isAuthenticated) {
setErrorTrackingUser({ id: authStore.user?.id ?? 'unknown', email: authStore.user?.email });
trackReturnVisit();
const getToken = () => authStore.getValidToken();
unifiedSync = createUnifiedSync(SYNC_SERVER_URL, getToken);
unifiedSync.startAll();
@ -255,6 +260,7 @@
// Phase C: Guest mode — welcome modal + nudge
if (!authStore.isAuthenticated) {
markAsGuest();
guestMode = createGuestMode('manacore', {
nudgeDelayMinutes: 3,
onRegister: () => goto('/register'),
@ -285,6 +291,12 @@
if (moduleSlug === activeModulePrefix) return;
// Track module usage for funnel analysis
const moduleName = pathname.split('/')[1];
if (moduleName && authStore.isAuthenticated) {
trackModuleUsed(moduleName);
}
const loader = getAdapterLoader(pathname);
if (!loader) {
inputBarAdapter = createFallbackAdapter(searchRegistry);

View file

@ -6,13 +6,16 @@
import { ManaCoreLogo } from '@manacore/shared-branding';
import AppSlider from '$lib/components/AppSlider.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { trackGuestConversion } from '$lib/stores/funnel-tracking';
import '$lib/i18n';
// Get translations based on current locale
const translations = $derived(getRegisterTranslations($locale || 'de'));
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
const result = await authStore.signUp(email, password);
if (result.success) trackGuestConversion();
return result;
}
async function handleResendVerification(email: string) {