telemetry.tsx 3.2 KB

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