From 12b3c4f0f36ee4dd370337f469ed078d1509b4ba Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 25 Mar 2026 10:13:05 +0100 Subject: [PATCH] feat(analytics): add custom event tracking to Context, SkillTree, Planta, Questions Add app-specific Umami event helpers and integrate tracking into: - Context: 6 events (document create/delete/pin, space create/delete, AI generated) - SkillTree: 3 events (skill create/delete with branch, XP added with level-up) - Planta: 4 events (plant analyzed/created/deleted, plant watered) - Questions: 5 events (question create/delete, research started, collection create/delete) Updates ManaScore analytics from 3/5 to 4/5 for all four apps. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/lib/stores/documents.svelte.ts | 4 ++ .../apps/web/src/lib/stores/spaces.svelte.ts | 3 ++ .../content/manascore/2026-03-19-context.md | 2 +- .../content/manascore/2026-03-19-planta.md | 2 +- .../content/manascore/2026-03-19-questions.md | 2 +- .../content/manascore/2026-03-19-skilltree.md | 2 +- .../web/src/routes/(app)/add/+page.svelte | 3 ++ .../src/routes/(app)/dashboard/+page.svelte | 2 + .../src/routes/(app)/plants/[id]/+page.svelte | 3 ++ .../web/src/lib/stores/collections.svelte.ts | 3 ++ .../web/src/lib/stores/questions.svelte.ts | 3 ++ .../routes/(app)/question/[id]/+page.svelte | 2 + .../apps/web/src/lib/stores/skills.svelte.ts | 5 +++ packages/shared-utils/src/analytics.ts | 43 +++++++++++++++++++ 14 files changed, 75 insertions(+), 4 deletions(-) diff --git a/apps/context/apps/web/src/lib/stores/documents.svelte.ts b/apps/context/apps/web/src/lib/stores/documents.svelte.ts index 27fc1a129..17ec52a0e 100644 --- a/apps/context/apps/web/src/lib/stores/documents.svelte.ts +++ b/apps/context/apps/web/src/lib/stores/documents.svelte.ts @@ -1,4 +1,5 @@ import type { Document, DocumentType } from '$lib/types'; +import { ContextEvents } from '@manacore/shared-utils/analytics'; import * as docsService from '$lib/services/documents'; let documents = $state([]); @@ -133,6 +134,7 @@ export const documentsStore = { if (result.data) { documents = [result.data, ...documents]; currentDocument = result.data; + ContextEvents.documentCreated(type); } return result; } finally { @@ -159,6 +161,7 @@ export const documentsStore = { async delete(id: string) { const result = await docsService.deleteDocument(id); if (result.success) { + ContextEvents.documentDeleted(); documents = documents.filter((d) => d.id !== id); if (currentDocument?.id === id) { currentDocument = null; @@ -173,6 +176,7 @@ export const documentsStore = { const newPinned = !doc.pinned; const result = await docsService.toggleDocumentPinned(id, newPinned); if (result.success) { + ContextEvents.documentPinned(newPinned); documents = documents.map((d) => (d.id === id ? { ...d, pinned: newPinned } : d)); if (currentDocument?.id === id) { currentDocument = { ...currentDocument, pinned: newPinned }; diff --git a/apps/context/apps/web/src/lib/stores/spaces.svelte.ts b/apps/context/apps/web/src/lib/stores/spaces.svelte.ts index 09d2ad010..4dbff7c6f 100644 --- a/apps/context/apps/web/src/lib/stores/spaces.svelte.ts +++ b/apps/context/apps/web/src/lib/stores/spaces.svelte.ts @@ -1,4 +1,5 @@ import type { Space } from '$lib/types'; +import { ContextEvents } from '@manacore/shared-utils/analytics'; import * as spacesService from '$lib/services/spaces'; let spaces = $state([]); @@ -39,6 +40,7 @@ export const spacesStore = { const result = await spacesService.createSpace(userId, name, description); if (result.data) { spaces = [result.data, ...spaces]; + ContextEvents.spaceCreated(); } return result; }, @@ -66,6 +68,7 @@ export const spacesStore = { const result = await spacesService.deleteSpace(id); if (result.success) { spaces = spaces.filter((s) => s.id !== id); + ContextEvents.spaceDeleted(); } return result; }, diff --git a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-context.md b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-context.md index b4cd4a8c5..f6c29a125 100644 --- a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-context.md +++ b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-context.md @@ -17,7 +17,7 @@ scores: ux: 65 analytics: pageViewTracking: true - customEvents: false + customEvents: true authTracking: true landingTracking: false publicDashboard: true diff --git a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-planta.md b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-planta.md index 7e9d5f97a..8b460f5d6 100644 --- a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-planta.md +++ b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-planta.md @@ -17,7 +17,7 @@ scores: ux: 50 analytics: pageViewTracking: true - customEvents: false + customEvents: true authTracking: true landingTracking: false publicDashboard: true diff --git a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-questions.md b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-questions.md index 67d8f3462..a5d4ed8ce 100644 --- a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-questions.md +++ b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-questions.md @@ -17,7 +17,7 @@ scores: ux: 55 analytics: pageViewTracking: true - customEvents: false + customEvents: true authTracking: true landingTracking: false publicDashboard: true diff --git a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-skilltree.md b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-skilltree.md index e417ce8e8..4e02c5844 100644 --- a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-skilltree.md +++ b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-skilltree.md @@ -17,7 +17,7 @@ scores: ux: 72 analytics: pageViewTracking: true - customEvents: false + customEvents: true authTracking: true landingTracking: false publicDashboard: true diff --git a/apps/planta/apps/web/src/routes/(app)/add/+page.svelte b/apps/planta/apps/web/src/routes/(app)/add/+page.svelte index ee73a8920..33d34e1d9 100644 --- a/apps/planta/apps/web/src/routes/(app)/add/+page.svelte +++ b/apps/planta/apps/web/src/routes/(app)/add/+page.svelte @@ -3,6 +3,7 @@ import { photosApi } from '$lib/api/photos'; import { analysisApi } from '$lib/api/analysis'; import { plantsApi } from '$lib/api/plants'; + import { PlantaEvents } from '@manacore/shared-utils/analytics'; import type { PlantPhoto, PlantAnalysis } from '@planta/shared'; let step = $state<'upload' | 'analyzing' | 'result'>('upload'); @@ -67,6 +68,7 @@ } analysis = analysisResult; + PlantaEvents.plantAnalyzed(); // Set default plant name from analysis if (analysisResult.commonNames && analysisResult.commonNames.length > 0) { @@ -105,6 +107,7 @@ return; } + PlantaEvents.plantCreated(); // Navigate to plant detail goto(`/plants/${plant.id}`); } diff --git a/apps/planta/apps/web/src/routes/(app)/dashboard/+page.svelte b/apps/planta/apps/web/src/routes/(app)/dashboard/+page.svelte index f20718612..2a0f5dacf 100644 --- a/apps/planta/apps/web/src/routes/(app)/dashboard/+page.svelte +++ b/apps/planta/apps/web/src/routes/(app)/dashboard/+page.svelte @@ -3,6 +3,7 @@ import { goto } from '$app/navigation'; import { plantsApi } from '$lib/api/plants'; import { wateringApi } from '$lib/api/watering'; + import { PlantaEvents } from '@manacore/shared-utils/analytics'; import type { Plant, WateringStatus } from '@planta/shared'; let plants = $state([]); @@ -42,6 +43,7 @@ e.stopPropagation(); const success = await wateringApi.logWatering(plantId); if (success) { + PlantaEvents.plantWatered(); // Refresh watering status wateringStatus = await wateringApi.getUpcoming(); } diff --git a/apps/planta/apps/web/src/routes/(app)/plants/[id]/+page.svelte b/apps/planta/apps/web/src/routes/(app)/plants/[id]/+page.svelte index cf091012c..48d34ee97 100644 --- a/apps/planta/apps/web/src/routes/(app)/plants/[id]/+page.svelte +++ b/apps/planta/apps/web/src/routes/(app)/plants/[id]/+page.svelte @@ -4,6 +4,7 @@ import { goto } from '$app/navigation'; import { plantsApi } from '$lib/api/plants'; import { wateringApi } from '$lib/api/watering'; + import { PlantaEvents } from '@manacore/shared-utils/analytics'; import type { PlantWithDetails, WateringLog } from '@planta/shared'; let plant = $state(null); @@ -34,6 +35,7 @@ watering = true; const success = await wateringApi.logWatering(plant.id); if (success) { + PlantaEvents.plantWatered(); // Reload plant data await loadPlant(plant.id); } @@ -46,6 +48,7 @@ const success = await plantsApi.delete(plant.id); if (success) { + PlantaEvents.plantDeleted(); goto('/dashboard'); } } diff --git a/apps/questions/apps/web/src/lib/stores/collections.svelte.ts b/apps/questions/apps/web/src/lib/stores/collections.svelte.ts index 17326f85d..ed2b117c4 100644 --- a/apps/questions/apps/web/src/lib/stores/collections.svelte.ts +++ b/apps/questions/apps/web/src/lib/stores/collections.svelte.ts @@ -5,6 +5,7 @@ */ import { collectionsApi } from '$lib/api/collections'; +import { QuestionsEvents } from '@manacore/shared-utils/analytics'; import type { Collection, CreateCollectionDto, UpdateCollectionDto } from '$lib/types'; import { authStore } from './auth.svelte'; import { DEMO_COLLECTION, isDemoCollection } from '$lib/data/demo-questions'; @@ -76,6 +77,7 @@ export const collectionsStore = { try { const collection = await collectionsApi.create(data); collections = [...collections, collection]; + QuestionsEvents.collectionCreated(); return collection; } catch (e) { error = e instanceof Error ? e.message : 'Failed to create collection'; @@ -126,6 +128,7 @@ export const collectionsStore = { try { await collectionsApi.delete(id); collections = collections.filter((c) => c.id !== id); + QuestionsEvents.collectionDeleted(); if (selectedId === id) { selectedId = null; } diff --git a/apps/questions/apps/web/src/lib/stores/questions.svelte.ts b/apps/questions/apps/web/src/lib/stores/questions.svelte.ts index 441c1f833..b8d86bc88 100644 --- a/apps/questions/apps/web/src/lib/stores/questions.svelte.ts +++ b/apps/questions/apps/web/src/lib/stores/questions.svelte.ts @@ -5,6 +5,7 @@ */ import { questionsApi, type QuestionFilters } from '$lib/api/questions'; +import { QuestionsEvents } from '@manacore/shared-utils/analytics'; import type { Question, CreateQuestionDto, UpdateQuestionDto } from '$lib/types'; import { authStore } from './auth.svelte'; import { generateDemoQuestions, isDemoQuestion } from '$lib/data/demo-questions'; @@ -104,6 +105,7 @@ export const questionsStore = { const question = await questionsApi.create(data); questions = [question, ...questions]; total++; + QuestionsEvents.questionCreated(data.researchDepth || 'standard'); return question; } catch (e) { error = e instanceof Error ? e.message : 'Failed to create question'; @@ -155,6 +157,7 @@ export const questionsStore = { await questionsApi.delete(id); questions = questions.filter((q) => q.id !== id); total--; + QuestionsEvents.questionDeleted(); return true; } catch (e) { error = e instanceof Error ? e.message : 'Failed to delete question'; diff --git a/apps/questions/apps/web/src/routes/(app)/question/[id]/+page.svelte b/apps/questions/apps/web/src/routes/(app)/question/[id]/+page.svelte index d80012c37..9db86b32e 100644 --- a/apps/questions/apps/web/src/routes/(app)/question/[id]/+page.svelte +++ b/apps/questions/apps/web/src/routes/(app)/question/[id]/+page.svelte @@ -4,6 +4,7 @@ import { questionsApi } from '$lib/api/questions'; import { researchApi } from '$lib/api/research'; import { sourcesApi } from '$lib/api/sources'; + import { QuestionsEvents } from '@manacore/shared-utils/analytics'; import { QuestionDetailSkeleton, ErrorAlert } from '$lib/components'; import { ArrowLeft, @@ -67,6 +68,7 @@ questionId: question.id, depth: question.researchDepth, }); + QuestionsEvents.researchStarted(question.researchDepth); researchResults = [result, ...researchResults]; sources = await sourcesApi.getByQuestion(question.id); // Reload question to get updated status diff --git a/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts b/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts index 6e16619c5..852af0e7d 100644 --- a/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts +++ b/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts @@ -1,5 +1,6 @@ import type { Skill, Activity, UserStats, SkillBranch, AchievementUnlockResult } from '$lib/types'; import { calculateLevel, createDefaultSkill, createActivity, BRANCH_INFO } from '$lib/types'; +import { SkillTreeEvents } from '@manacore/shared-utils/analytics'; import * as storage from '$lib/services/storage'; import * as skillsApi from '$lib/api/skills'; import * as activitiesApi from '$lib/api/activities'; @@ -128,6 +129,7 @@ async function addSkill(data: Partial): Promise { color: data.color ?? undefined, }); skills = [...skills, result.skill]; + SkillTreeEvents.skillCreated(data.branch || 'custom'); await updateStats(); if (result.newAchievements?.length > 0) { achievementStore.handleApiUnlocks(result.newAchievements); @@ -137,6 +139,7 @@ async function addSkill(data: Partial): Promise { const skill = createDefaultSkill(data); await storage.saveSkill(skill); skills = [...skills, skill]; + SkillTreeEvents.skillCreated(data.branch || 'custom'); await updateStats(); return skill; } @@ -171,6 +174,7 @@ async function deleteSkill(id: string): Promise { await storage.deleteSkill(id); } skills = skills.filter((s) => s.id !== id); + SkillTreeEvents.skillDeleted(); activities = activities.filter((a) => a.skillId !== id); await updateStats(); } @@ -188,6 +192,7 @@ async function addXp( const result = await skillsApi.addXp(skillId, { xp, description, duration }); skills = [...skills.slice(0, index), result.skill, ...skills.slice(index + 1)]; activities = [...activities, result.activity]; + SkillTreeEvents.xpAdded(xp, result.leveledUp); await updateStats(); if (result.newAchievements?.length > 0) { achievementStore.handleApiUnlocks(result.newAchievements); diff --git a/packages/shared-utils/src/analytics.ts b/packages/shared-utils/src/analytics.ts index b248aadde..1591434de 100644 --- a/packages/shared-utils/src/analytics.ts +++ b/packages/shared-utils/src/analytics.ts @@ -275,6 +275,49 @@ export const ManaCoreEvents = { profileUpdated: () => trackEvent('profile_updated'), }; +/** + * Context App Events + */ +export const ContextEvents = { + documentCreated: (type: string) => trackEvent('document_created', { type }), + documentDeleted: () => trackEvent('document_deleted'), + documentPinned: (pinned: boolean) => trackEvent('document_pinned', { pinned }), + spaceCreated: () => trackEvent('space_created'), + spaceDeleted: () => trackEvent('space_deleted'), + aiGenerated: () => trackEvent('ai_generated'), +}; + +/** + * SkillTree App Events + */ +export const SkillTreeEvents = { + skillCreated: (branch: string) => trackEvent('skill_created', { branch }), + skillDeleted: () => trackEvent('skill_deleted'), + xpAdded: (xp: number, leveledUp: boolean) => + trackEvent('xp_added', { xp, leveled_up: leveledUp }), +}; + +/** + * Planta App Events + */ +export const PlantaEvents = { + plantAnalyzed: () => trackEvent('plant_analyzed'), + plantCreated: () => trackEvent('plant_created'), + plantDeleted: () => trackEvent('plant_deleted'), + plantWatered: () => trackEvent('plant_watered'), +}; + +/** + * Questions App Events + */ +export const QuestionsEvents = { + questionCreated: (depth: string) => trackEvent('question_created', { depth }), + questionDeleted: () => trackEvent('question_deleted'), + researchStarted: (depth: string) => trackEvent('research_started', { depth }), + collectionCreated: () => trackEvent('collection_created'), + collectionDeleted: () => trackEvent('collection_deleted'), +}; + /** * Photos App Events */