Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions .claude/skills/telemetry-standards/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
name: telemetry-standards
description: PostHog event tracking standards for Supabase Studio. Use when reviewing
PRs for telemetry compliance or implementing new event tracking. Covers event naming,
property conventions, approved patterns, and implementation guide.
---

# Telemetry Standards for Supabase Studio

Standards for PostHog event tracking in `apps/studio/`. Apply these when
reviewing PRs that touch tracking or when implementing new tracking.

## Event Naming

**Format:** `[object]_[verb]` in snake_case

**Approved verbs only:**
opened, clicked, submitted, created, removed, updated, retrieved, intended, evaluated, added,
enabled, disabled, copied, exposed, failed, converted

**Flag these:**
- Unapproved verbs (saved, viewed, seen, pressed, etc.)
- Wrong order: `click_product_card` → should be `product_card_clicked`
- Wrong casing: `productCardClicked` → should be `product_card_clicked`

**Good examples:**
- `product_card_clicked`
- `backup_button_clicked`
- `sql_query_submitted`

**Common mistakes with corrections:**
- `database_saved` → `save_button_clicked` or `database_updated` (unapproved verb)
- `click_backup_button` → `backup_button_clicked` (wrong order)
- `dashboardViewed` → don't track passive views on page load
- `component_rendered` → don't track — no user interaction

## Property Standards

**Casing:** camelCase preferred for new events. The codebase has existing snake_case properties (e.g., `schema_name`, `table_name`) — when adding properties to an existing event, match its established convention.

**Names must be self-explanatory:**
- `{ productType: 'database', planTier: 'pro' }`
- `{ assistantType: 'sql', suggestionType: 'optimization' }`

**Flag these:**
- Generic names: `label`, `value`, `name`, `data`
- PascalCase properties
- Inconsistent names across similar events (e.g., `assistantType` in one event, `aiType` in a related event)
- Mixing camelCase and snake_case within the same event

## What NOT to Track

- Passive views/renders on page load (`dashboard_viewed`, `sidebar_appeared`, `page_loaded`)
- Component appearances without user interaction
- Generic "viewed" or "seen" events — already captured by pageview events

**DO track:** user clicks, form submissions, explicit opens/closes, user-initiated actions.

**Exception:** `_exposed` events for A/B experiment exposure tracking are valid even though they fire on render.

**Never track PII** (emails, names, IPs, etc.) in event properties.

## Required Pattern

Import `useTrack` from `lib/telemetry/track` (within `apps/studio/`). Never use `useSendEventMutation` (deprecated).

```typescript
import { useTrack } from 'lib/telemetry/track'

const MyComponent = () => {
const track = useTrack()

const handleClick = () => {
track('product_card_clicked', {
productType: 'database',
planTier: 'pro',
source: 'dashboard',
})
}

return <button onClick={handleClick}>Click me</button>
}
```

## Event Definitions

All events must be defined as TypeScript interfaces in `packages/common/telemetry-constants.ts`:

```typescript
/**
* [Event description]
*
* @group Events
* @source [what triggers this event]
*/
export interface MyFeatureClickedEvent {
action: 'my_feature_clicked'
properties: {
/** Description of property */
featureType: string
}
groups: TelemetryGroups
}
```

Add the new interface to the `TelemetryEvent` union type so `useTrack` picks it up.
`@group Events` and `@source` must be accurate.

## Review Rules

When reviewing a PR, flag these as **required changes:**

1. **Naming violations** — event not following `[object]_[verb]` snake_case, or using an unapproved verb
2. **Property violations** — not camelCase, generic names, or inconsistent with similar events
3. **Deprecated hook** — any usage of `useSendEventMutation` instead of `useTrack`
4. **Unnecessary view tracking** — events that fire on page load without user interaction
5. **Inaccurate docs** — `@page`/`@source` descriptions that don't match the actual implementation

When a PR adds user-facing interactions (buttons, forms, toggles, modals) **without** tracking, suggest:
- "This adds a user interaction that may benefit from tracking."
- Propose the event name following `[object]_[verb]` convention
- Propose the `useTrack()` call with suggested properties

When checking property consistency, search `packages/common/telemetry-constants.ts` for similar events and verify property names match.

## Well-Formed Event Examples

From the actual codebase:

```typescript
// User copies a connection string
track('connection_string_copied', {
connectionType: 'psql',
connectionMethod: 'transaction_pooler',
connectionTab: 'Connection String',
})

// User enables a feature preview
track('feature_preview_enabled', {
feature: 'realtime_inspector',
})

// User clicks a banner CTA
track('index_advisor_banner_dismiss_button_clicked')

// Experiment exposure (fires on render — valid exception)
track('home_new_experiment_exposed', {
variant: 'treatment',
})
```

## Implementing New Tracking

To add tracking for a user action:

1. **Name the event** — `[object]_[verb]` using approved verbs only
2. **Choose properties** — camelCase preferred for new events; check `packages/common/telemetry-constants.ts` for similar events and match their property names and casing
3. **Add interface to telemetry-constants.ts** — with `@group Events` and `@source` JSDoc, add to the `TelemetryEvent` union type
4. **Add to component** — `import { useTrack } from 'lib/telemetry/track'`, call `track('event_name', { properties })`

### Verification checklist

- [ ] Event name follows `[object]_[verb]` with approved verb
- [ ] Event name is snake_case
- [ ] Properties are camelCase and self-explanatory
- [ ] Event defined in telemetry-constants.ts with accurate `@page`/`@source`
- [ ] Using `useTrack` hook (not `useSendEventMutation`)
- [ ] Not tracking passive views/appearances
- [ ] No PII in event properties (emails, names, IPs, etc.)
- [ ] Property names consistent with similar events
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ database_id = "ba593989-12b5-464f-8ede-a525e1ca2ffb"

**What is Egress?**

Egress (also known as bandwidth) is any amount of network packets/bytes being streamed back to a connected client. Means, the data that is leaving the Supabase platform. Egress in Supabase includes any calls through PostgREST, to Storage, Realtime, Auth, Edge Functions, Database and Supavisor.
Egress (also known as bandwidth) is any amount of network packets/bytes being streamed to a connected client from your project. Means, the data that is leaving the Supabase platform. Egress in Supabase includes any calls through PostgREST, to Storage, Realtime, Auth, Edge Functions, Database and Supavisor.

You can read about Unified egress, included quota, and how to check the egress usage here: https://supabase.com/docs/guides/platform/manage-your-usage/egress. Additionally, the [project reports](/dashboard/project/_/observability) have a few egress related stats. You can create a custom report to look into daily egress.

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ExternalLink } from 'lucide-react'
import { useTheme } from 'next-themes'
import Image from 'next/image'
import Link from 'next/link'

import { useFlag, useParams } from 'common'
import {
ScaffoldSection,
Expand All @@ -19,10 +14,15 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { BASE_PATH, DOCS_URL } from 'lib/constants'
import { MANAGED_BY } from 'lib/constants/infrastructure'
import { ExternalLink } from 'lucide-react'
import { useTheme } from 'next-themes'
import Image from 'next/image'
import Link from 'next/link'
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
import { Alert, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
import { Alert, Alert_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip'

import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip'
import SpendCapSidePanel from './SpendCapSidePanel'

export interface CostControlProps {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,28 @@ export interface ProjectUpdateDisabledTooltipProps {
tooltip?: string
}

const ProjectUpdateDisabledTooltip = ({
export const ProjectUpdateDisabledTooltip = ({
projectUpdateDisabled,
projectNotActive = false,
children,
tooltip,
}: PropsWithChildren<ProjectUpdateDisabledTooltipProps>) => {
const showTooltip = projectUpdateDisabled || projectNotActive
const tooltipMessage =
tooltip ||
(projectUpdateDisabled
? 'Subscription changes are currently disabled. Our engineers are working on a fix.'
: projectNotActive
? 'Unable to update subscription as project is currently not active'
: undefined)

return (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
{showTooltip && (
<TooltipContent side="bottom" className="w-72 text-center">
{projectUpdateDisabled
? tooltip ||
'Subscription changes are currently disabled. Our engineers are working on a fix.'
: projectNotActive
? 'Unable to update subscription as project is currently not active'
: ''}
{tooltipMessage !== undefined && (
<TooltipContent side="bottom" className="w-64 text-center">
{tooltipMessage}
</TooltipContent>
)}
</Tooltip>
)
}

export default ProjectUpdateDisabledTooltip
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { MinusCircle, PauseCircle } from 'lucide-react'

import { getComputeSize, OrgProject } from 'data/projects/org-projects-infinite-query'
import type { OrgSubscription, ProjectAddon } from 'data/subscriptions/types'
import { MinusCircle, PauseCircle } from 'lucide-react'
import { useMemo } from 'react'
import { plans as subscriptionsPlans } from 'shared-data/plans'
import { Modal } from 'ui'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useState } from 'react'
import { toast } from 'sonner'

import { useFlag, useParams } from 'common'
import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants'
import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send'
import { getComputeSize, OrgProject } from 'data/projects/org-projects-infinite-query'
import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation'
import { useState } from 'react'
import { toast } from 'sonner'
import { Alert, Button, cn, Input, Modal } from 'ui'
import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip'

import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip'

export interface ExitSurveyModalProps {
visible: boolean
Expand Down Expand Up @@ -104,7 +104,7 @@ export const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalP
<Modal.Content>
<div className="space-y-4">
<p className="text-sm text-foreground-light">
Share with us why you're downgrading your plan.
What made you decide to downgrade your plan?
</p>
<div className="space-y-8 mt-6">
<div className="flex flex-wrap gap-2" data-toggle="buttons">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/constants'
import Link from 'next/link'

import { useFlag, useParams } from 'common'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import {
Expand All @@ -12,11 +10,13 @@ import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import Link from 'next/link'
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
import { Alert, Button } from 'ui'
import { Admonition } from 'ui-patterns'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import ProjectUpdateDisabledTooltip from '../ProjectUpdateDisabledTooltip'

import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip'
import { Restriction } from '../Restriction'
import { PlanUpdateSidePanel } from './PlanUpdateSidePanel'

Expand Down
22 changes: 16 additions & 6 deletions apps/studio/components/interfaces/Settings/Addons/Addons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
subscriptionHasHipaaAddon,
} from 'components/interfaces/Billing/Subscription/Subscription.utils'
import { NoticeBar } from 'components/interfaces/DiskManagement/ui/NoticeBar'
import ProjectUpdateDisabledTooltip from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip'
import { ProjectUpdateDisabledTooltip } from 'components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import {
ScaffoldContainer,
Expand All @@ -27,6 +27,7 @@ import dayjs from 'dayjs'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import {
useIsAwsCloudProvider,
useIsOrioleDbInAws,
useIsProjectActive,
useSelectedProjectQuery,
Expand All @@ -52,6 +53,7 @@ export const Addons = () => {
const { resolvedTheme } = useTheme()
const { ref: projectRef } = useParams()
const { setPanel } = useAddonsPagePanel()
const isAws = useIsAwsCloudProvider()
const isProjectActive = useIsProjectActive()
const isOrioleDbInAws = useIsOrioleDbInAws()

Expand Down Expand Up @@ -184,7 +186,7 @@ export const Addons = () => {
</ScaffoldContainer>
)}
<ScaffoldContainer>
<ScaffoldSection>
<ScaffoldSection className="!pb-12">
<ScaffoldSectionDetail>
<div className="space-y-6">
<p className="m-0">Compute Size</p>
Expand Down Expand Up @@ -358,7 +360,7 @@ export const Addons = () => {
<>
<ScaffoldDivider />
<ScaffoldContainer>
<ScaffoldSection>
<ScaffoldSection className="!pb-12">
<ScaffoldSectionDetail>
<div className="space-y-6">
<p className="m-0">Dedicated IPv4 address</p>
Expand Down Expand Up @@ -409,13 +411,21 @@ export const Addons = () => {
<ProjectUpdateDisabledTooltip
projectUpdateDisabled={projectUpdateDisabled}
projectNotActive={!isProjectActive}
tooltip={
!isAws
? 'Dedicated IPv4 address is only available for AWS projects'
: undefined
}
>
<Button
type="default"
className="mt-2 pointer-events-auto"
onClick={() => setPanel('ipv4')}
disabled={
!isProjectActive || projectUpdateDisabled || !(canUpdateIPv4 || ipv4)
!isAws ||
!isProjectActive ||
projectUpdateDisabled ||
!(canUpdateIPv4 || ipv4)
}
>
{!!ipv4
Expand All @@ -434,7 +444,7 @@ export const Addons = () => {
<ScaffoldDivider />

<ScaffoldContainer>
<ScaffoldSection>
<ScaffoldSection className="!pb-12">
<ScaffoldSectionDetail>
<div className="space-y-6">
<p className="m-0">Point in time recovery</p>
Expand Down Expand Up @@ -562,7 +572,7 @@ export const Addons = () => {
<>
<ScaffoldDivider />
<ScaffoldContainer>
<ScaffoldSection>
<ScaffoldSection className="!pb-12">
<ScaffoldSectionDetail>
<div className="space-y-6">
<p className="m-0">Custom domain</p>
Expand Down
Loading
Loading