feat(analytics): expand umami tracking in todo, calendar, and contacts apps

Todo:
- Track projectCreated, projectDeleted, labelCreated
- Track taskUncompleted
- Track quickAddUsed via QuickInputBar

Calendar:
- Track eventUpdated, eventDeleted
- Track calendarCreated, calendarDeleted

Contacts:
- Track contactUpdated, contactDeleted, contactFavorited, contactArchived
- Track searchPerformed in SearchModal
- Track contactExported in ExportModal
- Track contactImported for both Google and file (vCard/CSV) imports

Also extends analytics event helpers with new event types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-23 09:27:30 +01:00
parent c1ef55fd54
commit dd477d5fda
12 changed files with 39 additions and 5 deletions

View file

@ -8,6 +8,7 @@ import * as api from '$lib/api/calendars';
import { BIRTHDAY_CALENDAR } from '$lib/api/birthdays';
import { settingsStore } from './settings.svelte';
import { authStore } from './auth.svelte';
import { CalendarEvents } from '@manacore/shared-utils/analytics';
// Guest calendar for unauthenticated users
const GUEST_CALENDAR: Calendar = {
@ -128,6 +129,7 @@ export const calendarsStore = {
if (result.data) {
calendars = [...calendars, result.data];
CalendarEvents.calendarCreated();
}
return result;
@ -154,6 +156,7 @@ export const calendarsStore = {
if (!result.error) {
calendars = getCalendarsArray().filter((c) => c.id !== id);
CalendarEvents.calendarDeleted();
}
return result;

View file

@ -185,6 +185,7 @@ export const eventsStore = {
toastStore.error(`Termin konnte nicht aktualisiert werden: ${result.error.message}`);
} else if (result.data) {
events = events.map((e) => (e.id === id ? result.data! : e));
CalendarEvents.eventUpdated();
}
return result;
@ -207,6 +208,7 @@ export const eventsStore = {
}
toastStore.error(`Termin konnte nicht gelöscht werden: ${result.error.message}`);
} else {
CalendarEvents.eventDeleted();
toastStore.success('Termin gelöscht');
}

View file

@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import { contactsApi, type Contact } from '$lib/api/contacts';
import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte';
import { ContactsEvents } from '@manacore/shared-utils/analytics';
interface Props {
open: boolean;
@ -47,6 +48,7 @@
});
results = response.contacts || [];
selectedIndex = 0;
ContactsEvents.searchPerformed();
} catch (e) {
console.error('Search error:', e);
results = [];

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { exportApi, type ExportFormat } from '$lib/api/export';
import { ContactsEvents } from '@manacore/shared-utils/analytics';
interface Props {
isOpen: boolean;
@ -25,6 +26,7 @@
contactIds: selectedContactIds.length > 0 ? selectedContactIds : undefined,
includeArchived,
});
ContactsEvents.contactExported(format as 'csv' | 'vcard');
onClose();
} catch (e) {
error = e instanceof Error ? e.message : 'Export fehlgeschlagen';

View file

@ -11,6 +11,7 @@
} from '$lib/api/google';
import { contactsStore } from '$lib/stores/contacts.svelte';
import { GoogleImportSkeleton } from '$lib/components/skeletons';
import { ContactsEvents } from '@manacore/shared-utils/analytics';
type Step = 'connect' | 'select' | 'result';
@ -128,6 +129,7 @@
try {
result = await googleApi.importContacts(Array.from(selectedContacts));
step = 'result';
ContactsEvents.contactImported('google', result.imported);
// Refresh contacts list
await contactsStore.loadContacts();
} catch (e) {

View file

@ -222,6 +222,7 @@ export const contactsStore = {
if (selectedContact?.id === id) {
selectedContact = contact;
}
ContactsEvents.contactUpdated();
return contact;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to update contact';
@ -258,6 +259,7 @@ export const contactsStore = {
if (selectedContact?.id === id) {
selectedContact = null;
}
ContactsEvents.contactDeleted();
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to delete contact';
console.error('Failed to delete contact:', e);
@ -284,6 +286,7 @@ export const contactsStore = {
if (selectedContact?.id === id) {
selectedContact = contact;
}
ContactsEvents.contactFavorited();
return contact;
} catch (e) {
console.error('Failed to toggle favorite:', e);
@ -309,6 +312,7 @@ export const contactsStore = {
if (selectedContact?.id === id) {
selectedContact = null;
}
ContactsEvents.contactArchived();
return contact;
} catch (e) {
console.error('Failed to toggle archive:', e);

View file

@ -9,6 +9,7 @@
import { exportApi, type ExportFormat } from '$lib/api/export';
import { contactsStore } from '$lib/stores/contacts.svelte';
import { ImportPreviewSkeleton } from '$lib/components/skeletons';
import { ContactsEvents } from '@manacore/shared-utils/analytics';
import '$lib/i18n';
type Tab = 'import' | 'export';
@ -111,6 +112,8 @@
try {
importResult = await importApi.execute(preview.contacts, duplicateAction, skipIndices);
importStep = 'result';
const fileExt = selectedFile?.name?.endsWith('.csv') ? 'csv' : 'vcard';
ContactsEvents.contactImported(fileExt as 'csv' | 'vcard', importResult?.imported);
await contactsStore.loadContacts();
} catch (e) {
importError = e instanceof Error ? e.message : 'Fehler beim Importieren';

View file

@ -7,6 +7,7 @@
import type { Label } from '$lib/api/labels';
import * as labelsApi from '$lib/api/labels';
import { TodoEvents } from '@manacore/shared-utils/analytics';
// State
let labels = $state<Label[]>([]);
@ -65,6 +66,7 @@ export const labelsStore = {
try {
const newLabel = await labelsApi.createLabel(data);
labels = [...labels, newLabel];
TodoEvents.labelCreated();
return newLabel;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to create label';

View file

@ -6,6 +6,7 @@
import type { Project } from '@todo/shared';
import * as projectsApi from '$lib/api/projects';
import { authStore } from './auth.svelte';
import { TodoEvents } from '@manacore/shared-utils/analytics';
// Guest inbox project for unauthenticated users
const GUEST_INBOX: Project = {
@ -108,6 +109,7 @@ export const projectsStore = {
try {
const newProject = await projectsApi.createProject(data);
projects = [...projects, newProject];
TodoEvents.projectCreated();
return newProject;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to create project';
@ -145,6 +147,7 @@ export const projectsStore = {
try {
await projectsApi.deleteProject(id);
projects = projects.filter((p) => p.id !== id);
TodoEvents.projectDeleted();
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to delete project';
console.error('Failed to delete project:', e);

View file

@ -390,6 +390,7 @@ export const tasksStore = {
try {
const uncompletedTask = await tasksApi.uncompleteTask(id);
tasks = tasks.map((t) => (t.id === id ? uncompletedTask : t));
TodoEvents.taskUncompleted();
return uncompletedTask;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to uncomplete task';

View file

@ -36,6 +36,7 @@
import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from '$lib/utils/task-parser';
import { todoOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
import { TodoEvents } from '@manacore/shared-utils/analytics';
// App switcher items
const appItems = getPillAppItems('todo');
@ -102,6 +103,7 @@
projectId: resolved.projectId,
labelIds: resolved.labelIds,
});
TodoEvents.quickAddUsed();
} catch (error) {
console.error('Failed to create task:', error);
}

View file

@ -189,10 +189,14 @@ export const TodoEvents = {
taskCreated: (hasDeadline = false) => trackEvent('task_created', { has_deadline: hasDeadline }),
taskCompleted: () => trackEvent('task_completed'),
taskDeleted: () => trackEvent('task_deleted'),
taskUncompleted: () => trackEvent('task_uncompleted'),
subtaskCompleted: () => trackEvent('subtask_completed'),
projectCreated: () => trackEvent('project_created'),
projectDeleted: () => trackEvent('project_deleted'),
labelCreated: () => trackEvent('label_created'),
viewChanged: (view: 'inbox' | 'today' | 'upcoming' | 'project') =>
trackEvent('view_changed', { view }),
viewChanged: (view: string) => trackEvent('view_changed', { view }),
quickAddUsed: () => trackEvent('quick_add_used'),
filterUsed: (filterType: string) => trackEvent('filter_used', { filter: filterType }),
};
/**
@ -203,9 +207,11 @@ export const CalendarEvents = {
eventUpdated: () => trackEvent('event_updated'),
eventDeleted: () => trackEvent('event_deleted'),
calendarCreated: () => trackEvent('calendar_created'),
calendarDeleted: () => trackEvent('calendar_deleted'),
calendarShared: () => trackEvent('calendar_shared'),
viewChanged: (view: 'day' | 'week' | 'month' | 'agenda') => trackEvent('view_changed', { view }),
viewChanged: (view: string) => trackEvent('view_changed', { view }),
reminderSet: (minutesBefore: number) => trackEvent('reminder_set', { minutes: minutesBefore }),
eventDragged: () => trackEvent('event_dragged'),
};
/**
@ -229,8 +235,10 @@ export const ContactsEvents = {
contactCreated: () => trackEvent('contact_created'),
contactUpdated: () => trackEvent('contact_updated'),
contactDeleted: () => trackEvent('contact_deleted'),
contactImported: (source: 'google' | 'csv' | 'vcard') =>
trackEvent('contact_imported', { source }),
contactFavorited: () => trackEvent('contact_favorited'),
contactArchived: () => trackEvent('contact_archived'),
contactImported: (source: 'google' | 'csv' | 'vcard', count?: number) =>
trackEvent('contact_imported', { source, ...(count !== undefined && { count }) }),
contactExported: (format: 'csv' | 'vcard') => trackEvent('contact_exported', { format }),
tagCreated: () => trackEvent('tag_created'),
searchPerformed: () => trackEvent('search_performed'),