Skip to content

Commit 73e3143

Browse files
authored
Add type-safe event tracking utility (supabase#39745)
Adds a clean, type-safe wrapper for telemetry event tracking that automatically injects project and organization context. - Export TelemetryGroups type from telemetry-constants - Add useTrack() hook with full TypeScript event validation - Refactor project creation events to use new API - Reduces boilerplate from ~10 lines to ~2 lines per event
1 parent 5576b37 commit 73e3143

File tree

4 files changed

+78
-29
lines changed

4 files changed

+78
-29
lines changed

apps/studio/components/interfaces/ProjectCreation/SchemaGenerator.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { useEffect, useState } from 'react'
44

55
import { Markdown } from 'components/interfaces/Markdown'
66
import { onErrorChat } from 'components/ui/AIAssistantPanel/AIAssistant.utils'
7-
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
87
import { BASE_PATH } from 'lib/constants'
8+
import { useTrack } from 'lib/telemetry/track'
99
import { AiIconAnimation, Button, Label_Shadcn_, Textarea } from 'ui'
1010

1111
interface SupabaseService {
@@ -34,7 +34,7 @@ export const SchemaGenerator = ({
3434
const [hasSql, setHasSql] = useState(false)
3535

3636
const [promptIntendSent, setPromptIntendSent] = useState(false)
37-
const { mutate: sendEvent } = useSendEventMutation()
37+
const track = useTrack()
3838

3939
const { messages, setMessages, sendMessage, status, addToolResult } = useChat({
4040
id: 'schema-generator',
@@ -193,18 +193,12 @@ export const SchemaGenerator = ({
193193
const isNewPrompt = messages.length == 0
194194
// distinguish between initial step or second step
195195
if (step === 'initial') {
196-
sendEvent({
197-
action: 'project_creation_initial_step_prompt_intended',
198-
properties: {
199-
isNewPrompt,
200-
},
196+
track('project_creation_initial_step_prompt_intended', {
197+
isNewPrompt,
201198
})
202199
} else {
203-
sendEvent({
204-
action: 'project_creation_second_step_prompt_intended',
205-
properties: {
206-
isNewPrompt,
207-
},
200+
track('project_creation_second_step_prompt_intended', {
201+
isNewPrompt,
208202
})
209203
}
210204
setPromptIntendSent(true)

apps/studio/lib/telemetry/track.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { sendTelemetryEvent } from 'common'
2+
import { TelemetryEvent, TelemetryGroups } from 'common/telemetry-constants'
3+
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
4+
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
5+
import { API_URL } from 'lib/constants'
6+
import { useRouter } from 'next/router'
7+
import { useCallback } from 'react'
8+
9+
type EventMap = {
10+
[E in TelemetryEvent as E['action']]: E
11+
}
12+
13+
type PropertiesForAction<A extends keyof EventMap> = EventMap[A] extends { properties: infer P }
14+
? P
15+
: never
16+
17+
type HasProperties<A extends keyof EventMap> = EventMap[A] extends { properties: any }
18+
? true
19+
: false
20+
21+
/**
22+
* Hook for type-safe telemetry event tracking with automatic project/org context injection.
23+
*
24+
* @example
25+
* const track = useTrack()
26+
* track('table_created', { method: 'sql_editor', schema_name: 'public' })
27+
* track('help_button_clicked')
28+
*/
29+
export const useTrack = () => {
30+
const { data: project } = useSelectedProjectQuery()
31+
const { data: org } = useSelectedOrganizationQuery()
32+
const router = useRouter()
33+
34+
const track = useCallback(
35+
<A extends keyof EventMap>(
36+
action: A,
37+
...args: HasProperties<A> extends true
38+
? [properties: PropertiesForAction<A>, groupOverrides?: Partial<TelemetryGroups>]
39+
: [properties?: undefined, groupOverrides?: Partial<TelemetryGroups>]
40+
) => {
41+
const [properties, groupOverrides] = args
42+
43+
const groups = {
44+
...(project?.ref && { project: project.ref }),
45+
...(org?.slug && { organization: org.slug }),
46+
...groupOverrides,
47+
}
48+
49+
const event = {
50+
action,
51+
...(properties && { properties }),
52+
...(groups && { groups }),
53+
} as EventMap[A]
54+
55+
sendTelemetryEvent(API_URL, event, router.pathname)
56+
},
57+
[project?.ref, org?.slug, router.pathname]
58+
)
59+
60+
return track
61+
}

apps/studio/pages/new/[slug].tsx

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
ProjectCreateVariables,
4444
useProjectCreateMutation,
4545
} from 'data/projects/project-create-mutation'
46-
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
46+
import { useTrack } from 'lib/telemetry/track'
4747
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
4848
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
4949
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
@@ -171,7 +171,7 @@ const Wizard: NextPageWithLayout = () => {
171171
const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] =
172172
useState(false)
173173

174-
const { mutate: sendEvent } = useSendEventMutation()
174+
const track = useTrack()
175175

176176
FormSchema.superRefine(({ dbPassStrength }, refinementContext) => {
177177
if (dbPassStrength < DEFAULT_MINIMUM_PASSWORD_STRENGTH) {
@@ -247,16 +247,16 @@ const Wizard: NextPageWithLayout = () => {
247247
isSuccess: isSuccessNewProject,
248248
} = useProjectCreateMutation({
249249
onSuccess: (res) => {
250-
sendEvent({
251-
action: 'project_creation_simple_version_submitted',
252-
properties: {
250+
track(
251+
'project_creation_simple_version_submitted',
252+
{
253253
instanceSize: form.getValues('instanceSize'),
254254
},
255-
groups: {
255+
{
256256
project: res.ref,
257257
organization: res.organization_slug,
258-
},
259-
})
258+
}
259+
)
260260
router.push(isHomeNew ? `/project/${res.ref}` : `/project/${res.ref}/building`)
261261
},
262262
})
@@ -372,14 +372,8 @@ const Wizard: NextPageWithLayout = () => {
372372
!sizesWithNoCostConfirmationRequired.includes(values.instanceSize as DesiredInstanceSize)
373373

374374
if (additionalMonthlySpend > 0 && (hasOAuthApps || launchingLargerInstance)) {
375-
sendEvent({
376-
action: 'project_creation_simple_version_confirm_modal_opened',
377-
properties: {
378-
instanceSize: values.instanceSize,
379-
},
380-
groups: {
381-
organization: currentOrg?.slug ?? 'Unknown',
382-
},
375+
track('project_creation_simple_version_confirm_modal_opened', {
376+
instanceSize: values.instanceSize,
383377
})
384378
setIsComputeCostsConfirmationModalVisible(true)
385379
} else {

packages/common/telemetry-constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* @module telemetry-frontend
1010
*/
1111

12-
type TelemetryGroups = {
12+
export type TelemetryGroups = {
1313
project: string
1414
organization: string
1515
}

0 commit comments

Comments
 (0)