Files
Pamela Chia 0bed80b340 chore(telemetry): clean up frontend event catalog (#45964)
## Summary
Resolves 13 findings (2 HIGH, 5 MEDIUM, 6 LOW) from the frontend
telemetry audit: 1 action-string collision, 1 camelCase experiment name,
9 dead events removed, 4 missing org groups attached, 1 ambiguous
property renamed, 1 raw-string property narrowed, plus consolidations
and a structural tightening on TABLE_EVENT_ACTIONS.

## Changes
### HIGH
- Rename `EventPageCtaClickedEvent.action` to
`www_event_page_cta_clicked` so it no longer collides with the pricing
CTA event (which had a different schema sharing the same action string)
- Snake_case the header-upgrade experiment exposure name
(`headerUpgradeCta_experiment_exposed` →
`header_upgrade_cta_experiment_exposed`); PostHog flag key and
`?source=` URL param unchanged

### MEDIUM
- Remove 4 dead `ProjectCreation*Step*` events (referenced a v2 route
that doesn't exist; 0 emissions)
- Remove 4 dead experiment exposure events:
`ProjectCreationRlsOptionExperimentExposed`, `HomeNewExperimentExposed`,
`TableCreateGeneratePoliciesExperimentExposed`,
`TableCreateGeneratePoliciesExperimentConverted` (0 emissions)
- Attach org group to `dpa_request_button_clicked` (0% had `$group_0`
per Hex)
- Delete `RegisterStateOfStartups2025NewsletterClicked` (interface
naming outlier, 0 emissions, page renamed to 2026)
- Rename `AssistantSuggestionRunQueryClickedEvent.category` to
`mutationType` with tightened literal union (`'functions' |
'rls-policies' | 'unknown'`)
- Attach org group to `project_creation_default_privileges_exposed` on
Vercel surface via explicit `groupOverrides` (auto-injection misses
because `useSelectedOrganizationQuery` is undefined on that page)

### LOW
- Consolidate `IndexAdvisorBannerEnableButtonClickedEvent` +
`IndexAdvisorDialogEnableButtonClickedEvent` into one event with
`origin: 'banner' | 'dialog'`
- Rename `ImportDataFileDroppedEvent` → `ImportDataFileAddedEvent` so
the interface name matches the action and the verb is on the approved
list
- Rename `LogDrainConfirmButtonSubmittedEvent` → `LogDrainRemovedEvent`
and action to `log_drain_removed` (fires on delete-confirm modal,
matches `CronJobRemovedEvent` pattern)
- Add `type` property to `CronJobRemovedEvent` (parsed from the job's
command), matching the create/update event shape
- Tighten `TABLE_EVENT_ACTIONS` values with `satisfies` against the
event union so renames in the union fail typecheck here too
- Attach org group to `www_pricing_plan_cta_clicked` at 5 emission sites
when an org is available in the page context
- Narrow `unified_logs_row_clicked.logType` from raw `string` to the
5-literal `LOG_TYPES` union (zod already validates server values)

### Bundled refactor
Migrated 5 emission sites from deprecated `useSendEventMutation` to
`useTrack` while their containing files were being edited: `DPA.tsx`,
`DisplayBlockRenderer.tsx`, `Grid.tsx` (2 events), `DeleteCronJob.tsx`.
Full sweep of the remaining ~79 files is a separate follow-up.

## Testing

Mostly just renaming of events

## Linear
- fixes GROWTH-798


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Standardized telemetry to a unified tracking system for more
consistent analytics.
  * Simplified experiment exposure reporting for upgrade prompts.

* **New Features**
* More granular tracking for CSV import, cron job deletions, log drain
removals, DPA downloads/requests, and pricing CTAs.
  * Assistant now classifies mutation queries more precisely.

* **Bug Fixes**
* Improved default-privileges exposure logic on Vercel deployments
(skips when org missing).

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45964)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-18 18:21:58 +08:00

109 lines
3.5 KiB
TypeScript

import { parseAsString, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { toast } from 'sonner'
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
import { parseCronJobCommand } from './CronJobs.utils'
import { useCronJobsData } from './CronJobsTab.useCronJobsData'
import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
import { useDatabaseCronJobDeleteMutation } from '@/data/database-cron-jobs/database-cron-jobs-delete-mutation'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { cleanPointerEventsNoneOnBody } from '@/lib/helpers'
import { useTrack } from '@/lib/telemetry/track'
export const DeleteCronJob = () => {
const { data: project } = useSelectedProjectQuery()
const [searchQuery] = useQueryState('search', parseAsString.withDefault(''))
const [cronJobIdForDeletion, setCronJobForDeletion] = useQueryState('delete', parseAsString)
const { grid } = useCronJobsData({
projectRef: project?.ref,
connectionString: project?.connectionString,
searchQuery,
})
const cronJob = grid.rows.find((j) => j.jobid.toString() === cronJobIdForDeletion)
const track = useTrack()
const {
mutate: deleteDatabaseCronJob,
isPending,
isSuccess: isSuccessDelete,
} = useDatabaseCronJobDeleteMutation({
onSuccess: () => {
if (cronJob && project) {
const { type } = parseCronJobCommand(cronJob.command, project.ref)
track('cron_job_removed', { type })
}
toast.success(`Successfully removed cron job`)
setCronJobForDeletion(null)
},
})
async function handleDelete() {
if (!project) return console.error('Project is required')
if (!cronJob) return console.error('Cron job is missing')
deleteDatabaseCronJob({
jobId: cronJob.jobid,
projectRef: project.ref,
connectionString: project.connectionString,
searchTerm: searchQuery,
})
}
useEffect(() => {
if (grid.isSuccess && !!cronJobIdForDeletion && !cronJob && !isSuccessDelete) {
toast('Cron job not found')
setCronJobForDeletion(null)
}
}, [cronJob, cronJobIdForDeletion, grid.isSuccess, isSuccessDelete, setCronJobForDeletion])
if (!cronJob) {
return null
}
// Cron job name is optional. If the cron job has no name, show a simplified modal which doesn't require the user to input the name.
if (!cronJob.jobname) {
return (
<ConfirmationModal
variant="destructive"
visible={!!cronJob}
onCancel={() => {
setCronJobForDeletion(null)
cleanPointerEventsNoneOnBody()
}}
onConfirm={handleDelete}
title={`Delete the cron job`}
loading={isPending}
confirmLabel={`Delete`}
alert={{ title: 'You cannot recover this cron job once deleted.' }}
/>
)
}
return (
<TextConfirmModal
variant="destructive"
visible={!!cronJob}
onConfirm={handleDelete}
onCancel={() => {
setCronJobForDeletion(null)
cleanPointerEventsNoneOnBody()
}}
title="Delete this cron job"
loading={isPending}
confirmLabel={`Delete cron job ${cronJob.jobname}`}
confirmPlaceholder="Type in name of cron job"
confirmString={cronJob.jobname ?? 'Unknown'}
text={
<>
<span>This will delete the cron job</span>{' '}
<span className="text-bold text-foreground">{cronJob.jobname}</span>
</>
}
alert={{ title: 'You cannot recover this cron job once deleted.' }}
/>
)
}