feat(analytics): add Analytics Maturity metric to ManaScore and custom event tracking to 4 apps

Add new "Analytics Maturity" extended metric to ManaScore schema with 5 checks:
pageViewTracking, customEvents, authTracking, landingTracking, publicDashboard.
Populate analytics data across all 20 audit files. Document the metric in about.md.

Add app-specific Umami custom event helpers and integrate tracking into:
- ManaCore: 13 events (nav, onboarding, dashboard widgets, credits, settings)
- Presi: 12 events (deck/slide CRUD, presentation start/exit, sharing)
- Zitare: 9 events (quotes, favorites, categories, search, lists, language)
- Mukke: 12 events (upload, library, playlists, projects, editor export)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-25 09:20:59 +01:00
parent f0233b8d31
commit d2264f5360
46 changed files with 314 additions and 4 deletions

View file

@ -253,6 +253,16 @@ const manascoreCollection = defineCollection({
sharedLlm: z.boolean().optional(), // Uses @manacore/shared-llm (if applicable)
})
.optional(),
// Analytics maturity (Umami tracking depth)
analytics: z
.object({
pageViewTracking: z.boolean(), // Umami script injected (automatic page views)
customEvents: z.boolean(), // App-specific custom events implemented
authTracking: z.boolean(), // Login/signup/logout tracked via shared-auth
landingTracking: z.boolean(), // Landing page CTA/pricing events
publicDashboard: z.boolean(), // Public Umami dashboard configured
})
.optional(),
// Score history for trend visualization
history: z
.array(

View file

@ -49,6 +49,12 @@ lighthouse:
accessibility: 95
bestPractices: 96
seo: 100
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'production'
version: '1.1.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 85
security: 82
ux: 80
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'production'
version: '0.3.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 10
security: 60
ux: 55
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'beta'
version: '0.2.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 92
security: 85
ux: 85
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: false
publicDashboard: true
status: 'production'
version: '1.0.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 85
security: 68
ux: 65
analytics:
pageViewTracking: true
customEvents: false
authTracking: true
landingTracking: false
publicDashboard: true
status: 'beta'
version: '0.1.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 88
security: 80
ux: 92
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'production'
version: '0.3.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 25
security: 55
ux: 68
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'alpha'
version: '0.2.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 92
security: 88
ux: 82
analytics:
pageViewTracking: false
customEvents: false
authTracking: false
landingTracking: false
publicDashboard: false
status: 'production'
version: '0.2.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 80
security: 78
ux: 60
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'beta'
version: '0.2.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 85
security: 68
ux: 55
analytics:
pageViewTracking: true
customEvents: false
authTracking: true
landingTracking: true
publicDashboard: true
status: 'beta'
version: '0.2.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 78
security: 65
ux: 55
analytics:
pageViewTracking: true
customEvents: false
authTracking: true
landingTracking: false
publicDashboard: true
status: 'beta'
version: '0.2.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 78
security: 80
ux: 78
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'production'
version: '0.3.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 62
security: 55
ux: 50
analytics:
pageViewTracking: true
customEvents: false
authTracking: true
landingTracking: false
publicDashboard: true
status: 'alpha'
version: '0.1.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 90
security: 85
ux: 82
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'mature'
version: '0.2.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 72
security: 55
ux: 55
analytics:
pageViewTracking: true
customEvents: false
authTracking: true
landingTracking: false
publicDashboard: true
status: 'alpha'
version: '0.1.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 62
security: 65
ux: 72
analytics:
pageViewTracking: true
customEvents: false
authTracking: true
landingTracking: false
publicDashboard: true
status: 'beta'
version: '0.2.0'
stats:

View file

@ -19,6 +19,12 @@ scores:
documentation: 78
security: 78
ux: 75
analytics:
pageViewTracking: true
customEvents: false
authTracking: true
landingTracking: false
publicDashboard: true
status: 'production'
version: '0.6.0'
stats:

View file

@ -49,6 +49,12 @@ lighthouse:
accessibility: 93
bestPractices: 96
seo: 100
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'production'
version: '1.1.0'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 45
security: 55
ux: 35
analytics:
pageViewTracking: false
customEvents: false
authTracking: false
landingTracking: false
publicDashboard: false
status: 'alpha'
version: '0.0.1'
stats:

View file

@ -15,6 +15,12 @@ scores:
documentation: 20
security: 70
ux: 75
analytics:
pageViewTracking: true
customEvents: true
authTracking: true
landingTracking: true
publicDashboard: true
status: 'beta'
version: '0.2.0'
stats:

View file

@ -203,6 +203,20 @@ Prüft 7 Konsistenz-Kriterien der Backend-API:
| **Health Endpoint** | `/health` Endpoint vorhanden |
| **Validation** | DTO-Validation mit class-validator auf allen Inputs |
### Analytics Maturity
Bewertet die Tiefe der Umami-Analytics-Integration pro App:
| Check | Beschreibung |
| ---------------------- | ---------------------------------------------------------------------- |
| **Page View Tracking** | Umami-Script via `hooks.server.ts` injiziert (automatische Page Views) |
| **Custom Events** | App-spezifische Events implementiert (z.B. `TodoEvents`, `ChatEvents`) |
| **Auth Tracking** | Login/Signup/Logout automatisch via `@manacore/shared-auth` getrackt |
| **Landing Tracking** | Landing Page Events (CTA-Clicks, Pricing Views) via `Analytics.astro` |
| **Public Dashboard** | Öffentliches Dashboard auf `stats.mana.how` konfiguriert |
Apps mit allen 5 Checks sind **analytics-mature** — sie erfassen nicht nur Page Views, sondern auch Feature-spezifische Nutzerinteraktionen, Auth-Flows und Landing-Conversions.
### Cross-App Consistency
Misst wie konsequent eine App die shared Packages des Monorepos nutzt:

View file

@ -11,6 +11,7 @@
import type { WidgetConfig, WidgetSize } from '$lib/types/dashboard';
import { getWidgetMeta } from '$lib/types/dashboard';
import { dashboardStore } from '$lib/stores/dashboard.svelte';
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
// Widget components
import CreditsWidget from './widgets/CreditsWidget.svelte';
@ -48,9 +49,11 @@
function handleSizeChange(size: WidgetSize) {
dashboardStore.updateWidgetSize(widget.id, size);
ManaCoreEvents.widgetResized(widget.type, size);
}
function handleRemove() {
ManaCoreEvents.widgetRemoved(widget.type);
dashboardStore.removeWidget(widget.id);
}

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { onboardingStore } from '$lib/stores/onboarding.svelte';
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
import WelcomeStep from './steps/WelcomeStep.svelte';
import ProfileStep from './steps/ProfileStep.svelte';
import AppsStep from './steps/AppsStep.svelte';
@ -31,6 +32,7 @@
onboardingStore.complete();
onComplete();
} else {
ManaCoreEvents.onboardingStepCompleted(currentStepData.id, currentStep + 1);
onboardingStore.completeStep(currentStepData.id);
onboardingStore.nextStep();
}

View file

@ -14,6 +14,7 @@
import type { ThemeVariant } from '@manacore/shared-theme';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import { ManaCoreEvents, AppEvents } from '@manacore/shared-utils/analytics';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
@ -68,6 +69,7 @@
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
AppEvents.languageChanged(newLocale);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
@ -128,10 +130,12 @@
function handleToggleTheme() {
theme.toggleMode();
AppEvents.themeChanged(theme.isDark ? 'dark' : 'light');
}
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
theme.setMode(mode);
AppEvents.themeChanged(mode);
}
async function handleSignOut() {
@ -145,10 +149,12 @@
function handleOnboardingComplete() {
onboardingStore.complete();
ManaCoreEvents.onboardingCompleted();
showOnboarding = false;
}
function handleOnboardingSkip() {
ManaCoreEvents.onboardingSkipped(onboardingStore.currentStep);
onboardingStore.skip();
showOnboarding = false;
}
@ -183,6 +189,7 @@
onboardingStore.load();
if (onboardingStore.shouldShow) {
onboardingStore.start();
ManaCoreEvents.onboardingStarted();
showOnboarding = true;
}

View file

@ -15,6 +15,7 @@
formatCreditCost,
type CreditOperationType,
} from '@manacore/credit-operations';
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
let balance = $state<CreditBalance | null>(null);
let transactions = $state<CreditTransaction[]>([]);
@ -105,6 +106,7 @@
if (tab === 'packages') activeTab = 'packages';
else if (tab === 'transactions') activeTab = 'transactions';
else if (tab === 'costs') activeTab = 'costs';
if (activeTab !== 'overview') ManaCoreEvents.creditsTabViewed(activeTab);
// Handle success/canceled from Stripe redirect
const success = $page.url.searchParams.get('success');

View file

@ -6,11 +6,17 @@
import type { CreditBalance, CreditTransaction } from '$lib/api/credits';
import { authStore } from '$lib/stores/auth.svelte';
import { dashboardStore } from '$lib/stores/dashboard.svelte';
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
import DashboardGrid from '$lib/components/dashboard/DashboardGrid.svelte';
onMount(() => {
dashboardStore.initialize();
});
function handleToggleEditing() {
dashboardStore.toggleEditing();
ManaCoreEvents.dashboardEditToggled(dashboardStore.isEditing);
}
</script>
<div>
@ -22,7 +28,7 @@
/>
<button
type="button"
onclick={() => dashboardStore.toggleEditing()}
onclick={handleToggleEditing}
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors {dashboardStore.isEditing
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"

View file

@ -9,6 +9,7 @@
type AppIconId,
} from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
// Detect dev mode
const isDev =
@ -100,6 +101,7 @@
}
function handleAppClick(app: ManaApp) {
ManaCoreEvents.appOpened(app.id);
const url = getAppUrl(app.id);
if (url) {
window.open(url, '_blank', 'noopener,noreferrer');

View file

@ -6,6 +6,7 @@
import type { CreditBalance } from '$lib/api/credits';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { APP_VERSION } from '$lib/version';
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
let loading = $state(true);
let savingProfile = $state(false);
@ -45,6 +46,7 @@
// TODO: Implement profile update API when available
await new Promise((resolve) => setTimeout(resolve, 500));
profileSuccess = true;
ManaCoreEvents.profileUpdated();
} catch (e) {
profileError = e instanceof Error ? e.message : 'Fehler beim Speichern';
} finally {

View file

@ -4,6 +4,7 @@
import { onMount, onDestroy } from 'svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { projectStore } from '$lib/stores/project.svelte';
import { MukkeEvents } from '@manacore/shared-utils/analytics';
import { audioStore } from '$lib/stores/audio.svelte';
import { editorStore } from '$lib/stores/editor.svelte';
import { MARKER_COLORS } from '@mukke/shared';
@ -226,6 +227,7 @@
if (!response.ok) throw new Error('Export failed');
const blob = await response.blob();
MukkeEvents.projectExported(format);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;

View file

@ -4,6 +4,7 @@
import { libraryStore } from '$lib/stores/library.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { playerStore } from '$lib/stores/player.svelte';
import { MukkeEvents } from '@manacore/shared-utils/analytics';
import SongEditor from '$lib/components/SongEditor.svelte';
import { ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
import type { Song } from '@mukke/shared';
@ -109,7 +110,9 @@
async function handleToggleFavorite(id: string, e: Event) {
e.preventDefault();
e.stopPropagation();
const song = libraryStore.songs.find((s) => s.id === id);
await libraryStore.toggleFavorite(id);
MukkeEvents.songFavorited(!song?.favorite);
}
function handleEditSong(song: Song, e: Event) {
@ -120,6 +123,7 @@
function handlePlaySong(song: Song, index: number) {
playerStore.playSong(song, libraryStore.songs, index);
MukkeEvents.songPlayed();
}
async function openInEditor(songId: string, e: Event) {

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { playlistStore } from '$lib/stores/playlist.svelte';
import { MukkeEvents } from '@manacore/shared-utils/analytics';
let showCreateModal = $state(false);
let newName = $state('');
@ -16,6 +17,7 @@
isCreating = true;
try {
await playlistStore.createPlaylist(newName.trim(), newDescription.trim() || undefined);
MukkeEvents.playlistCreated();
newName = '';
newDescription = '';
showCreateModal = false;
@ -30,6 +32,7 @@
e.stopPropagation();
if (!confirm('Delete this playlist?')) return;
await playlistStore.deletePlaylist(id);
MukkeEvents.playlistDeleted();
}
function truncate(text: string, max: number): string {

View file

@ -3,6 +3,7 @@
import { page } from '$app/stores';
import { playlistStore } from '$lib/stores/playlist.svelte';
import { playerStore } from '$lib/stores/player.svelte';
import { MukkeEvents } from '@manacore/shared-utils/analytics';
import type { Song } from '@mukke/shared';
let isEditingName = $state(false);
@ -32,6 +33,7 @@
const playlist = playlistStore.currentPlaylist;
if (!playlist || playlist.songs.length === 0) return;
playerStore.playQueue(playlist.songs, 0);
MukkeEvents.playlistPlayAll();
}
function handleShufflePlay() {
@ -39,6 +41,7 @@
if (!playlist || playlist.songs.length === 0) return;
const shuffled = shuffleArray(playlist.songs);
playerStore.playQueue(shuffled, 0);
MukkeEvents.playlistShufflePlay();
}
function handlePlaySong(song: Song, index: number) {

View file

@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { projectStore } from '$lib/stores/project.svelte';
import { MukkeEvents } from '@manacore/shared-utils/analytics';
let showCreateModal = $state(false);
let newProjectTitle = $state('');
@ -18,6 +19,7 @@
isCreating = true;
try {
const project = await projectStore.createProject(newProjectTitle, newProjectDescription);
MukkeEvents.projectCreated();
showCreateModal = false;
newProjectTitle = '';
newProjectDescription = '';
@ -32,6 +34,7 @@
e.preventDefault();
if (confirm('Are you sure you want to delete this project?')) {
await projectStore.deleteProject(id);
MukkeEvents.projectDeleted();
}
}

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { libraryStore } from '$lib/stores/library.svelte';
import { MukkeEvents } from '@manacore/shared-utils/analytics';
interface UploadFile {
file: File;
@ -90,6 +91,7 @@
files[index].status = 'uploaded';
files[index].progress = 100;
files[index].songId = song.id;
MukkeEvents.songUploaded();
// Auto-extract ID3 tags from the uploaded file
try {
@ -107,6 +109,7 @@
} catch (e) {
files[index].status = 'error';
files[index].error = e instanceof Error ? e.message : 'Upload failed';
MukkeEvents.songUploadFailed();
}
}

View file

@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { decksStore } from '$lib/stores/decks.svelte';
import { PresiEvents } from '@manacore/shared-utils/analytics';
import { PageHeader, ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
import {
Plus,
@ -74,6 +75,7 @@
});
if (deck) {
PresiEvents.deckCreated();
showCreateModal = false;
newDeckTitle = '';
newDeckDescription = '';
@ -90,6 +92,7 @@
async function handleDelete() {
if (!deckToDelete) return;
await decksStore.deleteDeck(deckToDelete.id);
PresiEvents.deckDeleted();
showDeleteModal = false;
deckToDelete = null;
}

View file

@ -4,6 +4,7 @@
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { decksStore } from '$lib/stores/decks.svelte';
import { PresiEvents } from '@manacore/shared-utils/analytics';
import { shareApi } from '$lib/api/client';
import type { ShareLink } from '$lib/api/client';
import type { Slide, SlideContent } from '@presi/shared';
@ -93,8 +94,10 @@
if (editingSlide) {
await decksStore.updateSlide(editingSlide.id, { content });
PresiEvents.slideEdited();
} else {
await decksStore.createSlide(deckId, { content });
PresiEvents.slideCreated();
}
isSaving = false;
@ -109,6 +112,7 @@
async function handleDeleteSlide() {
if (!slideToDelete) return;
await decksStore.deleteSlide(slideToDelete.id);
PresiEvents.slideDeleted();
showDeleteModal = false;
slideToDelete = null;
}
@ -135,6 +139,7 @@
// Update order values
newSlides.forEach((s, i) => (s.order = i + 1));
await decksStore.reorderSlides(newSlides);
PresiEvents.slideReordered(direction);
}
function addBulletPoint() {
@ -170,6 +175,7 @@
try {
const newShare = await shareApi.createShare(deckId);
shareLinks = [newShare, ...shareLinks];
PresiEvents.shareLinkCreated();
} catch (e) {
console.error('Failed to create share link:', e);
} finally {
@ -181,6 +187,7 @@
try {
await shareApi.deleteShare(shareId);
shareLinks = shareLinks.filter((s) => s.id !== shareId);
PresiEvents.shareLinkDeleted();
} catch (e) {
console.error('Failed to delete share link:', e);
}
@ -195,6 +202,7 @@
const url = getShareUrl(share.shareCode);
try {
await navigator.clipboard.writeText(url);
PresiEvents.shareLinkCopied();
copiedLinkId = share.id;
setTimeout(() => {
copiedLinkId = null;

View file

@ -3,6 +3,7 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { decksStore } from '$lib/stores/decks.svelte';
import { PresiEvents } from '@manacore/shared-utils/analytics';
import type { Slide } from '@presi/shared';
import {
X,
@ -28,8 +29,12 @@
const deckId = $page.params.id as string;
let maxSlideReached = $state(0);
onMount(() => {
decksStore.loadDeck(deckId);
decksStore.loadDeck(deckId).then(() => {
PresiEvents.presentationStarted(decksStore.currentSlides.length);
});
// Keyboard navigation
window.addEventListener('keydown', handleKeydown);
@ -92,6 +97,7 @@
function nextSlide() {
if (currentSlideIndex < decksStore.currentSlides.length - 1) {
currentSlideIndex++;
if (currentSlideIndex > maxSlideReached) maxSlideReached = currentSlideIndex;
}
}
@ -126,6 +132,7 @@
}
function exitPresentation() {
PresiEvents.presentationExited(elapsedSeconds, maxSlideReached + 1);
if (document.fullscreenElement) {
document.exitFullscreen();
}

View file

@ -2,6 +2,7 @@
import { onMount, onDestroy } from 'svelte';
import { page } from '$app/stores';
import { shareApi } from '$lib/api/client';
import { PresiEvents } from '@manacore/shared-utils/analytics';
import type { Slide } from '@presi/shared';
import {
CaretLeft,
@ -34,6 +35,7 @@
const data = await shareApi.getByCode(shareCode);
deck = data;
slides = data.slides || [];
PresiEvents.sharedDeckViewed();
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load shared deck';
} finally {

View file

@ -3,6 +3,7 @@
import { quotesStore } from '$lib/stores/quotes.svelte';
import { favoritesStore } from '$lib/stores/favorites.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { ZitareEvents } from '@manacore/shared-utils/analytics';
import { _ } from 'svelte-i18n';
interface Props {
@ -47,7 +48,13 @@
async function toggleFavorite() {
if (!authStore.isAuthenticated) return;
const wasFavorite = isFavorite;
await favoritesStore.toggle(quote.id);
if (wasFavorite) {
ZitareEvents.quoteUnfavorited();
} else {
ZitareEvents.quoteFavorited(quote.category);
}
}
async function shareQuote() {
@ -60,6 +67,7 @@
} else {
await navigator.clipboard.writeText(text);
}
ZitareEvents.quoteShared(quote.category);
}
const sizeClasses = {

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { quotesStore } from '$lib/stores/quotes.svelte';
import { ZitareEvents } from '@manacore/shared-utils/analytics';
import QuoteCard from '$lib/components/QuoteCard.svelte';
let isRefreshing = $state(false);
@ -8,6 +9,7 @@
async function loadNewQuote() {
isRefreshing = true;
quotesStore.loadRandomQuote();
ZitareEvents.randomQuoteLoaded();
// Small delay for visual feedback
await new Promise((r) => setTimeout(r, 300));
isRefreshing = false;

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
import { ZitareEvents } from '@manacore/shared-utils/analytics';
import { CATEGORIES, getQuotesByCategory, type Category } from '@zitare/content';
// Category data with icons and gradients
@ -82,7 +83,10 @@
{#each CATEGORIES as category}
{@const data = categoryData[category]}
<button
onclick={() => goto(`/category/${category}`)}
onclick={() => {
ZitareEvents.categoryViewed(category);
goto(`/category/${category}`);
}}
class="group p-6 rounded-2xl bg-gradient-to-br {data.gradient} text-white text-left transition-transform hover:scale-105 hover:shadow-xl"
>
<div class="text-4xl mb-3">{data.icon}</div>

View file

@ -3,6 +3,7 @@
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
import { authStore } from '$lib/stores/auth.svelte';
import { ZitareEvents } from '@manacore/shared-utils/analytics';
interface QuoteList {
id: string;
@ -78,6 +79,7 @@
if (response.ok) {
const data = await response.json();
lists = [...lists, data.list];
ZitareEvents.listCreated();
showCreateModal = false;
newListName = '';
newListDescription = '';
@ -101,6 +103,7 @@
if (response.ok) {
lists = lists.filter((l) => l.id !== listId);
ZitareEvents.listDeleted();
}
} catch (error) {
console.error('Failed to delete list:', error);

View file

@ -1,15 +1,25 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { searchQuotes, type Quote } from '@zitare/content';
import { ZitareEvents } from '@manacore/shared-utils/analytics';
import { quotesStore } from '$lib/stores/quotes.svelte';
import QuoteCard from '$lib/components/QuoteCard.svelte';
let searchTerm = $state('');
let lastTrackedTerm = $state('');
// Search results
let results = $derived<Quote[]>(
searchTerm.length >= 2 ? searchQuotes(searchTerm, quotesStore.language) : []
);
// Track search when results change (debounced by derived reactivity)
$effect(() => {
if (searchTerm.length >= 2 && searchTerm !== lastTrackedTerm) {
lastTrackedTerm = searchTerm;
ZitareEvents.searchPerformed(results.length);
}
});
</script>
<svelte:head>

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { quotesStore } from '$lib/stores/quotes.svelte';
import { ZitareEvents } from '@manacore/shared-utils/analytics';
import type { SupportedLanguage } from '@zitare/content';
import { APP_VERSION } from '$lib/version';
@ -16,7 +17,9 @@
function handleLanguageChange(event: Event) {
const select = event.target as HTMLSelectElement;
quotesStore.setLanguage(select.value as SupportedLanguage);
const lang = select.value as SupportedLanguage;
quotesStore.setLanguage(lang);
ZitareEvents.quoteLanguageChanged(lang);
}
</script>

View file

@ -255,6 +255,80 @@ export const ManaDeckEvents = {
aiCardsGenerated: (count: number) => trackEvent('ai_cards_generated', { count }),
};
/**
* ManaCore Platform Events
*/
export const ManaCoreEvents = {
appOpened: (appId: string) => trackEvent('app_opened', { app: appId }),
navClicked: (destination: string) => trackEvent('nav_clicked', { destination }),
onboardingStarted: () => trackEvent('onboarding_started'),
onboardingStepCompleted: (step: string, stepNumber: number) =>
trackEvent('onboarding_step_completed', { step, step_number: stepNumber }),
onboardingCompleted: () => trackEvent('onboarding_completed'),
onboardingSkipped: (atStep: number) => trackEvent('onboarding_skipped', { at_step: atStep }),
dashboardEditToggled: (editing: boolean) => trackEvent('dashboard_edit_toggled', { editing }),
widgetAdded: (widgetType: string) => trackEvent('widget_added', { widget_type: widgetType }),
widgetRemoved: (widgetType: string) => trackEvent('widget_removed', { widget_type: widgetType }),
widgetResized: (widgetType: string, size: string) =>
trackEvent('widget_resized', { widget_type: widgetType, size }),
creditsTabViewed: (tab: string) => trackEvent('credits_tab_viewed', { tab }),
profileUpdated: () => trackEvent('profile_updated'),
};
/**
* Mukke App Events
*/
export const MukkeEvents = {
songUploaded: () => trackEvent('song_uploaded'),
songUploadFailed: () => trackEvent('song_upload_failed'),
songPlayed: () => trackEvent('song_played'),
songFavorited: (favorited: boolean) => trackEvent('song_favorited', { favorited }),
songDeleted: () => trackEvent('song_deleted'),
playlistCreated: () => trackEvent('playlist_created'),
playlistDeleted: () => trackEvent('playlist_deleted'),
playlistPlayAll: () => trackEvent('playlist_play_all'),
playlistShufflePlay: () => trackEvent('playlist_shuffle_play'),
projectCreated: () => trackEvent('project_created'),
projectDeleted: () => trackEvent('project_deleted'),
projectExported: (format: string) => trackEvent('project_exported', { format }),
};
/**
* Zitare App Events
*/
export const ZitareEvents = {
randomQuoteLoaded: () => trackEvent('random_quote_loaded'),
quoteShared: (category: string) => trackEvent('quote_shared', { category }),
quoteFavorited: (category: string) => trackEvent('quote_favorited', { category }),
quoteUnfavorited: () => trackEvent('quote_unfavorited'),
categoryViewed: (category: string) => trackEvent('category_viewed', { category }),
searchPerformed: (resultsCount: number) =>
trackEvent('search_performed', { results: resultsCount }),
listCreated: () => trackEvent('list_created'),
listDeleted: () => trackEvent('list_deleted'),
quoteLanguageChanged: (language: string) => trackEvent('quote_language_changed', { language }),
};
/**
* Presi App Events
*/
export const PresiEvents = {
deckCreated: () => trackEvent('deck_created'),
deckDeleted: () => trackEvent('deck_deleted'),
slideCreated: () => trackEvent('slide_created'),
slideEdited: () => trackEvent('slide_edited'),
slideDeleted: () => trackEvent('slide_deleted'),
slideReordered: (direction: 'up' | 'down') => trackEvent('slide_reordered', { direction }),
presentationStarted: (slideCount: number) =>
trackEvent('presentation_started', { slide_count: slideCount }),
presentationExited: (duration: number, slidesViewed: number) =>
trackEvent('presentation_exited', { duration_seconds: duration, slides_viewed: slidesViewed }),
shareLinkCreated: () => trackEvent('share_link_created'),
shareLinkCopied: () => trackEvent('share_link_copied'),
shareLinkDeleted: () => trackEvent('share_link_deleted'),
sharedDeckViewed: () => trackEvent('shared_deck_viewed'),
};
/**
* Subscription/Payment Events
*/