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