Skip to content

Commit b4b839f

Browse files
authored
RI-7218 Add telemetry collection for Create Vector Search Index wizard (#4807)
* feat(ui): collect telemetry on step 1 of the create vector search index wizard * feat(ui): collect telemetry on step 2 of the create vector search index wizard * feat(ui): collect telemetry on step 3 of the create vector search index wizard * feat(ui): collect telemetry on step 2 preview command of the create vector search index wizard re #RI-7218
1 parent 3310c53 commit b4b839f

File tree

16 files changed

+408
-40
lines changed

16 files changed

+408
-40
lines changed

jest.config.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
'\\.scss\\?inline$': '<rootDir>/redisinsight/__mocks__/scssRaw.js',
1414
'uiSrc/slices/store$': '<rootDir>/redisinsight/ui/src/utils/test-store.ts',
1515
'uiSrc/(.*)': '<rootDir>/redisinsight/ui/src/$1',
16+
'apiSrc/(.*)': '<rootDir>/redisinsight/api/src/$1',
1617
'@redislabsdev/redis-ui-components': '@redis-ui/components',
1718
'@redislabsdev/redis-ui-styles': '@redis-ui/styles',
1819
'@redislabsdev/redis-ui-icons': '@redis-ui/icons',

redisinsight/api/src/modules/bulk-actions/interfaces/bulk-action-overview.interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export interface IBulkActionOverview {
1111
databaseId: string;
1212
duration: number;
1313
type: BulkActionType;
14-
status: BulkActionStatus;
15-
filter: IBulkActionFilterOverview;
14+
status: BulkActionStatus; // Note: This can be null, according to the API response
15+
filter: IBulkActionFilterOverview; // Note: This can be null, according to the API response
1616
progress: IBulkActionProgressOverview;
1717
summary: IBulkActionSummaryOverview;
1818
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Factory } from 'fishery'
2+
import { faker } from '@faker-js/faker'
3+
import { RedisDataType } from 'uiSrc/constants'
4+
import { IBulkActionOverview } from 'apiSrc/modules/bulk-actions/interfaces/bulk-action-overview.interface'
5+
import { IBulkActionFilterOverview } from 'apiSrc/modules/bulk-actions/interfaces/bulk-action-filter-overview.interface'
6+
import { IBulkActionProgressOverview } from 'apiSrc/modules/bulk-actions/interfaces/bulk-action-progress-overview.interface'
7+
import { IBulkActionSummaryOverview } from 'apiSrc/modules/bulk-actions/interfaces/bulk-action-summary-overview.interface'
8+
import {
9+
BulkActionStatus,
10+
BulkActionType,
11+
} from 'apiSrc/modules/bulk-actions/constants'
12+
13+
export const bulkActionOverviewFactory = Factory.define<IBulkActionOverview>(
14+
({ sequence }) => ({
15+
id: `bulk-action-${sequence}`,
16+
databaseId: faker.string.ulid(),
17+
type: faker.helpers.enumValue(BulkActionType),
18+
summary: bulkActionSummaryOverviewFactory.build(),
19+
progress: bulkActionProgressOverviewFactory.build(),
20+
filter: bulkActionFilterOverviewFactory.build(),
21+
status: faker.helpers.enumValue(BulkActionStatus),
22+
duration: faker.number.int({ min: 10, max: 100 }),
23+
}),
24+
)
25+
26+
export const bulkActionSummaryOverviewFactory =
27+
Factory.define<IBulkActionSummaryOverview>(() => ({
28+
processed: faker.number.int({ min: 200, max: 299 }),
29+
succeed: faker.number.int({ min: 300, max: 399 }),
30+
failed: faker.number.int({ min: 400, max: 499 }),
31+
errors: [],
32+
keys: [],
33+
}))
34+
35+
export const bulkActionProgressOverviewFactory =
36+
Factory.define<IBulkActionProgressOverview>(() => ({
37+
total: faker.number.int({ min: 100, max: 1000 }),
38+
scanned: faker.number.int({ min: 0, max: 1000 }),
39+
}))
40+
41+
export const bulkActionFilterOverviewFactory =
42+
Factory.define<IBulkActionFilterOverview>(() => ({
43+
type: faker.helpers.enumValue(RedisDataType),
44+
match: faker.string.uuid(),
45+
}))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Factory } from 'fishery'
2+
import { faker } from '@faker-js/faker'
3+
import { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'
4+
5+
// Note: Types are temporaryly not binded properly due to issues in the Jset setup
6+
// SyntaxError: redisinsight/api/src/modules/workbench/models/command-execution.ts: Support for the experimental syntax 'decorators' isn't currently enabled (32:3):
7+
8+
// export const commandExecutionFactory = Factory.define<CommandExecution>(
9+
export const commandExecutionFactory = Factory.define(({ sequence }) => ({
10+
id: sequence.toString() ?? faker.string.uuid(),
11+
databaseId: faker.string.ulid(),
12+
db: faker.number.int({ min: 0, max: 15 }),
13+
type: faker.helpers.arrayElement(['WORKBENCH', 'SEARCH']), // faker.helpers.enumValue(CommandExecutionType),
14+
mode: faker.helpers.arrayElement(['RAW', 'ASCII']), // faker.helpers.enumValue(RunQueryMode),
15+
resultsMode: faker.helpers.arrayElement(['DEFAULT', 'GROUP_MODE', 'SILENT']), // faker.helpers.enumValue(ResultsMode),
16+
command: faker.lorem.paragraph(),
17+
result: commandExecutionResultFactory.buildList(1),
18+
executionTime: faker.number.int({ min: 1000, max: 5000 }),
19+
createdAt: faker.date.past(),
20+
}))
21+
22+
// export const commandExecutionResultFactory = Factory.define<CommandExecutionResult>(() => {
23+
export const commandExecutionResultFactory = Factory.define(() => {
24+
const includeSizeLimitExceeded = faker.datatype.boolean()
25+
26+
return {
27+
status: faker.helpers.enumValue(CommandExecutionStatus),
28+
response: faker.lorem.paragraph(),
29+
30+
// Optional properties
31+
...(includeSizeLimitExceeded && {
32+
sizeLimitExceeded: faker.datatype.boolean(),
33+
}),
34+
}
35+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { rest, RestHandler } from 'msw'
2+
import { ApiEndpoints } from 'uiSrc/constants'
3+
import { getMswURL } from 'uiSrc/utils/test-utils'
4+
import { getUrl } from 'uiSrc/utils'
5+
import { IBulkActionOverview } from 'uiSrc/slices/interfaces'
6+
import { bulkActionOverviewFactory } from 'uiSrc/mocks/factories/browser/bulkActions/bulkActionOverview.factory'
7+
import { INSTANCE_ID_MOCK } from '../instances/instancesHandlers'
8+
9+
const handlers: RestHandler[] = [
10+
rest.post<IBulkActionOverview>(
11+
getMswURL(
12+
getUrl(
13+
INSTANCE_ID_MOCK,
14+
ApiEndpoints.BULK_ACTIONS_IMPORT_VECTOR_COLLECTION,
15+
),
16+
),
17+
async (_req, res, ctx) =>
18+
res(ctx.status(200), ctx.json(bulkActionOverviewFactory.build())),
19+
),
20+
]
21+
22+
export default handlers
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { DefaultBodyType, MockedRequest, RestHandler } from 'msw'
22

33
import redisearch from './redisearchHandlers'
4+
import bulkActions from './bulkActionsHandlers'
45

56
const handlers: RestHandler<MockedRequest<DefaultBodyType>>[] = [].concat(
67
redisearch,
8+
bulkActions,
79
)
810
export default handlers

redisinsight/ui/src/mocks/handlers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import cloud from './oauth'
99
import tutorials from './tutorials'
1010
import rdi from './rdi'
1111
import user from './user'
12+
import workbench from './workbench'
1213

1314
// @ts-ignore
1415
export const handlers: RestHandler<MockedRequest>[] = [].concat(
@@ -22,4 +23,5 @@ export const handlers: RestHandler<MockedRequest>[] = [].concat(
2223
tutorials,
2324
rdi,
2425
user,
26+
workbench,
2527
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { rest, RestHandler } from 'msw'
2+
import { ApiEndpoints } from 'uiSrc/constants'
3+
import { getMswURL } from 'uiSrc/utils/test-utils'
4+
import { getUrl } from 'uiSrc/utils'
5+
import { CommandExecution } from 'uiSrc/slices/interfaces'
6+
import { commandExecutionFactory } from 'uiSrc/mocks/factories/workbench/commandExectution.factory'
7+
import { INSTANCE_ID_MOCK } from '../instances/instancesHandlers'
8+
9+
const handlers: RestHandler[] = [
10+
rest.post<CommandExecution[]>(
11+
getMswURL(
12+
getUrl(INSTANCE_ID_MOCK, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),
13+
),
14+
async (_req, res, ctx) =>
15+
res(ctx.status(200), ctx.json(commandExecutionFactory.buildList(1))),
16+
),
17+
]
18+
19+
export default handlers
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { DefaultBodyType, MockedRequest, RestHandler } from 'msw'
2+
3+
import commands from './commands'
4+
5+
const handlers: RestHandler<MockedRequest<DefaultBodyType>>[] = [].concat(
6+
commands,
7+
)
8+
export default handlers
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import React from 'react'
2+
import {
3+
render,
4+
screen,
5+
fireEvent,
6+
initialStateDefault,
7+
mockStore,
8+
} from 'uiSrc/utils/test-utils'
9+
import { RootState } from 'uiSrc/slices/store'
10+
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
11+
import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/dbAnalysisHistoryHandlers'
12+
import { VectorSearchCreateIndex } from './VectorSearchCreateIndex'
13+
import { SampleDataContent, SampleDataType, SearchIndexType } from './types'
14+
15+
// Mock the telemetry module, so we don't send actual telemetry data during tests
16+
jest.mock('uiSrc/telemetry', () => ({
17+
...jest.requireActual('uiSrc/telemetry'),
18+
sendEventTelemetry: jest.fn(),
19+
}))
20+
21+
const renderVectorSearchCreateIndexComponent = () => {
22+
const testState: RootState = {
23+
...initialStateDefault,
24+
connections: {
25+
...initialStateDefault.connections,
26+
instances: {
27+
...initialStateDefault.connections.instances,
28+
connectedInstance: {
29+
...initialStateDefault.connections.instances.connectedInstance,
30+
id: INSTANCE_ID_MOCK,
31+
name: 'test-instance',
32+
host: 'localhost',
33+
port: 6379,
34+
modules: [],
35+
},
36+
},
37+
},
38+
}
39+
const store = mockStore(testState)
40+
41+
return render(<VectorSearchCreateIndex />, { store })
42+
}
43+
44+
describe('VectorSearchCreateIndex', () => {
45+
beforeEach(() => {
46+
jest.clearAllMocks()
47+
})
48+
49+
it('should render correctly', () => {
50+
const { container } = renderVectorSearchCreateIndexComponent()
51+
52+
expect(container).toBeInTheDocument()
53+
})
54+
55+
describe('Telemetry', () => {
56+
it('should send telemetry events on start step', () => {
57+
renderVectorSearchCreateIndexComponent()
58+
59+
expect(sendEventTelemetry).toHaveBeenCalledTimes(1)
60+
expect(sendEventTelemetry).toHaveBeenCalledWith({
61+
event: TelemetryEvent.VECTOR_SEARCH_ONBOARDING_TRIGGERED,
62+
eventData: { databaseId: INSTANCE_ID_MOCK },
63+
})
64+
})
65+
66+
it('should send telemetry events on index info step', () => {
67+
renderVectorSearchCreateIndexComponent()
68+
69+
// Select the index type
70+
const indexTypeRadio = screen.getByText('Redis Query Engine')
71+
fireEvent.click(indexTypeRadio)
72+
73+
// Select the sample dataset
74+
const sampleDataRadio = screen.getByText('Pre-set data')
75+
fireEvent.click(sampleDataRadio)
76+
77+
// Select data content
78+
const dataContentRadio = screen.getByText('E-commerce Discovery')
79+
fireEvent.click(dataContentRadio)
80+
81+
// Simulate going to the index info step
82+
const buttonNext = screen.getByText('Proceed to index')
83+
fireEvent.click(buttonNext)
84+
85+
expect(sendEventTelemetry).toHaveBeenCalledTimes(2)
86+
expect(sendEventTelemetry).toHaveBeenNthCalledWith(2, {
87+
event: TelemetryEvent.VECTOR_SEARCH_ONBOARDING_PROCEED_TO_INDEX_INFO,
88+
eventData: {
89+
databaseId: INSTANCE_ID_MOCK,
90+
indexType: SearchIndexType.REDIS_QUERY_ENGINE,
91+
sampleDataType: SampleDataType.PRESET_DATA,
92+
dataContent: SampleDataContent.E_COMMERCE_DISCOVERY,
93+
},
94+
})
95+
})
96+
97+
it('should send telemetry events on create index step', () => {
98+
renderVectorSearchCreateIndexComponent()
99+
100+
// Simulate going to the index info step
101+
const buttonNext = screen.getByText('Proceed to index')
102+
fireEvent.click(buttonNext)
103+
104+
// Simulate creating the index
105+
const buttonCreateIndex = screen.getByText('Create index')
106+
fireEvent.click(buttonCreateIndex)
107+
108+
expect(sendEventTelemetry).toHaveBeenCalledTimes(3)
109+
expect(sendEventTelemetry).toHaveBeenNthCalledWith(3, {
110+
event: TelemetryEvent.VECTOR_SEARCH_ONBOARDING_PROCEED_TO_QUERIES,
111+
eventData: {
112+
databaseId: INSTANCE_ID_MOCK,
113+
},
114+
})
115+
})
116+
})
117+
})

0 commit comments

Comments
 (0)