Skip to content

Commit 66e6d31

Browse files
author
Artem
authored
Merge pull request #3178 from RedisInsight/be/feature/RI-5561-index-analytics
Be/feature/ri 5561 index analytics
2 parents e03e81c + 31d2d06 commit 66e6d31

16 files changed

+450
-9
lines changed

redisinsight/api/src/__mocks__/analytics.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ export const mockCliAnalyticsService = () => ({
77
sendCommandErrorEvent: jest.fn(),
88
sendClusterCommandExecutedEvent: jest.fn(),
99
sendConnectionErrorEvent: jest.fn(),
10+
sendIndexInfoEvent: jest.fn(),
1011
});
1112

1213
export const mockWorkbenchAnalyticsService = () => ({
1314
sendCommandExecutedEvents: jest.fn(),
1415
sendCommandExecutedEvent: jest.fn(),
1516
sendCommandDeletedEvent: jest.fn(),
17+
sendIndexInfoEvent: jest.fn(),
1618
});
1719

1820
export const mockSettingsAnalyticsService = () => ({

redisinsight/api/src/__mocks__/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from './encryption';
55
export * from './errors';
66
// export * from './redis-databases';
77
export * from './redis-info';
8+
export * from './redis-rs';
89
export * from './app-settings';
910
export * from './analytics';
1011
export * from './profiler';
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { isArray, isString } from 'lodash';
2+
3+
export const mockRedisFtInfoReply = [
4+
'index_name',
5+
'idx:bicycle',
6+
'index_options',
7+
[],
8+
'index_definition',
9+
[
10+
'key_type',
11+
'HASH',
12+
'prefixes',
13+
['bicycle:'],
14+
'default_score',
15+
'1',
16+
],
17+
'attributes',
18+
[
19+
[
20+
'identifier',
21+
'$.brand',
22+
'attribute',
23+
'brand',
24+
'type',
25+
'TEXT',
26+
'WEIGHT',
27+
'1',
28+
],
29+
[
30+
'identifier', '$.model',
31+
'attribute', 'model',
32+
'type', 'TEXT',
33+
'WEIGHT', '1',
34+
'NOSTEM',
35+
],
36+
[
37+
'identifier',
38+
'$.description',
39+
'attribute',
40+
'description',
41+
'type',
42+
'TEXT',
43+
'WEIGHT',
44+
'1',
45+
],
46+
[
47+
'identifier',
48+
'$.price',
49+
'attribute',
50+
'price',
51+
'type',
52+
'NUMERIC',
53+
'NOINDEX',
54+
],
55+
[
56+
'identifier',
57+
'$.condition',
58+
'attribute',
59+
'condition',
60+
'type',
61+
'TAG',
62+
'SEPARATOR',
63+
',',
64+
'CASESENSITIVE',
65+
'SORTABLE',
66+
],
67+
],
68+
'num_docs',
69+
'0',
70+
'max_doc_id',
71+
'0',
72+
'num_terms',
73+
'0',
74+
'num_records',
75+
'0',
76+
'inverted_sz_mb',
77+
'0',
78+
'vector_index_sz_mb',
79+
'0',
80+
'total_inverted_index_blocks',
81+
'0',
82+
'offset_vectors_sz_mb',
83+
'0',
84+
'doc_table_size_mb',
85+
'0',
86+
'sortable_values_size_mb',
87+
'0',
88+
'key_table_size_mb',
89+
'0',
90+
'records_per_doc_avg',
91+
'-nan',
92+
'bytes_per_record_avg',
93+
'-nan',
94+
'offsets_per_term_avg',
95+
'-nan',
96+
'offset_bits_per_record_avg',
97+
'-nan',
98+
'hash_indexing_failures',
99+
'0',
100+
'indexing',
101+
'0',
102+
'percent_indexed',
103+
'1',
104+
'gc_stats',
105+
[
106+
'bytes_collected',
107+
'0',
108+
'total_ms_run',
109+
'0',
110+
'total_cycles',
111+
'0',
112+
'average_cycle_time_ms',
113+
'-nan',
114+
'last_run_time_ms',
115+
'0',
116+
'gc_numeric_trees_missed',
117+
'0',
118+
'gc_blocks_denied',
119+
'0',
120+
],
121+
'cursor_stats',
122+
[
123+
'global_idle',
124+
0,
125+
'global_total',
126+
0,
127+
'index_capacity',
128+
128,
129+
'index_total',
130+
0,
131+
],
132+
'dialect_stats',
133+
[
134+
'dialect_1',
135+
'0',
136+
'dialect_2',
137+
'0',
138+
]
139+
];
140+
141+
export const mockFtInfoAnalyticsData = {
142+
attributes: [
143+
{
144+
type: 'TEXT',
145+
weight: '1',
146+
},
147+
{
148+
nostem: true,
149+
type: 'TEXT',
150+
weight: '1',
151+
},
152+
{
153+
type: 'TEXT',
154+
weight: '1',
155+
},
156+
{
157+
noindex: true,
158+
type: 'NUMERIC',
159+
},
160+
{
161+
casesensitive: true,
162+
sortable: true,
163+
type: 'TAG',
164+
},
165+
],
166+
default_score: '1',
167+
key_type: 'HASH',
168+
max_doc_id: '0',
169+
num_docs: '0',
170+
num_records: '0',
171+
num_terms: '0',
172+
dialect_stats: {
173+
dialect_1: '0',
174+
dialect_2: '0',
175+
}
176+
};
177+
178+
type InfoReplyRaw = string | number | InfoReplyRaw[];
179+
export const replyToBuffer = (input: InfoReplyRaw[]) => {
180+
if (isArray(input)) {
181+
return input.map(replyToBuffer);
182+
}
183+
184+
return isString(input) ? Buffer.from(input) : input;
185+
};

redisinsight/api/src/constants/telemetry-events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ export enum TelemetryEvents {
4747
CliClientDeleted = 'CLI_CLIENT_DELETED',
4848
CliClientRecreated = 'CLI_CLIENT_RECREATED',
4949
CliCommandExecuted = 'CLI_COMMAND_EXECUTED',
50+
CliIndexInfoSubmitted = 'CLI_INDEX_INFO_SUBMITTED',
5051
CliClusterNodeCommandExecuted = 'CLI_CLUSTER_COMMAND_EXECUTED',
5152
CliCommandErrorReceived = 'CLI_COMMAND_ERROR_RECEIVED',
5253

5354
// Events for workbench tool
5455
WorkbenchCommandExecuted = 'WORKBENCH_COMMAND_EXECUTED',
56+
WorkbenchIndexInfoSubmitted = 'WORKBENCH_INDEX_INFO_SUBMITTED',
5557
WorkbenchCommandErrorReceived = 'WORKBENCH_COMMAND_ERROR_RECEIVED',
5658
WorkbenchCommandDeleted = 'WORKBENCH_COMMAND_DELETE_COMMAND',
5759
// Custom tutorials

redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ describe('CliAnalyticsService', () => {
8484
});
8585
});
8686

87+
describe('sendCliClientCreatedEvent', () => {
88+
it('should emit CliIndexInfoSubmitted event', () => {
89+
service.sendIndexInfoEvent(databaseId, mockCustomData);
90+
91+
expect(sendEventMethod).toHaveBeenCalledWith(
92+
TelemetryEvents.CliIndexInfoSubmitted,
93+
{
94+
databaseId,
95+
...mockCustomData,
96+
},
97+
);
98+
});
99+
it('should not fail and should not emit when there is no data', () => {
100+
service.sendIndexInfoEvent(databaseId, null);
101+
102+
expect(sendEventMethod).not.toHaveBeenCalled();
103+
});
104+
});
105+
87106
describe('sendCliClientCreatedEvent', () => {
88107
it('should emit CliClientCreated event', () => {
89108
service.sendClientCreatedEvent(databaseId, mockCustomData);

redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ export class CliAnalyticsService extends CommandTelemetryBaseService {
7676
}
7777
}
7878

79+
sendIndexInfoEvent(
80+
databaseId: string,
81+
additionalData: object,
82+
): void {
83+
if (!additionalData) {
84+
return;
85+
}
86+
87+
try {
88+
this.sendEvent(
89+
TelemetryEvents.CliIndexInfoSubmitted,
90+
{
91+
databaseId,
92+
...additionalData,
93+
},
94+
);
95+
} catch (e) {
96+
// ignore error
97+
}
98+
}
99+
79100
public async sendCommandExecutedEvent(
80101
databaseId: string,
81102
additionalData: object = {},

redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
mockCliClientMetadata,
1414
mockDatabaseClientFactory,
1515
mockStandaloneRedisClient,
16-
mockClusterRedisClient,
16+
mockClusterRedisClient, mockRedisFtInfoReply, mockFtInfoAnalyticsData,
1717
} from 'src/__mocks__';
1818
import {
1919
CommandExecutionStatus,
@@ -232,6 +232,34 @@ describe('CliBusinessService', () => {
232232
});
233233

234234
describe('sendCommand', () => {
235+
it('should successfully execute ft.info command', async () => {
236+
const dto: SendCommandDto = { command: 'ft.info idx' };
237+
const formatSpy = jest.spyOn(rawFormatter, 'format');
238+
const mockResult: SendCommandResponse = {
239+
response: mockRedisFtInfoReply,
240+
status: CommandExecutionStatus.Success,
241+
};
242+
when(standaloneClient.sendCommand)
243+
.calledWith(['ft.info', 'idx'], expect.anything())
244+
.mockReturnValue(mockRedisFtInfoReply);
245+
246+
const result = await service.sendCommand(mockCliClientMetadata, dto);
247+
248+
expect(result).toEqual(mockResult);
249+
expect(formatSpy).toHaveBeenCalled();
250+
expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith(
251+
mockCliClientMetadata.databaseId,
252+
{
253+
command: 'ft.info',
254+
outputFormat: CliOutputFormatterTypes.Raw,
255+
},
256+
);
257+
expect(analyticsService.sendIndexInfoEvent).toHaveBeenCalledWith(
258+
mockCliClientMetadata.databaseId,
259+
mockFtInfoAnalyticsData,
260+
);
261+
});
262+
235263
it('should successfully execute command (RAW format)', async () => {
236264
const dto: SendCommandDto = { command: mockMemoryUsageCommand };
237265
const formatSpy = jest.spyOn(rawFormatter, 'format');

redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ import { DatabaseRecommendationService } from 'src/modules/database-recommendati
3030
import { RedisClient } from 'src/modules/redis/client';
3131
import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';
3232
import { v4 as uuidv4 } from 'uuid';
33+
import { getAnalyticsDataFromIndexInfo } from 'src/utils';
3334
import { OutputFormatterManager } from './output-formatter/output-formatter-manager';
3435
import { CliOutputFormatterTypes } from './output-formatter/output-formatter.interface';
3536
import { TextFormatterStrategy } from './output-formatter/strategies/text-formatter.strategy';
3637
import { RawFormatterStrategy } from './output-formatter/strategies/raw-formatter.strategy';
38+
import {inspect} from "util";
3739

3840
@Injectable()
3941
export class CliBusinessService {
@@ -167,6 +169,13 @@ export class CliBusinessService {
167169
},
168170
);
169171

172+
if (command.toLowerCase() === 'ft.info') {
173+
this.cliAnalyticsService.sendIndexInfoEvent(
174+
clientMetadata.databaseId,
175+
getAnalyticsDataFromIndexInfo(reply as string[]),
176+
);
177+
}
178+
170179
this.logger.log('Succeed to execute redis CLI command.');
171180

172181
return {

redisinsight/api/src/modules/redis/utils/reply.util.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { chunk } from 'lodash';
1+
import { chunk, isArray } from 'lodash';
22
import { IRedisClusterNode } from 'src/models';
33

44
/**
@@ -20,13 +20,20 @@ import { IRedisClusterNode } from 'src/models';
2020
* }
2121
* ```
2222
* @param input
23+
* @param options
2324
*/
24-
export const convertArrayReplyToObject = (input: string[]): { [key: string]: any } => chunk(
25+
export const convertArrayReplyToObject = (
26+
input: string[],
27+
options: { utf?: boolean } = {},
28+
): { [key: string]: any } => chunk(
2529
input,
2630
2,
2731
).reduce((prev: any, current: string[]) => {
2832
const [key, value] = current;
29-
return { ...prev, [key.toString().toLowerCase()]: value };
33+
return {
34+
...prev,
35+
[key.toString().toLowerCase()]: options.utf && !isArray(value) ? value?.toString() : value,
36+
};
3037
}, {});
3138

3239
/**

0 commit comments

Comments
 (0)