managarten/docs/ANALYTICS.md
Till JS cc5ba3bb90 chore: remove Hetzner legacy artifacts and update docs for Mac Mini self-hosting
Deleted files:
- docker/caddy/Caddyfile.production + Caddyfile.staging (Hetzner reverse proxy configs)
- scripts/deploy/ (deploy-hetzner.sh, build-and-push.sh, health-check.sh, migrate-db.sh, rollback.sh)
- scripts/generate-staging-secrets.sh
- cicd/ directory (11 Hetzner CI/CD planning docs)
- CI_CD_IMPLEMENTATION_SUMMARY.md, CI_CD_README.md, FILES_CREATED.md, HIVE_MIND_FINAL_REPORT.md

Updated docs:
- CLAUDE.md: Remove Hetzner Object Storage references, update to MinIO
- docs/ANALYTICS.md: Cloudflare Tunnel instead of Caddy
- docs/URL_SCHEMA.md: Mac Mini + Cloudflare Tunnel instead of Hetzner IP
- .env.development: Remove "Hetzner in production" comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 10:12:24 +01:00

15 KiB

Analytics & Event Tracking

ManaCore verwendet Umami für Web Analytics. Alle Events werden zu stats.mana.how gesendet.

Umami Dashboard

Architektur

Web-App Analytics werden über hooks.server.ts injiziert (nicht mehr hardcoded in app.html).

.env.development              → UMAMI_WEBSITE_ID_CHAT=xxx
       ↓ (scripts/generate-env.mjs)
apps/chat/apps/web/.env       → PUBLIC_UMAMI_WEBSITE_ID=xxx
       ↓ (process.env in hooks.server.ts)
injectUmamiAnalytics(html)    → <script defer src="stats.mana.how/script.js" data-website-id="xxx">

Zentrale Konfiguration

  • Website-IDs: .env.development (UMAMI_WEBSITE_ID_*)
  • Env-Verteilung: scripts/generate-env.mjsPUBLIC_UMAMI_WEBSITE_ID
  • Server-Side Injection: @manacore/shared-utils/analytics-server (injectUmamiAnalytics())
  • Client-Side Events: @manacore/shared-utils/analytics (trackEvent(), etc.)

Neue App hinzufügen

  1. Website in Umami anlegen (https://stats.mana.how)
  2. UMAMI_WEBSITE_ID_APPNAME=<uuid> zu .env.development hinzufügen
  3. PUBLIC_UMAMI_WEBSITE_ID Mapping in scripts/generate-env.mjs hinzufügen
  4. @manacore/shared-utils als Dependency in der Web-App package.json
  5. In hooks.server.ts: import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'
  6. injectUmamiAnalytics(html) im transformPageChunk aufrufen
  7. pnpm setup:env ausführen

Website IDs

Hinweis: Die Website-IDs sind in .env.development als UMAMI_WEBSITE_ID_* definiert. Das ist die Single Source of Truth. Die IDs hier dienen nur als Schnellreferenz.

Landing Pages

App Env-Variable
Chat UMAMI_WEBSITE_ID_CHAT_LANDING
ManaCore UMAMI_WEBSITE_ID_MANACORE_LANDING
ManaDeck UMAMI_WEBSITE_ID_MANADECK_LANDING
Calendar UMAMI_WEBSITE_ID_CALENDAR_LANDING
Clock UMAMI_WEBSITE_ID_CLOCK_LANDING
Picture UMAMI_WEBSITE_ID_PICTURE_LANDING
Todo UMAMI_WEBSITE_ID_TODO_LANDING
NutriPhi UMAMI_WEBSITE_ID_NUTRIPHI_LANDING
Presi UMAMI_WEBSITE_ID_PRESI_LANDING
Mukke UMAMI_WEBSITE_ID_MUKKE_LANDING

Web Apps

App Env-Variable
Chat UMAMI_WEBSITE_ID_CHAT
ManaCore UMAMI_WEBSITE_ID_MANACORE
Todo UMAMI_WEBSITE_ID_TODO
Calendar UMAMI_WEBSITE_ID_CALENDAR
Clock UMAMI_WEBSITE_ID_CLOCK
Contacts UMAMI_WEBSITE_ID_CONTACTS
Picture UMAMI_WEBSITE_ID_PICTURE
ManaDeck UMAMI_WEBSITE_ID_MANADECK
Planta UMAMI_WEBSITE_ID_PLANTA
Mukke UMAMI_WEBSITE_ID_MUKKE
Questions UMAMI_WEBSITE_ID_QUESTIONS
Zitare UMAMI_WEBSITE_ID_ZITARE
Presi UMAMI_WEBSITE_ID_PRESI
NutriPhi UMAMI_WEBSITE_ID_NUTRIPHI
Storage UMAMI_WEBSITE_ID_STORAGE
Photos UMAMI_WEBSITE_ID_PHOTOS
SkillTree UMAMI_WEBSITE_ID_SKILLTREE

Automatisches Auth-Tracking

Auth-Events werden automatisch in @manacore/shared-auth (src/core/authService.ts) getrackt (alle Web-Apps):

Event Wann Data
login Erfolgreicher Login { method: 'email' | 'google' | 'apple' | 'sso' }
login_failed Login fehlgeschlagen { method: 'email' | 'google' | 'apple' }
signup Erfolgreiche Registrierung { method: 'email' }
signup_failed Registrierung fehlgeschlagen { method: 'email' }
logout Benutzer abgemeldet -
password_reset_requested Passwort-Reset angefragt -

Diese Events erfordern keinen Code in den einzelnen Apps — sie werden automatisch vom shared Auth-Service ausgelöst.


Landing Page Event Tracking

Alle Landing Pages binden <Analytics /> aus @manacore/shared-landing-ui ein. Das Script trackt automatisch:

Event Wann Data
cta_click Klick auf CTA-Button/Link { location: 'hero' | 'pricing' | 'footer' | ... }
pricing_viewed Pricing-Section wird sichtbar -
pricing_plan_selected Klick auf Pricing-Plan CTA { plan: 'free' | 'pro' | ... }

Auto-Detection: Das Script erkennt die Section automatisch aus id-Attributen oder der Position im DOM (erster/letzter Abschnitt).

Explizite Attribute (optional):

<a href="/register" data-track-cta="hero">Jetzt starten</a>
<a href="/pro" data-track-cta="pricing" data-track-pricing="pro">Pro starten</a>
<section data-track-section="pricing">...</section>

Custom Event Tracking

Installation

Die Analytics-Utilities sind in @manacore/shared-utils verfügbar:

import {
  trackEvent,
  trackClick,
  AuthEvents,
  LandingEvents,
  ChatEvents,
  // ...
} from '@manacore/shared-utils/analytics';

Basis-Funktionen

trackEvent(eventName, data?)

Trackt ein benutzerdefiniertes Event.

trackEvent('custom_action', { key: 'value' });

trackClick(elementId, label?)

Trackt Button- oder Link-Klicks.

trackClick('cta_hero', 'Get Started');
// Trackt: { event: 'click', element: 'cta_hero', label: 'Get Started' }

trackView(section)

Trackt Section/Page Views.

trackView('pricing_section');
// Trackt: { event: 'view', section: 'pricing_section' }

trackFormSubmit(formId, success)

Trackt Formular-Submissions.

trackFormSubmit('contact_form', true);
// Trackt: { event: 'form_submit', form: 'contact_form', success: true }

trackSearch(query, resultsCount)

Trackt Suchanfragen (nur Länge für Privacy).

trackSearch('react hooks', 42);
// Trackt: { event: 'search', query_length: 11, results: 42 }

trackError(errorType, message?)

Trackt Fehler (Message wird auf 100 Zeichen gekürzt).

trackError('api_error', 'Failed to fetch data');

App-Spezifische Event Helpers

AuthEvents

Hinweis: Diese Helpers werden in der Regel nicht direkt verwendet. Das automatische Auth-Tracking in @manacore/shared-auth (siehe oben) deckt alle Login/Signup/Logout-Events ab.

import { AuthEvents } from '@manacore/shared-utils/analytics';

AuthEvents.login('email');           // login { method: 'email' }
AuthEvents.login('google');          // login { method: 'google' }
AuthEvents.login('github');          // login { method: 'github' }
AuthEvents.logout();                 // logout
AuthEvents.signup('email');          // signup { method: 'email' }
AuthEvents.signupCompleted();        // signup_completed
AuthEvents.passwordReset();          // password_reset

LandingEvents

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

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

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

import { TodoEvents } from '@manacore/shared-utils/analytics';

TodoEvents.taskCreated(true);            // task_created { has_deadline: true }
TodoEvents.taskCompleted();              // task_completed
TodoEvents.taskUncompleted();            // task_uncompleted
TodoEvents.taskDeleted();                // task_deleted
TodoEvents.subtaskCompleted();           // subtask_completed
TodoEvents.projectCreated();             // project_created
TodoEvents.projectDeleted();             // project_deleted
TodoEvents.labelCreated();               // label_created
TodoEvents.viewChanged('kanban');        // view_changed { view: 'kanban' }
TodoEvents.quickAddUsed();               // quick_add_used
TodoEvents.filterUsed('priority');       // filter_used { filter: 'priority' }

CalendarEvents

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.calendarDeleted();        // calendar_deleted
CalendarEvents.calendarShared();         // calendar_shared
CalendarEvents.viewChanged('week');      // view_changed { view: 'week' }
CalendarEvents.reminderSet(30);          // reminder_set { minutes: 30 }
CalendarEvents.eventDragged();           // event_dragged

ClockEvents

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

import { ContactsEvents } from '@manacore/shared-utils/analytics';

ContactsEvents.contactCreated();             // contact_created
ContactsEvents.contactUpdated();             // contact_updated
ContactsEvents.contactDeleted();             // contact_deleted
ContactsEvents.contactFavorited();           // contact_favorited
ContactsEvents.contactArchived();            // contact_archived
ContactsEvents.contactImported('google', 5); // contact_imported { source: 'google', count: 5 }
ContactsEvents.contactExported('vcard');     // contact_exported { format: 'vcard' }
ContactsEvents.tagCreated();                 // tag_created
ContactsEvents.searchPerformed();            // search_performed

ManaDeckEvents

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

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

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

<script lang="ts">
  import { LandingEvents } from '@manacore/shared-utils/analytics';

  function handleCtaClick() {
    LandingEvents.ctaClick('hero');
    // Navigate to app...
  }
</script>

<button onclick={handleCtaClick}>
  Get Started
</button>

Astro Landing Pages

---
// Layout.astro - Script tag is already in <head>
---

<script>
  import { LandingEvents } from '@manacore/shared-utils/analytics';

  document.querySelectorAll('[data-cta]').forEach(btn => {
    btn.addEventListener('click', () => {
      const location = btn.getAttribute('data-cta');
      LandingEvents.ctaClick(location);
    });
  });
</script>

Development Mode

Im Development-Modus ist Umami normalerweise nicht geladen (kein Script-Tag), daher werden Events stillschweigend ignoriert (isUmamiAvailable() gibt false zurück). Fehler beim Tracking werden als console.warn ausgegeben.


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: mana-mon-umami
  • Image: ghcr.io/umami-software/umami:postgresql-latest
  • Datenbank: PostgreSQL (umami DB, shared Postgres-Instanz)
  • Port: 3000 (intern) → 8010 (extern), via Cloudflare Tunnel erreichbar unter stats.mana.how