From be151fd6f7e7860cc60bee67bab815c155e86fcd Mon Sep 17 00:00:00 2001 From: Fuwn Date: Fri, 29 May 2026 23:05:37 +0000 Subject: feat(analytics): track umami events across core actions Add a guarded track() wrapper (src/lib/analytics.ts) so programmatic events no-op instead of throwing when umami is absent (localhost), lazily loaded, or ad-blocked. Instrument the core loop (progress increment, roulette, list filter, force refresh), the auth funnel (log in/out, command palette), settings toggles/selects at the wrapper level, sync/debug/feed actions, and tool/sharing actions. --- src/lib/Announcement.svelte | 4 +++- src/lib/CommandPalette/CommandPalette.svelte | 11 ++++++++++- src/lib/CommandPalette/authActions.ts | 5 +++++ src/lib/LandingHero.svelte | 3 +++ src/lib/List/Anime/CleanAnimeList.svelte | 13 ++++++++++++- src/lib/List/Manga/CleanMangaList.svelte | 11 ++++++++++- src/lib/List/MediaRoulette.svelte | 4 ++++ src/lib/Notification/Notification.svelte | 1 + src/lib/Settings/Categories/Debug.svelte | 6 +++++- src/lib/Settings/Categories/Display.svelte | 27 ++++++++++++++++++++++---- src/lib/Settings/Categories/RSSFeeds.svelte | 1 + src/lib/Settings/Categories/SettingSync.svelte | 4 ++++ src/lib/Settings/SettingCheckboxToggle.svelte | 13 +++++++++++-- src/lib/Settings/SettingToggle.svelte | 20 ++++++++++--------- src/lib/Tools/ActivityHistory/Tool.svelte | 4 +++- src/lib/Tools/Tracker/Tool.svelte | 12 +++++++++--- src/lib/analytics.ts | 11 +++++++++++ 17 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 src/lib/analytics.ts (limited to 'src/lib') diff --git a/src/lib/Announcement.svelte b/src/lib/Announcement.svelte index 8bbdfe67..4338f8ff 100644 --- a/src/lib/Announcement.svelte +++ b/src/lib/Announcement.svelte @@ -47,7 +47,9 @@ const maxWidth = (input: string, max = 100) => { - + {/if} diff --git a/src/lib/CommandPalette/CommandPalette.svelte b/src/lib/CommandPalette/CommandPalette.svelte index 04eabff9..bf568200 100644 --- a/src/lib/CommandPalette/CommandPalette.svelte +++ b/src/lib/CommandPalette/CommandPalette.svelte @@ -4,6 +4,7 @@ import { fly, fade } from "svelte/transition"; import { flip } from "svelte/animate"; import type { CommandPaletteAction } from "./actions"; import locale from "$stores/locale"; +import { track } from "$lib/analytics"; export let items: CommandPaletteAction[] = []; export let open = false; @@ -103,6 +104,8 @@ $: if (open && !isVisible) { } const executeItem = (item: CommandPaletteAction) => { + track("Run Command", { command: item.name }); + if (item.onClick) item.onClick(); if (!item.preventDefault) window.location.href = item.url; @@ -158,7 +161,11 @@ const handleGlobalKey = (e: KeyboardEvent) => { open = !open; - if (open) requestAnimationFrame(() => inputRef?.focus()); + if (open) { + track("Open Command Palette"); + + requestAnimationFrame(() => inputRef?.focus()); + } } }; @@ -214,6 +221,8 @@ const handleGlobalKey = (e: KeyboardEvent) => { out:fly={{ y: -20, duration: 150 }} animate:flip={{ duration: 200 }} onclick={(e) => { + track('Run Command', { command: item.name }); + if (item.preventDefault) e.preventDefault(); if (item.onClick) item.onClick(); diff --git a/src/lib/CommandPalette/authActions.ts b/src/lib/CommandPalette/authActions.ts index 9dbc9565..3dd51bf7 100644 --- a/src/lib/CommandPalette/authActions.ts +++ b/src/lib/CommandPalette/authActions.ts @@ -1,5 +1,6 @@ import { env } from "$env/dynamic/public"; import root from "$lib/Utility/root"; +import { track } from "$lib/analytics"; import localforage from "localforage"; import locale from "$stores/locale"; import { get } from "svelte/store"; @@ -18,6 +19,8 @@ export const authActions = ( preventDefault: true, tags: ["auth", "sign", "out", "user"], onClick: async () => { + track("Log Out", { source: "command-palette" }); + await localforage.removeItem("identity"); await localforage.removeItem("commit"); @@ -38,6 +41,8 @@ export const authActions = ( preventDefault: true, tags: ["auth", "sign", "in", "anilist"], onClick: async () => { + track("Log In", { source: "command-palette" }); + await localforage.setItem( "redirect", window.location.origin + diff --git a/src/lib/LandingHero.svelte b/src/lib/LandingHero.svelte index bec9d1f4..ed019274 100644 --- a/src/lib/LandingHero.svelte +++ b/src/lib/LandingHero.svelte @@ -2,6 +2,7 @@ import { env } from "$env/dynamic/public"; import localforage from "localforage"; import lenis from "$stores/lenis"; +import { track } from "$lib/analytics"; let heroSection = $state(); @@ -35,6 +36,8 @@ const scrollPastHero = () => { class="cta" href={`https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code`} onclick={async () => { + track('Log In', { source: 'landing' }); + await localforage.setItem( 'redirect', window.location.origin + window.location.pathname + window.location.search diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte index e27378eb..c8bf1b7d 100644 --- a/src/lib/List/Anime/CleanAnimeList.svelte +++ b/src/lib/List/Anime/CleanAnimeList.svelte @@ -21,6 +21,7 @@ import stateBin from "$stores/stateBin"; import localforage from "localforage"; import MediaRoulette from "../MediaRoulette.svelte"; import type { Title } from "../mediaTitle"; +import { track } from "$lib/analytics"; export let media: Media[]; export let title: Title; @@ -117,6 +118,10 @@ $: if (browser && !dummy && media && previousAnimeList !== media) const updateSelectedList = (event: Event) => { const nextSelectedList = (event.currentTarget as HTMLSelectElement).value; + track("Filter Anime List", { + scope: nextSelectedList === "All" ? "all" : "custom", + }); + selectedList = nextSelectedList; if (!disableFilter && $stateBin[filterKey] !== nextSelectedList) @@ -213,6 +218,8 @@ onDestroy(() => clearAiringRefreshTimeout()); const increment = (anime: Media, progress: number) => { if (dummy || pendingUpdate === anime.id) return; + track("Increment Anime Progress"); + pendingUpdate = anime.id; lastUpdatedMedia = anime.id; @@ -247,6 +254,7 @@ const increment = (anime: Media, progress: number) => { class="small-button" onclick={() => (showRoulette = true)} title={$locale().lists.actions?.pickRandomAnime} + data-umami-event="Open Anime Roulette" > Roulette @@ -255,7 +263,10 @@ const increment = (anime: Media, progress: number) => { {#if media.length === 0} {$locale().lists.empty?.anime} - {:else if $settings.displayMediaListFilter && !disableFilter && hasDistinguishingList} diff --git a/src/lib/List/Manga/CleanMangaList.svelte b/src/lib/List/Manga/CleanMangaList.svelte index 25e6d48f..af52ca02 100644 --- a/src/lib/List/Manga/CleanMangaList.svelte +++ b/src/lib/List/Manga/CleanMangaList.svelte @@ -31,6 +31,7 @@ import CleanList from "../CleanList.svelte"; import stateBin from "$stores/stateBin"; import localforage from "localforage"; import MediaRoulette from "../MediaRoulette.svelte"; +import { track } from "$lib/analytics"; export let media: Media[]; export let cleanCache: () => void; @@ -105,6 +106,10 @@ $: filteredMedia = const updateSelectedList = (event: Event) => { const nextSelectedList = (event.currentTarget as HTMLSelectElement).value; + track("Filter Manga List", { + scope: nextSelectedList === "All" ? "all" : "custom", + }); + selectedList = nextSelectedList; if (!disableFilter && $stateBin[filterKey] !== nextSelectedList) @@ -125,8 +130,11 @@ $: if (rateLimited && !serviceStatusResponse) serviceStatusResponse = getMangadexServiceStatus(); const increment = (manga: Media) => { - if (!(pendingUpdate === manga.id || dummy)) + if (!(pendingUpdate === manga.id || dummy)) { + track("Increment Manga Progress"); + updateMedia(manga.id, manga.mediaListEntry?.progress, media); + } }; @@ -154,6 +162,7 @@ const increment = (manga: Media) => { class="small-button" onclick={() => (showRoulette = true)} title={$locale().lists.actions?.pickRandomManga} + data-umami-event="Open Manga Roulette" > Roulette diff --git a/src/lib/List/MediaRoulette.svelte b/src/lib/List/MediaRoulette.svelte index dc9a2269..64c585df 100644 --- a/src/lib/List/MediaRoulette.svelte +++ b/src/lib/List/MediaRoulette.svelte @@ -5,6 +5,7 @@ import { outboundLink } from "$lib/Media/links"; import settings from "$stores/settings"; import locale from "$stores/locale"; import { mediaTitle } from "./mediaTitle"; +import { track } from "$lib/analytics"; interface Props { media: Media[]; @@ -25,6 +26,8 @@ let currentMedia = $derived(media[displayIndex]); const startRoulette = () => { if (media.length === 0 || isSpinning) return; + track("Spin Roulette", { type }); + isSpinning = true; showResult = false; selectedIndex = Math.floor(Math.random() * media.length); @@ -131,6 +134,7 @@ const handleOverlayClick = (e: MouseEvent) => { href={outboundLink(currentMedia, type, $settings.displayOutboundLinksTo)} target="_blank" class="view-link" + data-umami-event="Roulette View Media" > {$locale({ values: { diff --git a/src/lib/Notification/Notification.svelte b/src/lib/Notification/Notification.svelte index 1543f3b9..3c466b90 100644 --- a/src/lib/Notification/Notification.svelte +++ b/src/lib/Notification/Notification.svelte @@ -27,6 +27,7 @@ const remove = () => { id="notification-container" class={removed ? 'fade-out' : 'fade-in'} onclick={remove} + data-umami-event="Dismiss Notification" onkeydown={() => { return; }} diff --git a/src/lib/Settings/Categories/Debug.svelte b/src/lib/Settings/Categories/Debug.svelte index a2cb35c5..a18eea9d 100644 --- a/src/lib/Settings/Categories/Debug.svelte +++ b/src/lib/Settings/Categories/Debug.svelte @@ -25,11 +25,14 @@ import { get } from "svelte/store";
- + + diff --git a/src/lib/Tools/Tracker/Tool.svelte b/src/lib/Tools/Tracker/Tool.svelte index b495522a..3185e79b 100644 --- a/src/lib/Tools/Tracker/Tool.svelte +++ b/src/lib/Tools/Tracker/Tool.svelte @@ -88,8 +88,10 @@ const deleteEntry = async (id: string) => { placeholder={$locale().tools.tracker?.progressPlaceholder} bind:value={progress} /> - addEntry(url, title, progress)} + data-umami-event="Add Tracker Entry">{$locale().common?.add} @@ -125,16 +127,20 @@ const deleteEntry = async (id: string) => { | - + diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts new file mode 100644 index 00000000..014f72db --- /dev/null +++ b/src/lib/analytics.ts @@ -0,0 +1,11 @@ +/** + * Safe wrapper around umami's programmatic `track`. The analytics script is + * skipped on localhost, lazily loaded elsewhere, and routinely blocked by + * content blockers, so a bare `umami.track(...)` throws a ReferenceError that + * would take core actions (incrementing progress, syncing settings) down with + * it. Declarative `data-umami-event` attributes are handled by the script + * itself and never need this; reach for it only from event handlers. + */ +export const track = (event: string, data?: Record): void => { + if (typeof umami !== "undefined") umami.track(event, data); +}; -- cgit v1.2.3