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

@ -9,7 +9,8 @@
".": "./src/index.ts",
"./analytics": "./src/analytics.ts",
"./analytics-server": "./src/analytics-server.ts",
"./security-headers": "./src/security-headers.ts"
"./security-headers": "./src/security-headers.ts",
"./web-vitals": "./src/web-vitals.ts"
},
"scripts": {
"type-check": "tsc --noEmit",
@ -17,7 +18,8 @@
"lint": "eslint ."
},
"dependencies": {
"date-fns": "^4.1.0"
"date-fns": "^4.1.0",
"web-vitals": "^5.2.0"
},
"devDependencies": {
"@types/node": "^24.10.1",

View file

@ -328,6 +328,16 @@ export const ManaCoreEvents = {
track.manacore('widget_resized', { widget_type: widgetType, size }),
creditsTabViewed: (tab: string) => track.manacore('credits_tab_viewed', { tab }),
profileUpdated: () => track.manacore('profile_updated'),
// Funnel events — track key activation & retention moments
/** User created their first piece of content in any module */
firstContentCreated: (appId: string) => track.manacore('first_content_created', { app: appId }),
/** User returned after first session (fired once per user) */
userReturnVisit: () => track.manacore('user_return_visit'),
/** User used a second module (cross-app engagement) */
secondModuleUsed: (appId: string) => track.manacore('second_module_used', { app: appId }),
/** Guest user converted to registered user */
guestConverted: () => track.manacore('guest_converted'),
};
/**

View file

@ -0,0 +1,57 @@
/**
* Web Vitals Umami Integration
*
* Tracks Core Web Vitals (LCP, CLS, INP) and additional metrics (FCP, TTFB)
* as Umami events. Call `trackWebVitals()` once on app startup.
*
* @example
* ```typescript
* import { trackWebVitals } from '@manacore/shared-utils/web-vitals';
* trackWebVitals();
* ```
*/
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
import { trackEvent } from './analytics';
/**
* Rating thresholds per metric (good / needs-improvement / poor).
* Based on https://web.dev/articles/vitals
*/
function getRating(name: string, value: number): 'good' | 'needs-improvement' | 'poor' {
const thresholds: Record<string, [number, number]> = {
CLS: [0.1, 0.25],
INP: [200, 500],
LCP: [2500, 4000],
FCP: [1800, 3000],
TTFB: [800, 1800],
};
const [good, poor] = thresholds[name] ?? [Infinity, Infinity];
if (value <= good) return 'good';
if (value <= poor) return 'needs-improvement';
return 'poor';
}
function reportMetric(name: string, value: number): void {
const rounded = name === 'CLS' ? Math.round(value * 1000) / 1000 : Math.round(value);
trackEvent('web_vital', {
metric: name,
value: rounded,
rating: getRating(name, value),
module: 'performance',
});
}
/**
* Start tracking all Core Web Vitals + FCP/TTFB.
* Each metric fires once per page load when the browser has a final value.
*/
export function trackWebVitals(): void {
if (typeof window === 'undefined') return;
onCLS((m) => reportMetric('CLS', m.value));
onINP((m) => reportMetric('INP', m.value));
onLCP((m) => reportMetric('LCP', m.value));
onFCP((m) => reportMetric('FCP', m.value));
onTTFB((m) => reportMetric('TTFB', m.value));
}