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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-25 10:13:05 +01:00
parent 623ce1f051
commit 12b3c4f0f3
14 changed files with 75 additions and 4 deletions

View file

@ -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<Document[]>([]);
@ -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 };

View file

@ -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<Space[]>([]);
@ -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;
},

View file

@ -17,7 +17,7 @@ scores:
ux: 65
analytics:
pageViewTracking: true
customEvents: false
customEvents: true
authTracking: true
landingTracking: false
publicDashboard: true

View file

@ -17,7 +17,7 @@ scores:
ux: 50
analytics:
pageViewTracking: true
customEvents: false
customEvents: true
authTracking: true
landingTracking: false
publicDashboard: true

View file

@ -17,7 +17,7 @@ scores:
ux: 55
analytics:
pageViewTracking: true
customEvents: false
customEvents: true
authTracking: true
landingTracking: false
publicDashboard: true

View file

@ -17,7 +17,7 @@ scores:
ux: 72
analytics:
pageViewTracking: true
customEvents: false
customEvents: true
authTracking: true
landingTracking: false
publicDashboard: true

View file

@ -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}`);
}

View file

@ -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<Plant[]>([]);
@ -42,6 +43,7 @@
e.stopPropagation();
const success = await wateringApi.logWatering(plantId);
if (success) {
PlantaEvents.plantWatered();
// Refresh watering status
wateringStatus = await wateringApi.getUpcoming();
}

View file

@ -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<PlantWithDetails | null>(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');
}
}

View file

@ -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;
}

View file

@ -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';

View file

@ -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

View file

@ -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<Skill>): Promise<Skill> {
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<Skill>): Promise<Skill> {
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<void> {
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);

View file

@ -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
*/