From b8a84edfe022e958de7e6c5d6e1ccccc5bfd4cd4 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:58:04 +0100 Subject: [PATCH] feat(analytics): add Umami event tracking utilities - Add comprehensive analytics.ts with type-safe event tracking - Include app-specific event helpers (Auth, Landing, Chat, Picture, Todo, Calendar, Clock, Contacts, ManaDeck, Subscription, App events) - Export from shared-utils package - Add complete documentation in docs/ANALYTICS.md Co-Authored-By: Claude Opus 4.5 --- docs/ANALYTICS.md | 334 +++++++++++++++++++++++++ packages/shared-utils/src/analytics.ts | 276 ++++++++++++++++++++ packages/shared-utils/src/index.ts | 3 + 3 files changed, 613 insertions(+) create mode 100644 docs/ANALYTICS.md create mode 100644 packages/shared-utils/src/analytics.ts diff --git a/docs/ANALYTICS.md b/docs/ANALYTICS.md new file mode 100644 index 000000000..68c391060 --- /dev/null +++ b/docs/ANALYTICS.md @@ -0,0 +1,334 @@ +# Analytics & Event Tracking + +ManaCore verwendet Umami für Web Analytics. Alle Events werden zu `stats.mana.how` gesendet. + +## Umami Dashboard + +- **URL**: https://stats.mana.how +- **Public Stats**: Alle Websites haben Public Sharing aktiviert + +## Website IDs + +### Landing Pages + +| App | Website ID | Public URL | +|-----|-----------|------------| +| Chat | `a264b165-80d2-47ab-91f4-2efc01de0b66` | stats.mana.how/share/chatlanding | +| ManaCore | `cef3798d-85ae-47df-a44a-e9bee09dbcf9` | stats.mana.how/share/manacorelanding | +| ManaDeck | `2ac83d50-107f-4d4e-ac23-5540946e96e3` | stats.mana.how/share/manadecklanding | +| Calendar | `84862d98-727e-4e25-8645-639241dd1544` | stats.mana.how/share/calendarlanding | +| Clock | `0332b471-a022-46af-a726-0f45932bfd58` | stats.mana.how/share/clocklanding | +| Picture | `d3ac98e6-0d1a-47a3-a218-2a81fff596bd` | stats.mana.how/share/picturelanding | + +### Web Apps + +| App | Website ID | Public URL | +|-----|-----------|------------| +| Chat | `5cf9d569-3266-4a57-80dd-3a652dc32786` | stats.mana.how/share/chatwebapp | +| ManaCore | `4a14016d-394a-44e0-8ecc-67271f63ffb0` | stats.mana.how/share/manacorewebapp | +| Todo | `ac021d98-778e-46cf-b6b2-2f650ea78f07` | stats.mana.how/share/todowebapp | +| Calendar | `884fc0a8-3b67-43bd-903b-2be531c66792` | stats.mana.how/share/calendarwebapp | +| Clock | `1e7b5006-87a5-4547-8a3d-ab30eac15dd4` | stats.mana.how/share/clockwebapp | +| Contacts | `ab89a839-be15-4949-99b4-e72492cee4ff` | stats.mana.how/share/contactswebapp | +| Picture | `bc552bd2-667d-44b4-a717-0dce6a8db98f` | stats.mana.how/share/picturewebapp | +| ManaDeck | `314fc57a-c63d-4008-b19e-5e272c0329d6` | stats.mana.how/share/manadeckwebapp | +| Planta | `876f30bd-43e3-405a-9697-6157db67ca6b` | stats.mana.how/share/plantawebapp | + +--- + +## Custom Event Tracking + +### Installation + +Die Analytics-Utilities sind in `@manacore/shared-utils` verfügbar: + +```typescript +import { + trackEvent, + trackClick, + AuthEvents, + LandingEvents, + ChatEvents, + // ... +} from '@manacore/shared-utils/analytics'; +``` + +### Basis-Funktionen + +#### `trackEvent(eventName, data?)` + +Trackt ein benutzerdefiniertes Event. + +```typescript +trackEvent('custom_action', { key: 'value' }); +``` + +#### `trackClick(elementId, label?)` + +Trackt Button- oder Link-Klicks. + +```typescript +trackClick('cta_hero', 'Get Started'); +// Trackt: { event: 'click', element: 'cta_hero', label: 'Get Started' } +``` + +#### `trackView(section)` + +Trackt Section/Page Views. + +```typescript +trackView('pricing_section'); +// Trackt: { event: 'view', section: 'pricing_section' } +``` + +#### `trackFormSubmit(formId, success)` + +Trackt Formular-Submissions. + +```typescript +trackFormSubmit('contact_form', true); +// Trackt: { event: 'form_submit', form: 'contact_form', success: true } +``` + +#### `trackSearch(query, resultsCount)` + +Trackt Suchanfragen (nur Länge für Privacy). + +```typescript +trackSearch('react hooks', 42); +// Trackt: { event: 'search', query_length: 11, results: 42 } +``` + +#### `trackError(errorType, message?)` + +Trackt Fehler (Message wird auf 100 Zeichen gekürzt). + +```typescript +trackError('api_error', 'Failed to fetch data'); +``` + +--- + +## App-Spezifische Event Helpers + +### AuthEvents + +```typescript +import { AuthEvents } from '@manacore/shared-utils/analytics'; + +AuthEvents.login('email'); // login { method: 'email' } +AuthEvents.login('google'); // login { method: 'google' } +AuthEvents.logout(); // logout +AuthEvents.signup('email'); // signup { method: 'email' } +AuthEvents.signupCompleted(); // signup_completed +AuthEvents.passwordReset(); // password_reset +``` + +### LandingEvents + +```typescript +import { LandingEvents } from '@manacore/shared-utils/analytics'; + +LandingEvents.ctaClick('hero'); // cta_click { location: 'hero' } +LandingEvents.ctaClick('pricing'); // cta_click { location: 'pricing' } +LandingEvents.pricingViewed(); // pricing_viewed +LandingEvents.pricingPlanSelected('pro'); // pricing_plan_selected { plan: 'pro' } +LandingEvents.demoStarted(); // demo_started +LandingEvents.featureExplored('ai-chat'); // feature_explored { feature: 'ai-chat' } +LandingEvents.faqOpened('How does it work?'); // faq_opened { question: 'How does...' } +LandingEvents.contactFormSubmitted(); // contact_form_submitted +LandingEvents.newsletterSubscribed(); // newsletter_subscribed +``` + +### ChatEvents + +```typescript +import { ChatEvents } from '@manacore/shared-utils/analytics'; + +ChatEvents.conversationCreated(); // conversation_created +ChatEvents.messageSent('gpt-4'); // message_sent { model: 'gpt-4' } +ChatEvents.modelChanged('claude-3'); // model_changed { model: 'claude-3' } +ChatEvents.conversationDeleted(); // conversation_deleted +ChatEvents.conversationShared(); // conversation_shared +``` + +### PictureEvents + +```typescript +import { PictureEvents } from '@manacore/shared-utils/analytics'; + +PictureEvents.imageGenerated('flux', 'realistic'); // image_generated { model: 'flux', style: 'realistic' } +PictureEvents.imageDownloaded(); // image_downloaded +PictureEvents.imageFavorited(); // image_favorited +PictureEvents.imageShared(); // image_shared +PictureEvents.modelSelected('sdxl'); // model_selected { model: 'sdxl' } +PictureEvents.styleSelected('anime'); // style_selected { style: 'anime' } +PictureEvents.generationFailed('timeout'); // generation_failed { reason: 'timeout' } +``` + +### TodoEvents + +```typescript +import { TodoEvents } from '@manacore/shared-utils/analytics'; + +TodoEvents.taskCreated(true); // task_created { has_deadline: true } +TodoEvents.taskCompleted(); // task_completed +TodoEvents.taskDeleted(); // task_deleted +TodoEvents.projectCreated(); // project_created +TodoEvents.labelCreated(); // label_created +TodoEvents.viewChanged('today'); // view_changed { view: 'today' } +``` + +### CalendarEvents + +```typescript +import { CalendarEvents } from '@manacore/shared-utils/analytics'; + +CalendarEvents.eventCreated(true); // event_created { recurring: true } +CalendarEvents.eventUpdated(); // event_updated +CalendarEvents.eventDeleted(); // event_deleted +CalendarEvents.calendarCreated(); // calendar_created +CalendarEvents.calendarShared(); // calendar_shared +CalendarEvents.viewChanged('week'); // view_changed { view: 'week' } +CalendarEvents.reminderSet(30); // reminder_set { minutes: 30 } +``` + +### ClockEvents + +```typescript +import { ClockEvents } from '@manacore/shared-utils/analytics'; + +ClockEvents.timerStarted('pomodoro'); // timer_started { type: 'pomodoro' } +ClockEvents.timerCompleted('pomodoro', 1500); // timer_completed { type: 'pomodoro', duration_seconds: 1500 } +ClockEvents.timerCanceled(); // timer_canceled +ClockEvents.focusSessionStarted(); // focus_session_started +ClockEvents.focusSessionCompleted(45); // focus_session_completed { duration_minutes: 45 } +``` + +### ContactsEvents + +```typescript +import { ContactsEvents } from '@manacore/shared-utils/analytics'; + +ContactsEvents.contactCreated(); // contact_created +ContactsEvents.contactUpdated(); // contact_updated +ContactsEvents.contactDeleted(); // contact_deleted +ContactsEvents.contactImported('google'); // contact_imported { source: 'google' } +ContactsEvents.contactExported('vcard'); // contact_exported { format: 'vcard' } +ContactsEvents.tagCreated(); // tag_created +ContactsEvents.searchPerformed(); // search_performed +``` + +### ManaDeckEvents + +```typescript +import { ManaDeckEvents } from '@manacore/shared-utils/analytics'; + +ManaDeckEvents.deckCreated(); // deck_created +ManaDeckEvents.deckStudied(25); // deck_studied { cards: 25 } +ManaDeckEvents.cardCreated(); // card_created +ManaDeckEvents.cardReviewed(4); // card_reviewed { rating: 4 } +ManaDeckEvents.aiCardsGenerated(10); // ai_cards_generated { count: 10 } +``` + +### SubscriptionEvents + +```typescript +import { SubscriptionEvents } from '@manacore/shared-utils/analytics'; + +SubscriptionEvents.pricingViewed(); // pricing_viewed +SubscriptionEvents.planSelected('pro'); // plan_selected { plan: 'pro' } +SubscriptionEvents.checkoutStarted('pro'); // checkout_started { plan: 'pro' } +SubscriptionEvents.checkoutCompleted('pro'); // checkout_completed { plan: 'pro' } +SubscriptionEvents.checkoutAbandoned('pro'); // checkout_abandoned { plan: 'pro' } +SubscriptionEvents.subscriptionCanceled('pro'); // subscription_canceled { plan: 'pro' } +SubscriptionEvents.trialStarted(); // trial_started +SubscriptionEvents.trialEnded(true); // trial_ended { converted: true } +``` + +### AppEvents + +```typescript +import { AppEvents } from '@manacore/shared-utils/analytics'; + +AppEvents.appOpened('chat'); // app_opened { app: 'chat' } +AppEvents.themeChanged('dark'); // theme_changed { theme: 'dark' } +AppEvents.languageChanged('de'); // language_changed { language: 'de' } +AppEvents.feedbackSubmitted('bug'); // feedback_submitted { type: 'bug' } +AppEvents.helpOpened(); // help_opened +AppEvents.settingsOpened(); // settings_opened +AppEvents.shareClicked('twitter'); // share_clicked { platform: 'twitter' } +``` + +--- + +## Integration Guide + +### Svelte/SvelteKit + +```svelte + + + +``` + +### Astro Landing Pages + +```astro +--- +// Layout.astro - Script tag is already in +--- + + +``` + +### Development Mode + +Im Development-Modus (`import.meta.env?.DEV`) werden Events in die Console geloggt: + +``` +[Analytics] cta_click { location: 'hero' } +``` + +--- + +## Event Naming Conventions + +1. **snake_case** für Event-Namen: `task_created`, nicht `taskCreated` +2. **Kurze, beschreibende Namen**: `signup_completed`, nicht `user_has_completed_signup_process` +3. **Konsistente Suffixe**: + - `_created`, `_updated`, `_deleted` für CRUD + - `_started`, `_completed`, `_canceled` für Prozesse + - `_clicked`, `_viewed` für UI-Interaktionen + +## Privacy + +- Keine persönlichen Daten in Events (keine E-Mails, Namen, etc.) +- Suchanfragen: Nur Länge wird getracked, nicht der Inhalt +- Error Messages: Auf 100 Zeichen gekürzt +- GDPR-konform: Umami ist privacy-focused und setzt keine Cookies + +## Umami Server + +- **Host**: Mac Mini (mana-server) +- **Container**: `umami` +- **Datenbank**: PostgreSQL (shared mit anderen Services) +- **Port**: 3200 (intern), via Caddy erreichbar unter stats.mana.how diff --git a/packages/shared-utils/src/analytics.ts b/packages/shared-utils/src/analytics.ts new file mode 100644 index 000000000..5e8f110aa --- /dev/null +++ b/packages/shared-utils/src/analytics.ts @@ -0,0 +1,276 @@ +/** + * Umami Analytics Utility + * + * Provides type-safe event tracking for all ManaCore apps. + * Events are automatically sent to Umami at stats.mana.how + * + * @example + * ```typescript + * import { trackEvent, trackClick } from '@manacore/shared-utils/analytics'; + * + * // Track a custom event + * trackEvent('signup_completed', { method: 'email' }); + * + * // Track a button click + * trackClick('cta_hero', 'Get Started'); + * ``` + */ + +// Umami types +declare global { + interface Window { + umami?: { + track: (eventName: string, eventData?: Record) => void; + }; + } +} + +/** + * Check if Umami is available + */ +export function isUmamiAvailable(): boolean { + return typeof window !== 'undefined' && typeof window.umami?.track === 'function'; +} + +/** + * Track a custom event + * + * @param eventName - Name of the event (snake_case recommended) + * @param data - Optional event data/properties + * + * @example + * trackEvent('image_generated', { model: 'flux', style: 'realistic' }); + */ +export function trackEvent( + eventName: string, + data?: Record +): void { + if (!isUmamiAvailable()) { + return; + } + + try { + window.umami!.track(eventName, data); + } catch (error) { + console.warn('[Analytics] Failed to track event:', eventName, error); + } +} + +/** + * Track a button/link click + * + * @param elementId - Identifier for the element (e.g., 'cta_hero', 'nav_pricing') + * @param label - Human-readable label + * + * @example + * trackClick('cta_hero', 'Start Free Trial'); + */ +export function trackClick(elementId: string, label?: string): void { + trackEvent('click', { element: elementId, label: label || elementId }); +} + +/** + * Track a page/section view + * + * @param section - Section identifier + * + * @example + * trackView('pricing_section'); + */ +export function trackView(section: string): void { + trackEvent('view', { section }); +} + +/** + * Track form submission + * + * @param formId - Form identifier + * @param success - Whether submission was successful + * + * @example + * trackFormSubmit('contact_form', true); + */ +export function trackFormSubmit(formId: string, success: boolean): void { + trackEvent('form_submit', { form: formId, success }); +} + +/** + * Track search queries + * + * @param query - Search query (consider privacy - don't track full queries) + * @param resultsCount - Number of results + * + * @example + * trackSearch('react hooks', 42); + */ +export function trackSearch(query: string, resultsCount: number): void { + // Only track query length for privacy + trackEvent('search', { query_length: query.length, results: resultsCount }); +} + +/** + * Track errors + * + * @param errorType - Type of error + * @param message - Error message (sanitized) + * + * @example + * trackError('api_error', 'Failed to fetch data'); + */ +export function trackError(errorType: string, message?: string): void { + trackEvent('error', { + type: errorType, + message: message?.substring(0, 100) || 'unknown', + }); +} + +// ============================================================================= +// App-Specific Event Helpers +// ============================================================================= + +/** + * Auth Events + */ +export const AuthEvents = { + login: (method: 'email' | 'google' | 'github' = 'email') => trackEvent('login', { method }), + logout: () => trackEvent('logout'), + signup: (method: 'email' | 'google' | 'github' = 'email') => trackEvent('signup', { method }), + signupCompleted: () => trackEvent('signup_completed'), + passwordReset: () => trackEvent('password_reset'), +}; + +/** + * Landing Page Events + */ +export const LandingEvents = { + ctaClick: (location: 'hero' | 'pricing' | 'features' | 'footer' | string) => + trackEvent('cta_click', { location }), + pricingViewed: () => trackEvent('pricing_viewed'), + pricingPlanSelected: (plan: string) => trackEvent('pricing_plan_selected', { plan }), + demoStarted: () => trackEvent('demo_started'), + featureExplored: (feature: string) => trackEvent('feature_explored', { feature }), + faqOpened: (question: string) => + trackEvent('faq_opened', { question: question.substring(0, 50) }), + contactFormSubmitted: () => trackEvent('contact_form_submitted'), + newsletterSubscribed: () => trackEvent('newsletter_subscribed'), +}; + +/** + * Chat App Events + */ +export const ChatEvents = { + conversationCreated: () => trackEvent('conversation_created'), + messageSent: (modelId?: string) => + trackEvent('message_sent', modelId ? { model: modelId } : undefined), + modelChanged: (modelId: string) => trackEvent('model_changed', { model: modelId }), + conversationDeleted: () => trackEvent('conversation_deleted'), + conversationShared: () => trackEvent('conversation_shared'), +}; + +/** + * Picture App Events + */ +export const PictureEvents = { + imageGenerated: (model: string, style?: string) => + trackEvent('image_generated', { model, ...(style && { style }) }), + imageDownloaded: () => trackEvent('image_downloaded'), + imageFavorited: () => trackEvent('image_favorited'), + imageShared: () => trackEvent('image_shared'), + modelSelected: (model: string) => trackEvent('model_selected', { model }), + styleSelected: (style: string) => trackEvent('style_selected', { style }), + generationFailed: (reason?: string) => + trackEvent('generation_failed', { reason: reason || 'unknown' }), +}; + +/** + * Todo App Events + */ +export const TodoEvents = { + taskCreated: (hasDeadline = false) => trackEvent('task_created', { has_deadline: hasDeadline }), + taskCompleted: () => trackEvent('task_completed'), + taskDeleted: () => trackEvent('task_deleted'), + projectCreated: () => trackEvent('project_created'), + labelCreated: () => trackEvent('label_created'), + viewChanged: (view: 'inbox' | 'today' | 'upcoming' | 'project') => + trackEvent('view_changed', { view }), +}; + +/** + * Calendar App Events + */ +export const CalendarEvents = { + eventCreated: (isRecurring = false) => trackEvent('event_created', { recurring: isRecurring }), + eventUpdated: () => trackEvent('event_updated'), + eventDeleted: () => trackEvent('event_deleted'), + calendarCreated: () => trackEvent('calendar_created'), + calendarShared: () => trackEvent('calendar_shared'), + viewChanged: (view: 'day' | 'week' | 'month' | 'agenda') => trackEvent('view_changed', { view }), + reminderSet: (minutesBefore: number) => trackEvent('reminder_set', { minutes: minutesBefore }), +}; + +/** + * Clock App Events + */ +export const ClockEvents = { + timerStarted: (type: 'pomodoro' | 'stopwatch' | 'countdown') => + trackEvent('timer_started', { type }), + timerCompleted: (type: 'pomodoro' | 'stopwatch' | 'countdown', duration: number) => + trackEvent('timer_completed', { type, duration_seconds: duration }), + timerCanceled: () => trackEvent('timer_canceled'), + focusSessionStarted: () => trackEvent('focus_session_started'), + focusSessionCompleted: (duration: number) => + trackEvent('focus_session_completed', { duration_minutes: duration }), +}; + +/** + * Contacts App Events + */ +export const ContactsEvents = { + contactCreated: () => trackEvent('contact_created'), + contactUpdated: () => trackEvent('contact_updated'), + contactDeleted: () => trackEvent('contact_deleted'), + contactImported: (source: 'google' | 'csv' | 'vcard') => + trackEvent('contact_imported', { source }), + contactExported: (format: 'csv' | 'vcard') => trackEvent('contact_exported', { format }), + tagCreated: () => trackEvent('tag_created'), + searchPerformed: () => trackEvent('search_performed'), +}; + +/** + * ManaDeck App Events + */ +export const ManaDeckEvents = { + deckCreated: () => trackEvent('deck_created'), + deckStudied: (cardsCount: number) => trackEvent('deck_studied', { cards: cardsCount }), + cardCreated: () => trackEvent('card_created'), + cardReviewed: (rating: 1 | 2 | 3 | 4 | 5) => trackEvent('card_reviewed', { rating }), + aiCardsGenerated: (count: number) => trackEvent('ai_cards_generated', { count }), +}; + +/** + * Subscription/Payment Events + */ +export const SubscriptionEvents = { + pricingViewed: () => trackEvent('pricing_viewed'), + planSelected: (plan: string) => trackEvent('plan_selected', { plan }), + checkoutStarted: (plan: string) => trackEvent('checkout_started', { plan }), + checkoutCompleted: (plan: string) => trackEvent('checkout_completed', { plan }), + checkoutAbandoned: (plan: string) => trackEvent('checkout_abandoned', { plan }), + subscriptionCanceled: (plan: string) => trackEvent('subscription_canceled', { plan }), + trialStarted: () => trackEvent('trial_started'), + trialEnded: (converted: boolean) => trackEvent('trial_ended', { converted }), +}; + +/** + * General App Events + */ +export const AppEvents = { + appOpened: (app: string) => trackEvent('app_opened', { app }), + themeChanged: (theme: 'light' | 'dark' | 'system') => trackEvent('theme_changed', { theme }), + languageChanged: (language: string) => trackEvent('language_changed', { language }), + feedbackSubmitted: (type: 'bug' | 'feature' | 'other') => + trackEvent('feedback_submitted', { type }), + helpOpened: () => trackEvent('help_opened'), + settingsOpened: () => trackEvent('settings_opened'), + shareClicked: (platform: string) => trackEvent('share_clicked', { platform }), +}; diff --git a/packages/shared-utils/src/index.ts b/packages/shared-utils/src/index.ts index 611e1384d..788fe5150 100644 --- a/packages/shared-utils/src/index.ts +++ b/packages/shared-utils/src/index.ts @@ -25,3 +25,6 @@ export * from './cache'; // Natural Language Parsers export * from './parsers'; + +// Umami Analytics +export * from './analytics';