diff --git a/.claude/skills/event-tracking/SKILL.md b/.claude/skills/event-tracking/SKILL.md new file mode 100644 index 000000000..aea8bf698 --- /dev/null +++ b/.claude/skills/event-tracking/SKILL.md @@ -0,0 +1,206 @@ +--- +name: event-tracking +description: > + Add event tracking calls to Vue/Nuxt components in the Insights app using the useTrackEvent composable. + Use this skill whenever a developer asks to "track" a user action or page view, "add analytics", "instrument" a + component, "fire an event", implement an event from the events catalog, or wire up tracking for anything in + the Insights frontend — even if they just say "track when the user clicks X" or "add tracking to this page". +--- + +# Event Tracking — Insights App + +## What this skill does + +Helps you add the right `trackEvent()` call to the right place in the codebase, using the catalog-approved event definitions. + +## File structure + +``` +frontend/app/components/shared/types/events/ +├── index.ts # EventType, EventFeature enums; EventDefinition interface; +│ # EventKey union type; aggregated EVENT_DEFINITIONS record +└── collections.ts # CollectionsEventKey enum + COLLECTIONS_EVENT_DEFINITIONS + .ts # (future) FeatureEventKey enum + FEATURE_EVENT_DEFINITIONS + +frontend/composables/useTrackEvent.ts # just the tracking function +``` + +**Adding events for a new feature:** create `events/.ts`, define its `EventKey` enum and `_EVENT_DEFINITIONS` record, then re-export both from `index.ts`. + +**`index.ts` shape:** +```ts +export type EventKey = CollectionsEventKey // | FutureFeatureEventKey | ... +export const EVENT_DEFINITIONS: Record = { + ...COLLECTIONS_EVENT_DEFINITIONS, + // ...FUTURE_FEATURE_EVENT_DEFINITIONS, +} +``` + +## How to use + +Always use the **feature-specific key enum** (e.g. `CollectionsEventKey`), not the union `EventKey` type. The composable itself only needs `useTrackEvent` — the key enum comes from the feature's events file. + +```ts +import { useTrackEvent } from '~~/composables/useTrackEvent'; +import { CollectionsEventKey } from '~/components/shared/types/events/collections'; + +const { trackEvent } = useTrackEvent() + +trackEvent({ + key: CollectionsEventKey.CREATE_COLLECTION, + properties: { collectionId, isPrivate }, // catalog-defined fields only — optional +}) +``` + +`name`, `type`, `description`, and `feature` are looked up automatically from `EVENT_DEFINITIONS` — **never pass them in the call**. + +`source` (current URL) and `entrySource` (referrer) are captured **automatically** — never pass them manually. + +The call is always fire-and-forget: errors are caught inside the composable and never bubble up. + +## Step-by-step workflow + +### 1. Read the events catalog + +Always start by reading `references/events-catalog.md` to find the event that matches what the developer described. The catalog is organized by feature section (e.g. "Community Collections"). Note the `enum value` and `properties` columns — use those exactly. + +If no catalog entry matches, note this to the developer and suggest the closest match or a new entry following the conventions at the bottom of the catalog. + +### 2. Identify the target file and feature + +Infer the feature from context (e.g. "track when the user creates a collection" → Community Collections). This tells you which key enum to import: + +| Feature | Key enum | Import path | +|---------|----------|-------------| +| Community Collections | `CollectionsEventKey` | `~/components/shared/types/events/collections` | +| (future features) | `EventKey` | `~/components/shared/types/events/` | + +Read the target component or page file before making any changes. You need to understand: +- Whether `useTrackEvent` is already imported +- For **feature** events: which function handles the user action +- For **page** events: whether `onMounted` already exists + +### 3. Insert the tracking call + +#### Feature events + +Place `trackEvent(...)` inside the action handler, **after** the main logic succeeds. + +```ts +const handleCreateCollection = async () => { + const result = await createCollection(...) + + trackEvent({ + key: CollectionsEventKey.CREATE_COLLECTION, + properties: { collectionId: result.id, isPrivate: form.isPrivate }, + }) +} +``` + +For abandonment events, fire when the user dismisses or navigates away without completing the flow — typically on modal close before the success state is reached. + +#### Page events + +Place inside `onMounted()`. Add to an existing `onMounted` if one already exists. + +```ts +onMounted(() => { + trackEvent({ key: CollectionsEventKey.VIEW_DISCOVER_COLLECTIONS }) +}) +``` + +#### Events that need async data (e.g. `viewerType`) + +If the event requires data that loads asynchronously (e.g. collection details needed to determine `viewerType`), use `watch` with `{ once: true }` instead of `onMounted`: + +```ts +watch(data, (collection) => { + if (!collection) return + trackEvent({ + key: CollectionsEventKey.VIEW_COLLECTION, + properties: { + collectionId: collection.id, + viewerType: collection.ssoUserId && user.value?.sub === collection.ssoUserId ? 'owner' : 'guest', + }, + }) +}, { once: true }) +``` + +### 4. Add the imports (if missing) + +```ts +import { useTrackEvent } from '~~/composables/useTrackEvent'; +import { CollectionsEventKey } from '~/components/shared/types/events/collections'; +``` + +Then destructure at the top level of ` diff --git a/frontend/app/pages/collection/index.vue b/frontend/app/pages/collection/index.vue index 3c0097155..bcb585cfa 100644 --- a/frontend/app/pages/collection/index.vue +++ b/frontend/app/pages/collection/index.vue @@ -9,7 +9,19 @@ SPDX-License-Identifier: MIT