telemetry.test.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { render, waitFor } from '@testing-library/react'
  2. import { beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { Telemetry } from './telemetry'
  4. const mocks = vi.hoisted(() => ({
  5. identify: vi.fn(),
  6. useUser: vi.fn(),
  7. useOrganizationsQuery: vi.fn(),
  8. }))
  9. vi.mock('common', async (importOriginal) => {
  10. const actual = await importOriginal<typeof import('common')>()
  11. return {
  12. ...actual,
  13. posthogClient: {
  14. identify: mocks.identify,
  15. },
  16. useUser: () => mocks.useUser(),
  17. PageTelemetry: () => null,
  18. }
  19. })
  20. vi.mock('ui-patterns/consent', () => ({
  21. useConsentToast: () => ({ hasAcceptedConsent: true }),
  22. }))
  23. vi.mock('@/data/organizations/organizations-query', () => ({
  24. useOrganizationsQuery: () => mocks.useOrganizationsQuery(),
  25. }))
  26. vi.mock('@/hooks/misc/useSelectedOrganization', () => ({
  27. useSelectedOrganizationQuery: () => ({ data: undefined }),
  28. }))
  29. vi.mock('@sentry/nextjs', () => ({
  30. setUser: vi.fn(),
  31. }))
  32. const USER_ID = 'user-abc-123'
  33. const CREATED_AT = '2026-05-14T22:30:00.000Z'
  34. const orgs = (count: number) =>
  35. Array.from({ length: count }, (_, i) => ({ id: i, slug: `org-${i}` }))
  36. describe('Telemetry — posthog identify mirroring', () => {
  37. beforeEach(() => {
  38. mocks.identify.mockReset()
  39. mocks.useUser.mockReset()
  40. mocks.useOrganizationsQuery.mockReset()
  41. })
  42. it('fires identify with both org_count and signup_timestamp when user and orgs are loaded', async () => {
  43. mocks.useUser.mockReturnValue({ id: USER_ID, created_at: CREATED_AT })
  44. mocks.useOrganizationsQuery.mockReturnValue({ data: orgs(1) })
  45. render(<Telemetry />)
  46. await waitFor(() => {
  47. expect(mocks.identify).toHaveBeenCalledWith(USER_ID, {
  48. org_count: 1,
  49. signup_timestamp: CREATED_AT,
  50. })
  51. })
  52. })
  53. it('omits signup_timestamp when created_at is missing', async () => {
  54. mocks.useUser.mockReturnValue({ id: USER_ID, created_at: undefined })
  55. mocks.useOrganizationsQuery.mockReturnValue({ data: orgs(1) })
  56. render(<Telemetry />)
  57. await waitFor(() => {
  58. expect(mocks.identify).toHaveBeenCalledWith(USER_ID, { org_count: 1 })
  59. })
  60. })
  61. it('dedupes when nothing has changed', async () => {
  62. mocks.useUser.mockReturnValue({ id: USER_ID, created_at: CREATED_AT })
  63. mocks.useOrganizationsQuery.mockReturnValue({ data: orgs(1) })
  64. const { rerender } = render(<Telemetry />)
  65. await waitFor(() => expect(mocks.identify).toHaveBeenCalledTimes(1))
  66. rerender(<Telemetry />)
  67. rerender(<Telemetry />)
  68. // Still only one identify — same user, same orgCount, same signupTimestamp.
  69. expect(mocks.identify).toHaveBeenCalledTimes(1)
  70. })
  71. it('re-fires identify when created_at arrives after the first effect run (CodeRabbit regression)', async () => {
  72. // Initial render: user.id is set, but created_at is still undefined (e.g. partial session parse).
  73. mocks.useUser.mockReturnValue({ id: USER_ID, created_at: undefined })
  74. mocks.useOrganizationsQuery.mockReturnValue({ data: orgs(1) })
  75. const { rerender } = render(<Telemetry />)
  76. await waitFor(() => expect(mocks.identify).toHaveBeenCalledTimes(1))
  77. expect(mocks.identify).toHaveBeenLastCalledWith(USER_ID, { org_count: 1 })
  78. // created_at lands later. The dedup ref must track signupTimestamp,
  79. // and the effect deps must include user.created_at, or the second
  80. // identify gets silently skipped — which is exactly the race we shipped
  81. // the original fix to close.
  82. mocks.useUser.mockReturnValue({ id: USER_ID, created_at: CREATED_AT })
  83. rerender(<Telemetry />)
  84. await waitFor(() => expect(mocks.identify).toHaveBeenCalledTimes(2))
  85. expect(mocks.identify).toHaveBeenLastCalledWith(USER_ID, {
  86. org_count: 1,
  87. signup_timestamp: CREATED_AT,
  88. })
  89. })
  90. it('re-fires identify when org_count changes', async () => {
  91. mocks.useUser.mockReturnValue({ id: USER_ID, created_at: CREATED_AT })
  92. mocks.useOrganizationsQuery.mockReturnValue({ data: orgs(1) })
  93. const { rerender } = render(<Telemetry />)
  94. await waitFor(() => expect(mocks.identify).toHaveBeenCalledTimes(1))
  95. mocks.useOrganizationsQuery.mockReturnValue({ data: orgs(2) })
  96. rerender(<Telemetry />)
  97. await waitFor(() => expect(mocks.identify).toHaveBeenCalledTimes(2))
  98. expect(mocks.identify).toHaveBeenLastCalledWith(USER_ID, {
  99. org_count: 2,
  100. signup_timestamp: CREATED_AT,
  101. })
  102. })
  103. it('does not fire identify when user or orgs are missing', async () => {
  104. // user missing
  105. mocks.useUser.mockReturnValue(null)
  106. mocks.useOrganizationsQuery.mockReturnValue({ data: orgs(1) })
  107. const first = render(<Telemetry />)
  108. await new Promise((r) => setTimeout(r, 10))
  109. expect(mocks.identify).not.toHaveBeenCalled()
  110. first.unmount()
  111. // user present but orgs not loaded
  112. mocks.useUser.mockReturnValue({ id: USER_ID, created_at: CREATED_AT })
  113. mocks.useOrganizationsQuery.mockReturnValue({ data: undefined })
  114. render(<Telemetry />)
  115. await new Promise((r) => setTimeout(r, 10))
  116. expect(mocks.identify).not.toHaveBeenCalled()
  117. })
  118. })