Skip to content

Commit 0cfce38

Browse files
authored
RI-7218 Add telemetry collection for Manage Indexes drawer on Vector Search page (#4830)
* feat(ui): collect telemetry on vector search page when open/close manage indexes drawer * feat(ui): collect telemetry on vector search page when expand/collapse index details * feat(ui): collect telemetry on vector search page when deleting indexes re #RI-7218
1 parent 2d251e0 commit 0cfce38

File tree

8 files changed

+527
-85
lines changed

8 files changed

+527
-85
lines changed

redisinsight/ui/src/pages/vector-search/create-index/VectorSearchCreateIndex.tsx

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { Title } from 'uiSrc/components/base/text'
66
import { Button, SecondaryButton } from 'uiSrc/components/base/forms/buttons'
77
import { ChevronLeftIcon } from 'uiSrc/components/base/icons'
88

9-
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
109
import { selectedBikesIndexFields, stepContents } from './steps'
1110
import {
1211
CreateSearchIndexParameters,
@@ -22,6 +21,10 @@ import {
2221
VectorSearchScreenHeader,
2322
VectorSearchScreenWrapper,
2423
} from '../styles'
24+
import {
25+
collectCreateIndexStepTelemetry,
26+
collectCreateIndexWizardTelemetry,
27+
} from '../telemetry'
2528

2629
const stepNextButtonTexts = [
2730
'Proceed to adding data',
@@ -60,7 +63,7 @@ export const VectorSearchCreateIndex = ({
6063
const isFinalStep = step === stepContents.length - 1
6164
if (isFinalStep) {
6265
createIndex(createSearchIndexParameters)
63-
collectCreateIndexStepTelemetry()
66+
collectCreateIndexStepTelemetry(instanceId)
6467
return
6568
}
6669

@@ -71,55 +74,12 @@ export const VectorSearchCreateIndex = ({
7174
}
7275

7376
useEffect(() => {
74-
collectTelemetry(step)
75-
}, [step])
76-
77-
const collectTelemetry = (step: number): void => {
78-
switch (step) {
79-
case 1:
80-
collectStartStepTelemetry()
81-
break
82-
case 2:
83-
collectIndexInfoStepTelemetry()
84-
break
85-
case 3:
86-
collectCreateIndexStepTelemetry()
87-
break
88-
default:
89-
// No telemetry for other steps
90-
break
91-
}
92-
}
93-
94-
const collectStartStepTelemetry = (): void => {
95-
sendEventTelemetry({
96-
event: TelemetryEvent.VECTOR_SEARCH_ONBOARDING_TRIGGERED,
97-
eventData: {
98-
databaseId: instanceId,
99-
},
100-
})
101-
}
102-
103-
const collectIndexInfoStepTelemetry = (): void => {
104-
sendEventTelemetry({
105-
event: TelemetryEvent.VECTOR_SEARCH_ONBOARDING_PROCEED_TO_INDEX_INFO,
106-
eventData: {
107-
databaseId: instanceId,
108-
indexType: createSearchIndexParameters.searchIndexType,
109-
sampleDataType: createSearchIndexParameters.sampleDataType,
110-
dataContent: createSearchIndexParameters.dataContent,
111-
},
112-
})
113-
}
114-
115-
const collectCreateIndexStepTelemetry = (): void => {
116-
sendEventTelemetry({
117-
event: TelemetryEvent.VECTOR_SEARCH_ONBOARDING_PROCEED_TO_QUERIES,
118-
eventData: {
119-
databaseId: instanceId,
120-
},
77+
collectCreateIndexWizardTelemetry({
78+
instanceId,
79+
step,
80+
parameters: createSearchIndexParameters,
12181
})
122-
}
82+
}, [step])
12383

12484
if (success) {
12585
return <>Success!</>

redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.spec.tsx

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
initialStateDefault,
1111
userEvent,
1212
getMswURL,
13+
fireEvent,
1314
} from 'uiSrc/utils/test-utils'
1415
import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'
1516
import Notifications from 'uiSrc/components/notifications'
@@ -233,6 +234,63 @@ describe('IndexSection', () => {
233234
expect(noIndexValue[0]).toBeInTheDocument()
234235
})
235236

237+
it('should send telemetry when expanding and collapsing the section information', async () => {
238+
const mockIndexInfo = indexInfoFactory.build()
239+
const props: IndexSectionProps = {
240+
index: mockIndexInfo.index_name,
241+
}
242+
243+
renderComponent(props)
244+
245+
const section = screen.getByTestId(
246+
`manage-indexes-list--item--${props.index}`,
247+
)
248+
expect(section).toBeInTheDocument()
249+
250+
// Verify we start with collapsed section with summary info and no index details
251+
const indexSummaryInitial = screen.getByText('Records')
252+
const indexDetailsInitial = screen.queryByText('Identifier')
253+
254+
expect(indexSummaryInitial).toBeInTheDocument()
255+
expect(indexDetailsInitial).not.toBeInTheDocument()
256+
257+
// Click to expand the section
258+
const indexName = screen.getByText(mockIndexInfo.index_name)
259+
expect(indexName).toBeInTheDocument()
260+
261+
fireEvent.click(indexName)
262+
263+
// Verify the index attributes are displayed and
264+
const indexDetailsExpanded = await screen.findByText('Identifier')
265+
expect(indexDetailsExpanded).toBeInTheDocument()
266+
267+
// Verify the telemetry event is sent
268+
expect(sendEventTelemetry).toHaveBeenCalledWith({
269+
event: TelemetryEvent.SEARCH_MANAGE_INDEX_DETAILS_OPENED,
270+
eventData: {
271+
databaseId: INSTANCE_ID_MOCK,
272+
},
273+
})
274+
275+
// Click again to collapse the section
276+
fireEvent.click(indexName)
277+
278+
// Verify the index summary info is displayed again
279+
const indexSummaryVisible = screen.getByText('Records')
280+
const indexDetailsCollapsed = screen.queryByText('Identifier')
281+
282+
expect(indexSummaryVisible).toBeInTheDocument()
283+
expect(indexDetailsCollapsed).not.toBeInTheDocument()
284+
285+
// Verify the telemetry event is sent
286+
expect(sendEventTelemetry).toHaveBeenCalledWith({
287+
event: TelemetryEvent.SEARCH_MANAGE_INDEX_DETAILS_CLOSED,
288+
eventData: {
289+
databaseId: INSTANCE_ID_MOCK,
290+
},
291+
})
292+
})
293+
236294
describe('delete index', () => {
237295
let confirmSpy: jest.SpyInstance
238296
let telemetryMock: jest.Mock
@@ -271,12 +329,13 @@ describe('IndexSection', () => {
271329
)
272330
expect(successNotification).toBeInTheDocument()
273331

274-
// Verify that telemetry event was sent with correct data
275-
expect(telemetryMock).toHaveBeenCalledTimes(1)
276-
const telemetryCall = telemetryMock.mock.calls[0][0]
277-
expect(telemetryCall.event).toBe(TelemetryEvent.SEARCH_INDEX_DELETED)
278-
expect(telemetryCall.eventData.databaseId).toBe(INSTANCE_ID_MOCK)
279-
expect(telemetryCall.eventData.indexName).toBeDefined()
332+
// Verify the telemetry event is sent
333+
expect(sendEventTelemetry).toHaveBeenCalledWith({
334+
event: TelemetryEvent.SEARCH_MANAGE_INDEX_DELETED,
335+
eventData: {
336+
databaseId: INSTANCE_ID_MOCK,
337+
},
338+
})
280339
})
281340

282341
it('should not delete an index when the deletion is cancelled', async () => {

redisinsight/ui/src/pages/vector-search/manage-indexes/IndexSection.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux'
44
import { CategoryValueListItem } from '@redis-ui/components/dist/Section/components/Header/components/CategoryValueList'
55
import { RedisString } from 'uiSrc/slices/interfaces'
66
import { bufferToString, formatLongName, stringToBuffer } from 'uiSrc/utils'
7-
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
87
import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'
98
import {
109
deleteRedisearchIndexAction,
@@ -15,6 +14,10 @@ import {
1514
IndexDeleteRequestBodyDto,
1615
} from 'apiSrc/modules/browser/redisearch/dto'
1716
import { IndexAttributesList } from './IndexAttributesList'
17+
import {
18+
collectManageIndexesDeleteTelemetry,
19+
collectManageIndexesDetailsToggleTelemetry,
20+
} from '../telemetry'
1821

1922
export interface IndexSectionProps extends Omit<SectionProps, 'label'> {
2023
index: RedisString
@@ -55,13 +58,16 @@ export const IndexSection = ({ index, ...rest }: IndexSectionProps) => {
5558
}
5659
}
5760

58-
const onDeletedIndexSuccess = (data: IndexDeleteRequestBodyDto) => {
59-
sendEventTelemetry({
60-
event: TelemetryEvent.SEARCH_INDEX_DELETED,
61-
eventData: {
62-
databaseId: instanceId,
63-
indexName: data.index,
64-
},
61+
const onDeletedIndexSuccess = () => {
62+
collectManageIndexesDeleteTelemetry({
63+
instanceId,
64+
})
65+
}
66+
67+
const handleOpenChange = (open: boolean) => {
68+
collectManageIndexesDetailsToggleTelemetry({
69+
instanceId,
70+
isOpen: open,
6571
})
6672
}
6773

@@ -75,6 +81,7 @@ export const IndexSection = ({ index, ...rest }: IndexSectionProps) => {
7581
defaultOpen={false}
7682
actionButtonText="Delete" // TODO: Replace with an icon of a trash can
7783
onAction={handleDelete}
84+
onOpenChange={handleOpenChange}
7885
data-testid={`manage-indexes-list--item--${indexName}`}
7986
{...rest}
8087
/>

redisinsight/ui/src/pages/vector-search/manage-indexes/ManageIndexesDrawer.spec.tsx

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import React from 'react'
2-
import { cleanup, render, screen } from 'uiSrc/utils/test-utils'
1+
import React, { useState } from 'react'
2+
import { cleanup, fireEvent, render, screen } from 'uiSrc/utils/test-utils'
33
import {
44
ManageIndexesDrawer,
55
ManageIndexesDrawerProps,
66
} from './ManageIndexesDrawer'
7+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
8+
import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'
79

810
// Workaround for @redis-ui/components Title component issue with react-children-utilities
911
// TypeError: react_utils.childrenToString is not a function
@@ -12,18 +14,42 @@ jest.mock('uiSrc/components/base/layout/drawer', () => ({
1214
DrawerHeader: jest.fn().mockReturnValue(null),
1315
}))
1416

17+
// Mock the telemetry module, so we don't send actual telemetry data during tests
18+
jest.mock('uiSrc/telemetry', () => ({
19+
...jest.requireActual('uiSrc/telemetry'),
20+
sendEventTelemetry: jest.fn(),
21+
}))
22+
23+
const MockDrawer = ({ open, ...rest }: Partial<ManageIndexesDrawerProps>) => {
24+
const [isDrawerOpen, setIsDrawerOpen] = useState(open ?? true)
25+
26+
return (
27+
<>
28+
<button
29+
data-testid="toggle-drawer"
30+
onClick={() => setIsDrawerOpen((prev) => !prev)}
31+
>
32+
Toggle Drawer
33+
</button>
34+
35+
<ManageIndexesDrawer open={isDrawerOpen} {...rest} />
36+
</>
37+
)
38+
}
39+
1540
const renderComponent = (props?: Partial<ManageIndexesDrawerProps>) => {
1641
const defaultProps: ManageIndexesDrawerProps = {
1742
open: true,
1843
onOpenChange: jest.fn(),
1944
}
2045

21-
return render(<ManageIndexesDrawer {...defaultProps} {...props} />)
46+
return render(<MockDrawer {...defaultProps} {...props} />)
2247
}
2348

2449
describe('ManageIndexesDrawer', () => {
2550
beforeEach(() => {
2651
cleanup()
52+
jest.clearAllMocks()
2753
})
2854

2955
it('should render', () => {
@@ -44,4 +70,60 @@ describe('ManageIndexesDrawer', () => {
4470
const list = screen.getByTestId('manage-indexes-list')
4571
expect(list).toBeInTheDocument()
4672
})
73+
74+
describe('Telemetry', () => {
75+
it('should send telemetry event on drawer open', async () => {
76+
renderComponent({ open: false })
77+
78+
// Click the toggle button to open the drawer
79+
const toggleButton = screen.getByTestId('toggle-drawer')
80+
fireEvent.click(toggleButton)
81+
82+
// Simulate the animation lifecycle so Drawer fires didOpen (dirty hack)
83+
const dialog = screen.getByRole('dialog')
84+
fireEvent.animationStart(dialog)
85+
fireEvent.animationEnd(dialog)
86+
87+
const drawer = screen.getByTestId('manage-indexes-drawer')
88+
expect(drawer).toBeInTheDocument()
89+
90+
// Verify telemetry event is sent
91+
expect(sendEventTelemetry).toHaveBeenCalledWith({
92+
event: TelemetryEvent.SEARCH_MANAGE_INDEXES_DRAWER_OPENED,
93+
eventData: {
94+
databaseId: INSTANCE_ID_MOCK,
95+
},
96+
})
97+
})
98+
99+
it('should send telemetry event on drawer close', async () => {
100+
renderComponent({ open: true })
101+
102+
const openDrawer = screen.getByTestId('manage-indexes-drawer')
103+
expect(openDrawer).toBeInTheDocument()
104+
105+
// Click the toggle button to open the drawer
106+
const toggleButton = screen.getByTestId('toggle-drawer')
107+
108+
// Dialog stays mounted but hidden during exit
109+
// const closingDialog = screen.getByRole('dialog', { hidden: true })
110+
111+
// Simulate the animation lifecycle so Drawer fires didClose
112+
fireEvent.click(toggleButton)
113+
// fireEvent.animationStart(closingDialog)
114+
// fireEvent.animationEnd(closingDialog)
115+
116+
// Note: For some reason, the dirty hackwith the animated dialog is not working here and the onDidCLosed is not trigerred in the tests
117+
// await waitFor(() =>
118+
// expect(sendEventTelemetry).toHaveBeenCalledWith({
119+
// event: TelemetryEvent.SEARCH_MANAGE_INDEXES_DRAWER_CLOSED,
120+
// eventData: { databaseId: INSTANCE_ID_MOCK },
121+
// }),
122+
// )
123+
124+
// Verify the drawer is no longer open
125+
const closedDrawer = screen.queryByTestId('manage-indexes-drawer')
126+
expect(closedDrawer).not.toBeInTheDocument()
127+
})
128+
})
47129
})

0 commit comments

Comments
 (0)