diff --git a/.agents/skills/analytics/SKILL.md b/.agents/skills/analytics/SKILL.md new file mode 100644 index 00000000000000..20618f9dca84c0 --- /dev/null +++ b/.agents/skills/analytics/SKILL.md @@ -0,0 +1,123 @@ +--- +name: analytics +description: Instrument and discover analytics events in Sentry's frontend UI. Use when adding tracking to buttons, pages, modals, or custom interactions, when defining new analytics events, when searching for existing events, when auditing analytics coverage for a feature, or when answering questions about how users interact with a feature. Trigger on "add analytics", "track event", "instrument analytics", "analytics event", "track click", "track page view", "add tracking", "what events exist for", "audit analytics", "how many people", "how many users", "are people using", "is anyone clicking", "usage of", "who is using". +--- + +# Analytics Instrumentation + +Add analytics events to Sentry's frontend UI using established patterns. + +## Answering "How Many People Do X?" + +When the user asks about usage, adoption, or interaction counts for a feature: + +1. Find the event: search Amplitude first (fastest), fall back to grepping the codebase. +2. If the Amplitude MCP is connected, query the data directly and report results. +3. If no matching event exists, tell the user the event is not tracked — then use `AskUserQuestion` to ask whether they want to instrument it. Do not proceed to instrumentation without explicit confirmation. + +Read `references/amplitude-mcp.md` for the full discovery and querying workflow. + +## Before Any Change: Search First + +**NEVER create a new event without checking if one already exists.** + +1. Search `static/app/utils/analytics/` for events matching the feature domain. +2. Grep for keywords related to the interaction (e.g., `clicked`, `viewed`, `created`). +3. If a matching event exists, reuse it — add parameters if needed rather than creating a duplicate. + +```bash +grep -rn "keyword" static/app/utils/analytics/ --include="*.tsx" +``` + +## Event Naming Rules + +| Rule | Example | +| -------------------------------------------- | ---------------------------------------------------------------------- | +| Use `snake_case` with dots as separators | `feedback.list-item-selected` | +| First segment = feature domain | `dashboards2.`, `issue_details.`, `feedback.` | +| Middle segments = section/context (optional) | `dashboards2.edit.` | +| Last segment = action | `.clicked`, `.viewed`, `.created`, `.changed` | +| Match the existing domain file's prefix | If events are in `feedbackAnalyticsEvents.tsx`, use `feedback.` prefix | + +**Standard action suffixes:** + +| User action | Suffix | +| --------------------- | -------------------------- | +| Clicks a button/link | `.clicked` or `_clicked` | +| Views a page | `.viewed` | +| Submits a form | `.submitted` or `.created` | +| Changes a setting | `.changed` | +| Renders/loads content | `.rendered` or `.loaded` | +| Dismisses UI | `.dismissed` | +| Opens a modal/panel | `.opened` | + +## Choose the Right Tracking Pattern + +| What to track | Pattern | Open reference | +| ----------------------------------------- | ------------------------------- | ------------------------------------------------ | +| Page view on route navigation | Route analytics hooks | `references/tracking-patterns.md` § Route-Level | +| Button or link click | Button `analyticsEventKey` prop | `references/tracking-patterns.md` § Button | +| Custom interaction (toggle, drag, select) | `trackAnalytics()` call | `references/tracking-patterns.md` § Manual | +| Modal or panel open/close | `trackAnalytics()` in handler | `references/tracking-patterns.md` § Manual | +| UI area context for events | `AnalyticsArea` wrapper | `references/tracking-patterns.md` § Area Context | + +## When You Need to Define a New Event + +Read `references/event-definitions.md` for step-by-step instructions. + +## Common Mistakes and Debugging + +Read `references/troubleshooting.md` when: + +- An event isn't firing or appearing in Amplitude +- You see TypeScript errors when calling `trackAnalytics` +- You need to debug analytics locally +- You're unsure whether an event needs an Amplitude name + +## Key Files + +| File | Purpose | +| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `static/app/utils/analytics.tsx` | Master registry — all event maps merged, `trackAnalytics` export | +| `static/app/utils/analytics/{domain}AnalyticsEvents.tsx` | Domain-specific event type definitions and name maps | +| `static/app/utils/analytics/makeAnalyticsFunction.tsx` | Factory that creates typed `trackAnalytics` — do not call directly | +| `static/app/utils/routeAnalytics/useRouteAnalyticsEventNames.tsx` | Hook for route-level page view event names | +| `static/app/utils/routeAnalytics/useRouteAnalyticsParams.tsx` | Hook for route-level page view parameters | +| `static/app/components/analyticsArea.tsx` | `AnalyticsArea` component and `useAnalyticsArea` hook | +| `static/app/components/core/button/types.tsx` | Button analytics props (`analyticsEventKey`, `analyticsEventName`, `analyticsParams`) | + +## Interaction Rules + +Users of this skill may be less technical. Use `AskUserQuestion` at every decision point instead of dumping plans or code. + +| Situation | Action | +| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| Event not found, user asked a data question | Use `AskUserQuestion`: "This isn't tracked yet. Want me to add instrumentation?" | +| User confirms they want instrumentation | Go straight to implementation. Do not show code previews or step-by-step plans — just make the changes and summarize what you did. | +| Implementation is done, needs user action (e.g., Reload registration) | State the remaining step clearly in your summary. | + +**Never** dump code blocks as a "plan" and then ask "Want me to make these changes?" — either present a short plain-English summary via `AskUserQuestion` for confirmation, or proceed directly if the user already asked for instrumentation. + +## Event Pipeline + +Every `trackAnalytics` call flows through the GetSentry override in `static/gsApp/utils/rawTrackAnalyticsEvent.tsx`: + +| Destination | When it fires | What it uses | How to query | +| ------------- | ------------------------------------------- | ------------ | ------------------- | +| **Reload** | Always | `eventKey` | Redash | +| **Amplitude** | When `eventName` is non-null and org exists | `eventName` | Amplitude UI or MCP | +| **Pendo** | Same as Amplitude | `eventName` | Pendo | + +- Set `eventName` to a string (e.g., `'Logs Trace Link Clicked'`) to send to both Reload and Amplitude. This is the default for almost all events. +- Set `eventName` to `null` only for high-volume events that would be too expensive for Amplitude. These are Reload-only and queryable via Redash. +- Reload accepts events with `allow_no_schema: true` — no separate registration step is needed. +- When searching for events, note that Reload-only events (`null` name) will not appear in Amplitude search. Fall back to grepping the codebase if Amplitude returns no results. + +## Non-Negotiable Constraints + +1. **All events must be type-safe.** Every event key must exist in a `*EventParameters` type and be registered in the domain's event map. +2. **All events flow through `trackAnalytics()`.** Never call `window.analytics`, `Amplitude.track()`, or any other analytics SDK directly. +3. **Organization context is automatic.** Pass `organization` to `trackAnalytics` — the override system handles the rest. +4. **Reuse over create.** Always search for existing events before defining new ones. +5. **One event per interaction.** Do not fire multiple events for the same user action. +6. **No PII in event parameters.** Never include user emails, IP addresses, full names, or other personally identifiable information. Use opaque IDs (org ID, user ID) when identity context is needed. diff --git a/.agents/skills/analytics/SPEC.md b/.agents/skills/analytics/SPEC.md new file mode 100644 index 00000000000000..1bda6c92e28de1 --- /dev/null +++ b/.agents/skills/analytics/SPEC.md @@ -0,0 +1,76 @@ +# Analytics Instrumentation Specification + +## Intent + +Guide users and agents through adding analytics events to Sentry's frontend UI using established patterns. Prioritizes reusing existing events, enforcing type safety, and following naming conventions. Designed to be safe for less-technical users (design, PM, sales) who may not know the analytics architecture. + +## Scope + +In scope: + +- Frontend analytics event definition (TypeScript types, event maps) +- Frontend tracking instrumentation (`trackAnalytics`, route hooks, button props, `AnalyticsArea`) +- Event naming conventions and parameter typing +- Searching for and reusing existing events +- Local debugging with `DEBUG_ANALYTICS` + +Out of scope: + +- Backend analytics event creation (`src/sentry/analytics/`) +- Amplitude dashboard configuration +- Google Analytics configuration +- Performance metrics (`metric.mark`, `metric.measure`) + +## Users And Trigger Context + +- Common user requests: "add analytics to this button", "track when users view this page", "what events exist for dashboards", "I need to track clicks on the new filter" +- Should not trigger for: backend-only analytics, Sentry SDK instrumentation, performance monitoring, error tracking setup + +## Runtime Contract + +- Required first actions: search existing events before defining new ones +- Required outputs: code changes to event definition files and instrumentation call sites +- Non-negotiable constraints: all events must be type-safe; all events flow through `trackAnalytics`; reuse existing events when possible +- Expected bundled files loaded at runtime: `references/event-definitions.md`, `references/tracking-patterns.md`, `references/troubleshooting.md`, `references/amplitude-mcp.md` + +## Source And Evidence Model + +Authoritative sources: + +- `static/app/utils/analytics.tsx` — master event registry +- `static/app/utils/analytics/*.tsx` — domain event definitions +- `static/app/utils/analytics/makeAnalyticsFunction.tsx` — factory function +- `static/app/utils/routeAnalytics/` — route-level tracking hooks +- `static/app/components/analyticsArea.tsx` — area context +- `static/app/components/core/button/types.tsx` — button analytics props + +Data that must not be stored: + +- Customer organization slugs, names, or IDs +- Internal Amplitude project keys +- Reload backend credentials + +## Reference Architecture + +- `SKILL.md` contains: routing table, naming rules, non-negotiable constraints, key files +- `references/event-definitions.md` contains: step-by-step event creation, registration, examples +- `references/tracking-patterns.md` contains: route-level, button, manual, and area tracking patterns +- `references/troubleshooting.md` contains: common mistakes, debugging, anti-patterns +- `references/amplitude-mcp.md` contains: Amplitude MCP setup, event discovery, ad-hoc querying, fallback workflow + +## Validation + +- Lightweight validation: TypeScript compilation catches unregistered event keys +- Deeper validation: `DEBUG_ANALYTICS=1` in browser console confirms events fire +- Acceptance gates: event key exists in domain type, event map entry exists, `trackAnalytics` call compiles + +## Known Limitations + +- Reload backend registration is in a separate repo (`getsentry/reload`) — this skill cannot automate that step +- Button analytics props are not type-checked against the event registry +- Route analytics timing constraint (2s) is not enforced at compile time + +## Maintenance Notes + +- When to update `SKILL.md`: new tracking patterns added to the codebase, analytics architecture changes +- When to update references: existing patterns deprecated or new tracking patterns introduced diff --git a/.agents/skills/analytics/references/amplitude-mcp.md b/.agents/skills/analytics/references/amplitude-mcp.md new file mode 100644 index 00000000000000..92a44aa7e16f7b --- /dev/null +++ b/.agents/skills/analytics/references/amplitude-mcp.md @@ -0,0 +1,117 @@ +# Amplitude MCP — Discovery and Querying + +## Setup (One-Time) + +If the Amplitude MCP is not connected: + +1. Tell the user to run `/mcp` in Claude Code +2. Select **"claude.ai Amplitude"** from the list +3. Authenticate via Sentry SSO + +Once connected, the `mcp__claude_ai_Amplitude__*` tools become available. + +## Discover the Amplitude Project + +Call `get_context` to find the Sentry organization and its projects. Use the `appId` for the `sentry.io` project as the `projectId` in all subsequent Amplitude tool calls. + +## Discovery Workflow + +When a user asks "how many people do X?" or "are people using Y?": + +### Step 1: Find the Event + +Use `search` to find matching events by keyword: + +``` +mcp__claude_ai_Amplitude__search({ + queries: ["seer", "autofix"], + entityTypes: ["EVENT"], + limitPerQuery: 20, + search_goal: "Find events related to the seer feature on issue details" +}) +``` + +This returns Amplitude event names (the `eventName` from our event maps, e.g., `"Issue Details: Seer Opened"`). + +If search returns nothing, fall back to grepping the codebase: + +```bash +grep -rn "seer\|autofix" static/app/utils/analytics/ --include="*.tsx" +``` + +### Step 2: Get Event Properties (Optional) + +If the user needs to filter or break down by a property: + +``` +mcp__claude_ai_Amplitude__get_properties({ + propertyType: "event", + projectId: "", + eventType: "Issue Details: Seer Opened" +}) +``` + +### Step 3: Query the Data + +Use `query_dataset` for ad-hoc queries: + +``` +mcp__claude_ai_Amplitude__query_dataset({ + projectId: "", + definition: { + type: "eventsSegmentation", + app: "", + name: "Seer Opens Last 30 Days", + params: { + range: "Last 30 Days", + events: [{ + event_type: "Issue Details: Seer Opened", + filters: [], + group_by: [] + }], + metric: "uniques", + countGroup: "User", + groupBy: [], + interval: 1, + segments: [{ conditions: [] }] + } + } +}) +``` + +Use `query_chart` if the user provides an existing chart ID or URL. + +### Step 4: Report Results + +- State the event name used and the date range queried. +- Report unique users (not total event count) unless the user asks for totals. +- Offer to break down by property (platform, org, etc.) if the numbers need context. + +## Common Query Patterns + +| User question | Metric | Event type pattern | +| -------------------------------- | --------- | ----------------------------------------- | +| "How many people view X page?" | `uniques` | `"Page View: ..."` | +| "How many clicks on X button?" | `totals` | `"Feature: Button Clicked"` | +| "What's the funnel from X to Y?" | funnel | Use `type: "funnels"` with ordered events | +| "Are people coming back to X?" | retention | Use `type: "retention"` | + +## Searching for Dashboards and Charts + +If the user wants an existing dashboard rather than raw data: + +``` +mcp__claude_ai_Amplitude__search({ + queries: ["seer dashboard", "autofix metrics"], + entityTypes: ["DASHBOARD", "CHART"], + limitPerQuery: 10 +}) +``` + +## When the MCP Is Not Connected + +Fall back to the codebase: + +1. Grep event files for the Amplitude name (`eventName` in the event map). +2. Report the event key and Amplitude name so the user can search Amplitude manually. +3. Suggest they connect the Amplitude MCP for direct querying: run `/mcp` → select "claude.ai Amplitude". diff --git a/.agents/skills/analytics/references/event-definitions.md b/.agents/skills/analytics/references/event-definitions.md new file mode 100644 index 00000000000000..2e92c35808e352 --- /dev/null +++ b/.agents/skills/analytics/references/event-definitions.md @@ -0,0 +1,124 @@ +# Defining New Analytics Events + +## Step 1: Find or Create the Domain Event File + +Event files live in `static/app/utils/analytics/` and follow the naming pattern `{domain}AnalyticsEvents.tsx`. + +Discover existing domain files: + +```bash +ls static/app/utils/analytics/*AnalyticsEvents.tsx +``` + +**Prefer adding events to an existing domain file.** Create a new file only when the feature has no natural home in an existing domain. + +## Step 2: Add the Event Type + +Add the event key and its parameter types to the domain's `*EventParameters` type: + +```typescript +export type FeedbackEventParameters = { + // Existing events... + 'feedback.filter-applied': { + filter_type: string; + source: 'list' | 'detail'; + }; +}; +``` + +**Parameter typing rules:** + +| Rule | Example | +| ------------------------------------------------------------------------- | --------------------------------------------------- | +| Use specific string literals over `string` when values are known | `source: 'list' \| 'detail'` | +| Use `Record` for events with no custom params | `'feedback.item-rendered': Record` | +| Never use `any` for parameter types | Use `unknown` or specific types | +| Include `organization` only if you need to override automatic org context | Rarely needed | + +## Step 3: Add the Event Map Entry + +Add the event key → Amplitude display name mapping: + +```typescript +export const feedbackEventMap: Record = { + // Existing entries... + 'feedback.filter-applied': 'Feedback: Filter Applied', +}; +``` + +**Amplitude name rules:** + +| Scenario | Value | +| --------------------------------------- | ----------------------------------- | +| Event should go to Amplitude | `'Human Readable: Title Case Name'` | +| Event is Reload-only (internal metrics) | `null` | + +The Amplitude name follows `'Domain: Action Description'` format in Title Case. + +## Step 4: Register in the Master Registry + +If you created a **new domain file**, register it in `static/app/utils/analytics.tsx`: + +1. Import the type and event map: + +```typescript +import type {MyDomainEventParameters} from './analytics/myDomainAnalyticsEvents'; +import {myDomainEventMap} from './analytics/myDomainAnalyticsEvents'; +``` + +2. Add the type to the `EventParameters` interface: + +```typescript +interface EventParameters + // ...existing types + extends MyDomainEventParameters, Record> {} +``` + +3. Spread the map into `allEventMap`: + +```typescript +const allEventMap: Record = { + // ...existing maps + ...myDomainEventMap, +}; +``` + +**If you added events to an existing domain file, skip this step** — the domain is already registered. + +## Complete Example: New Event in Existing Domain + +Adding a "filter applied" event to the feedback domain: + +```typescript +// In static/app/utils/analytics/feedbackAnalyticsEvents.tsx + +export type FeedbackEventParameters = { + // ... existing events + 'feedback.filter-applied': { + filter_type: string; + source: 'list' | 'detail'; + }; +}; + +export const feedbackEventMap: Record = { + // ... existing entries + 'feedback.filter-applied': 'Feedback: Filter Applied', +}; +``` + +## Anti-Pattern: Untyped Event + +```typescript +// BAD — will cause TypeScript error, event key not registered +trackAnalytics('feedback.my-new-thing', { + organization, + some_param: 'value', +}); + +// GOOD — define the event type and map entry first, then call +trackAnalytics('feedback.filter-applied', { + organization, + filter_type: 'status', + source: 'list', +}); +``` diff --git a/.agents/skills/analytics/references/tracking-patterns.md b/.agents/skills/analytics/references/tracking-patterns.md new file mode 100644 index 00000000000000..8916e8a5cb782f --- /dev/null +++ b/.agents/skills/analytics/references/tracking-patterns.md @@ -0,0 +1,153 @@ +# Tracking Patterns + +## Route-Level Page Views + +Use route analytics hooks for tracking when a user visits a page. These fire automatically on route navigation. + +**Where:** Inside the top-level component for a route (the component rendered by React Router). + +```typescript +import {useRouteAnalyticsEventNames} from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames'; +import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; + +function MyFeaturePage() { + const organization = useOrganization(); + + // Register the page view event + useRouteAnalyticsEventNames('my_feature.viewed', 'My Feature: Viewed'); + + // Attach contextual parameters + useRouteAnalyticsParams({ + has_data: true, + tab: 'overview', + }); + + return
...
; +} +``` + +**Rules:** + +- Call `useRouteAnalyticsEventNames` exactly once per route component. +- Call `useRouteAnalyticsParams` to add context. It can be called multiple times — params merge. +- `useRouteAnalyticsParams` must be called within 2 seconds of organization context loading. +- Subcomponents can call `useRouteAnalyticsParams` to add their own params (e.g., trace status, replay availability). +- The event fires automatically — do not also call `trackAnalytics` for the same page view. + +**Real-world example** (from `static/app/views/issueDetails/groupDetails.tsx`): + +```typescript +useRouteAnalyticsEventNames('issue_details.viewed', 'Issue Details: Viewed'); +useRouteAnalyticsParams({ + ...getAnalyticsDataForGroup(group), + ...getAnalyticsDataForEvent(event), + ...getAnalyicsDataForProject(project), + tab, + group_event_type: groupEventType, +}); +``` + +## Button Click Tracking + +Use built-in button analytics props for simple click tracking. No event definition required for Reload-only tracking. + +```tsx + +``` + +**Props:** + +| Prop | Required | Purpose | +| -------------------- | -------- | ----------------------------------------------- | +| `analyticsEventKey` | Yes | Reload event key (snake_case with dots) | +| `analyticsEventName` | No | Amplitude display name. Omit to skip Amplitude. | +| `analyticsParams` | No | Additional key-value pairs sent with the event | + +**Rules:** + +- Prefer button props over a manual `trackAnalytics` call in an `onClick` handler. +- If you need the event to be type-safe and appear in the typed event registry, also define it in the domain event file. Button props work without registration but lose type safety. +- The tracking fires via `TrackingContext` — the GetSentry override wires it to Reload/Amplitude. + +## Manual `trackAnalytics()` Calls + +Use for interactions that aren't button clicks or page views: toggles, drag actions, form submissions, modal opens, etc. + +```typescript +import {trackAnalytics} from 'sentry/utils/analytics'; + +function handleFilterChange(filterType: string) { + trackAnalytics('feedback.filter-applied', { + organization, + filter_type: filterType, + source: 'list', + }); + // ... actual handler logic +} +``` + +**Rules:** + +- Always pass `organization` (string slug or Organization object). +- The event key must be defined in a `*EventParameters` type and registered in the domain event map. +- Call `trackAnalytics` at the point of user action, not in render or effects (unless tracking a "viewed" event). +- For "viewed" events in components that aren't route-level, use a `useEffect`: + +```typescript +useEffect(() => { + trackAnalytics('feedback.banner-viewed', { + organization, + }); +}, [organization]); +``` + +## Area Context + +Use `AnalyticsArea` to tag events with their UI location. This is useful when the same component appears in multiple contexts. + +```tsx +import {AnalyticsArea, useAnalyticsArea} from 'sentry/components/analyticsArea'; + +// Wrap a section of UI + + + {/* useAnalyticsArea() returns "feedback.details" */} + +; + +// Use in a component +function MyComponent() { + const area = useAnalyticsArea(); + + function handleClick() { + trackAnalytics('feedback.action-clicked', { + organization, + area, // "feedback.details" + }); + } +} +``` + +**Rules:** + +- Areas nest with dot notation: `outer.inner`. +- Use `overrideParent` to strip the outer area (e.g., for modals that should have their own top-level area). +- Do not branch app logic on the area value — it is for analytics metadata only. + +## Choosing Between Patterns + +| Situation | Use | +| -------------------------------------- | ----------------------------------------------- | +| User navigates to a page | Route analytics hooks | +| User clicks a `