telemetry.tsx88 lines · main
1import * as Sentry from '@sentry/nextjs'
2import { LOCAL_STORAGE_KEYS, PageTelemetry, posthogClient, useUser } from 'common'
3import { useEffect, useRef } from 'react'
4import { useConsentToast } from 'ui-patterns/consent'
5
6import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
7import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
8import { API_URL, IS_PLATFORM } from '@/lib/constants'
9
10const getAnonId = async (id: string) => {
11 const encoder = new TextEncoder()
12 const data = encoder.encode(id)
13 const hashBuffer = await crypto.subtle.digest('SHA-256', data)
14 const hashArray = Array.from(new Uint8Array(hashBuffer))
15 const base64String = btoa(hashArray.map((byte) => String.fromCharCode(byte)).join(''))
16
17 return base64String
18}
19
20export function Telemetry() {
21 // Although this is "technically" breaking the rules of hooks
22 // IS_PLATFORM never changes within a session, so this won't cause any issues
23 // eslint-disable-next-line react-hooks/rules-of-hooks
24 const { hasAcceptedConsent } = IS_PLATFORM ? useConsentToast() : { hasAcceptedConsent: true }
25
26 // Get org from selected organization query because it's not
27 // always available in the URL params
28 const { data: organization } = useSelectedOrganizationQuery()
29
30 const user = useUser()
31
32 // Mirror the user's org-list length into a PostHog person property so feature
33 // flags and analytics can segment by current org membership. signup_timestamp
34 // is set on the same identify so flag audiences requiring both properties see
35 // them together on /decide. Only fires when the value changes.
36 const { data: organizations } = useOrganizationsQuery()
37 const lastSentRef = useRef<{
38 userId: string
39 orgCount: number
40 signupTimestamp?: string
41 } | null>(null)
42 useEffect(() => {
43 if (!user?.id || !organizations) return
44 const orgCount = organizations.length
45 const signupTimestamp = user.created_at ?? undefined
46 const last = lastSentRef.current
47 if (
48 last?.userId === user.id &&
49 last.orgCount === orgCount &&
50 last.signupTimestamp === signupTimestamp
51 ) {
52 return
53 }
54 lastSentRef.current = { userId: user.id, orgCount, signupTimestamp }
55 posthogClient.identify(user.id, {
56 org_count: orgCount,
57 ...(signupTimestamp && { signup_timestamp: signupTimestamp }),
58 })
59 }, [user?.id, user?.created_at, organizations])
60
61 useEffect(() => {
62 // don't set the sentry user id if the user hasn't logged in (so that Sentry errors show null user id instead of anonymous id)
63 if (!user?.id) {
64 return
65 }
66
67 const setSentryId = async () => {
68 let sentryUserId = localStorage.getItem(LOCAL_STORAGE_KEYS.SENTRY_USER_ID)
69 if (!sentryUserId) {
70 sentryUserId = await getAnonId(user?.id)
71 localStorage.setItem(LOCAL_STORAGE_KEYS.SENTRY_USER_ID, sentryUserId)
72 }
73 Sentry.setUser({ id: sentryUserId })
74 }
75
76 // if an error happens, continue without setting a sentry id
77 setSentryId().catch((e) => console.error(e))
78 }, [user?.id])
79
80 return (
81 <PageTelemetry
82 API_URL={API_URL}
83 hasAcceptedConsent={hasAcceptedConsent}
84 enabled={IS_PLATFORM}
85 organizationSlug={organization?.slug}
86 />
87 )
88}