diff --git a/redisinsight/api/src/constants/telemetry-events.ts b/redisinsight/api/src/constants/telemetry-events.ts index 31fd085378..b443e52bdd 100644 --- a/redisinsight/api/src/constants/telemetry-events.ts +++ b/redisinsight/api/src/constants/telemetry-events.ts @@ -58,6 +58,12 @@ export enum TelemetryEvents { WorkbenchIndexInfoSubmitted = 'WORKBENCH_INDEX_INFO_SUBMITTED', WorkbenchCommandErrorReceived = 'WORKBENCH_COMMAND_ERROR_RECEIVED', WorkbenchCommandDeleted = 'WORKBENCH_COMMAND_DELETE_COMMAND', + + // Events for search tool + SearchCommandExecuted = 'SEARCH_COMMAND_EXECUTED', + SearchIndexInfoSubmitted = 'SEARCH_INDEX_INFO_SUBMITTED', + SearchCommandErrorReceived = 'SEARCH_COMMAND_ERROR_RECEIVED', + // Custom tutorials WorkbenchEnablementAreaImportSucceeded = 'WORKBENCH_ENABLEMENT_AREA_IMPORT_SUCCEEDED', WorkbenchEnablementAreaImportFailed = 'WORKBENCH_ENABLEMENT_AREA_IMPORT_FAILED', diff --git a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts index a0e6b99e7b..a4eca0be94 100644 --- a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts +++ b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts @@ -26,7 +26,10 @@ import { FormatterTypes, } from 'src/common/transformers'; import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory'; -import { RunQueryMode } from 'src/modules/workbench/models/command-execution'; +import { + CommandExecutionType, + RunQueryMode, +} from 'src/modules/workbench/models/command-execution'; import { WorkbenchAnalytics } from 'src/modules/workbench/workbench.analytics'; const MOCK_ERROR_MESSAGE = 'Some error'; @@ -85,6 +88,7 @@ describe('WorkbenchCommandsExecutor', () => { const result = await service.sendCommand(client, { command: 'ft.info idx', mode: RunQueryMode.Raw, + type: CommandExecutionType.Workbench, }); expect(result).toEqual([ @@ -99,6 +103,7 @@ describe('WorkbenchCommandsExecutor', () => { ).toHaveBeenCalledWith( mockSessionMetadata, mockWorkbenchClientMetadata.databaseId, + CommandExecutionType.Workbench, [ { response: mockRedisFtInfoReply, @@ -113,6 +118,7 @@ describe('WorkbenchCommandsExecutor', () => { expect(mockAnalyticsService.sendIndexInfoEvent).toHaveBeenCalledWith( mockSessionMetadata, mockWorkbenchClientMetadata.databaseId, + CommandExecutionType.Workbench, mockFtInfoAnalyticsData, ); }); @@ -122,6 +128,7 @@ describe('WorkbenchCommandsExecutor', () => { const result = await service.sendCommand(client, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, + type: CommandExecutionType.Workbench, }); expect(result).toEqual([ @@ -136,6 +143,7 @@ describe('WorkbenchCommandsExecutor', () => { ).toHaveBeenCalledWith( mockSessionMetadata, mockWorkbenchClientMetadata.databaseId, + CommandExecutionType.Workbench, [ { response: mockCommandExecutionResult.response, @@ -156,6 +164,7 @@ describe('WorkbenchCommandsExecutor', () => { const result = await service.sendCommand(client, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, + type: CommandExecutionType.Workbench, }); expect(result).toEqual([ @@ -170,6 +179,7 @@ describe('WorkbenchCommandsExecutor', () => { ).toHaveBeenCalledWith( mockSessionMetadata, mockWorkbenchClientMetadata.databaseId, + CommandExecutionType.Workbench, { response: MOCK_ERROR_MESSAGE, error: new CommandNotSupportedError(MOCK_ERROR_MESSAGE), @@ -192,6 +202,7 @@ describe('WorkbenchCommandsExecutor', () => { const result = await service.sendCommand(client, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, + type: CommandExecutionType.Workbench, }); expect(result).toEqual([ @@ -206,6 +217,7 @@ describe('WorkbenchCommandsExecutor', () => { ).toHaveBeenCalledWith( mockSessionMetadata, mockWorkbenchClientMetadata.databaseId, + CommandExecutionType.Workbench, { response: MOCK_ERROR_MESSAGE, error: replyError, @@ -227,6 +239,7 @@ describe('WorkbenchCommandsExecutor', () => { const result = await service.sendCommand(client, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, + type: CommandExecutionType.Workbench, }); expect(result).toEqual([ @@ -242,6 +255,7 @@ describe('WorkbenchCommandsExecutor', () => { ).toHaveBeenCalledWith( mockSessionMetadata, mockWorkbenchClientMetadata.databaseId, + CommandExecutionType.Workbench, [ { response: mockCommandExecutionResult.response, @@ -264,6 +278,7 @@ describe('WorkbenchCommandsExecutor', () => { const result = await service.sendCommand(client, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.Raw, + type: CommandExecutionType.Workbench, }); expect(result).toEqual([ @@ -279,6 +294,7 @@ describe('WorkbenchCommandsExecutor', () => { ).toHaveBeenCalledWith( mockSessionMetadata, mockWorkbenchClientMetadata.databaseId, + CommandExecutionType.Workbench, [ { response: mockCommandExecutionResult.response, @@ -299,6 +315,7 @@ describe('WorkbenchCommandsExecutor', () => { const result = await service.sendCommand(client, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, + type: CommandExecutionType.Workbench, }); expect(result).toEqual([ @@ -313,6 +330,7 @@ describe('WorkbenchCommandsExecutor', () => { ).toHaveBeenCalledWith( mockSessionMetadata, mockWorkbenchClientMetadata.databaseId, + CommandExecutionType.Workbench, { response: MOCK_ERROR_MESSAGE, error: new ServiceUnavailableException(MOCK_ERROR_MESSAGE), @@ -337,6 +355,7 @@ describe('WorkbenchCommandsExecutor', () => { const result = await service.sendCommand(client, { command: mockGetEscapedKeyCommand, mode: RunQueryMode.ASCII, + type: CommandExecutionType.Workbench, }); expect(result).toEqual(mockResult); @@ -345,6 +364,7 @@ describe('WorkbenchCommandsExecutor', () => { ).toHaveBeenCalledWith( mockSessionMetadata, mockWorkbenchClientMetadata.databaseId, + CommandExecutionType.Workbench, { response: ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(), error: new CommandParsingError( diff --git a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts index 72d5fa15a2..1fafe3e15e 100644 --- a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts +++ b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts @@ -79,6 +79,7 @@ export class WorkbenchCommandsExecutor { this.analyticsService.sendCommandExecutedEvents( client.clientMetadata.sessionMetadata, client.clientMetadata.databaseId, + dto.type, result, { command, rawMode: mode === RunQueryMode.Raw }, ); @@ -87,6 +88,7 @@ export class WorkbenchCommandsExecutor { this.analyticsService.sendIndexInfoEvent( client.clientMetadata.sessionMetadata, client.clientMetadata.databaseId, + dto.type, getAnalyticsDataFromIndexInfo(response as string[]), ); } @@ -102,6 +104,7 @@ export class WorkbenchCommandsExecutor { this.analyticsService.sendCommandExecutedEvent( client.clientMetadata.sessionMetadata, client.clientMetadata.databaseId, + dto.type, { ...errorResult, error }, { command, rawMode: dto.mode === RunQueryMode.Raw }, ); diff --git a/redisinsight/api/src/modules/workbench/workbench.analytics.spec.ts b/redisinsight/api/src/modules/workbench/workbench.analytics.spec.ts index aad27f6712..c79dcabb7f 100644 --- a/redisinsight/api/src/modules/workbench/workbench.analytics.spec.ts +++ b/redisinsight/api/src/modules/workbench/workbench.analytics.spec.ts @@ -13,6 +13,7 @@ import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { CommandParsingError } from 'src/modules/cli/constants/errors'; import { CommandsService } from 'src/modules/commands/commands.service'; import { WorkbenchAnalytics } from './workbench.analytics'; +import { CommandExecutionType } from './models/command-execution'; const redisReplyError: ReplyError = { ...mockRedisWrongTypeError, @@ -81,10 +82,15 @@ describe('WorkbenchAnalytics', () => { }); describe('sendIndexInfoEvent', () => { - it('should emit index info event', async () => { - service.sendIndexInfoEvent(mockSessionMetadata, instanceId, { - any: 'fields', - }); + it('should emit index info event for Workbench commands', async () => { + service.sendIndexInfoEvent( + mockSessionMetadata, + instanceId, + CommandExecutionType.Workbench, + { + any: 'fields', + }, + ); expect(sendEventMethod).toHaveBeenCalledWith( mockSessionMetadata, @@ -95,17 +101,42 @@ describe('WorkbenchAnalytics', () => { }, ); }); + it('should emit index info event for Search commands', async () => { + service.sendIndexInfoEvent( + mockSessionMetadata, + instanceId, + CommandExecutionType.Search, + { + any: 'fields', + }, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + mockSessionMetadata, + TelemetryEvents.SearchIndexInfoSubmitted, + { + databaseId: instanceId, + any: 'fields', + }, + ); + }); it('should not fail and should not emit when no data to send', async () => { - service.sendIndexInfoEvent(mockSessionMetadata, instanceId, null); + service.sendIndexInfoEvent( + mockSessionMetadata, + instanceId, + CommandExecutionType.Workbench, + null, + ); expect(sendEventMethod).not.toHaveBeenCalled(); }); }); describe('sendCommandExecutedEvents', () => { - it('should emit multiple events', async () => { + it('should emit multiple Workbench events', async () => { await service.sendCommandExecutedEvents( mockSessionMetadata, instanceId, + CommandExecutionType.Workbench, [ { response: 'OK', status: CommandExecutionStatus.Success }, { response: 'OK', status: CommandExecutionStatus.Success }, @@ -126,12 +157,38 @@ describe('WorkbenchAnalytics', () => { }, ); }); + it('should emit multiple Search events', async () => { + await service.sendCommandExecutedEvents( + mockSessionMetadata, + instanceId, + CommandExecutionType.Search, + [ + { response: 'OK', status: CommandExecutionStatus.Success }, + { response: 'OK', status: CommandExecutionStatus.Success }, + ], + { command: 'set' }, + ); + + expect(sendEventMethod).toHaveBeenCalledTimes(2); + expect(sendEventMethod).toHaveBeenCalledWith( + mockSessionMetadata, + TelemetryEvents.SearchCommandExecuted, + { + databaseId: instanceId, + command: 'set', + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', + }, + ); + }); }); describe('sendCommandExecutedEvent', () => { it('should emit WorkbenchCommandExecuted event', async () => { await service.sendCommandExecutedEvent( mockSessionMetadata, instanceId, + CommandExecutionType.Workbench, { response: 'OK', status: CommandExecutionStatus.Success }, { command: 'set' }, ); @@ -156,6 +213,7 @@ describe('WorkbenchAnalytics', () => { await service.sendCommandExecutedEvent( mockSessionMetadata, instanceId, + CommandExecutionType.Workbench, { response: 'OK', status: CommandExecutionStatus.Success }, { command: 'set' }, ); @@ -173,6 +231,7 @@ describe('WorkbenchAnalytics', () => { await service.sendCommandExecutedEvent( mockSessionMetadata, instanceId, + CommandExecutionType.Workbench, { response: 'OK', status: CommandExecutionStatus.Success }, { command: 'bF.rEsErvE' }, ); @@ -193,6 +252,7 @@ describe('WorkbenchAnalytics', () => { await service.sendCommandExecutedEvent( mockSessionMetadata, instanceId, + CommandExecutionType.Workbench, { response: 'OK', status: CommandExecutionStatus.Success }, { command: 'CUSTOM.COMMAnd' }, ); @@ -213,6 +273,7 @@ describe('WorkbenchAnalytics', () => { await service.sendCommandExecutedEvent( mockSessionMetadata, instanceId, + CommandExecutionType.Workbench, { response: 'OK', status: CommandExecutionStatus.Success }, { command: 'some.command' }, ); @@ -230,10 +291,15 @@ describe('WorkbenchAnalytics', () => { ); }); it('should emit WorkbenchCommandExecuted event without additional data', async () => { - await service.sendCommandExecutedEvent(mockSessionMetadata, instanceId, { - response: 'OK', - status: CommandExecutionStatus.Success, - }); + await service.sendCommandExecutedEvent( + mockSessionMetadata, + instanceId, + CommandExecutionType.Workbench, + { + response: 'OK', + status: CommandExecutionStatus.Success, + }, + ); expect(sendEventMethod).toHaveBeenCalledWith( mockSessionMetadata, @@ -247,6 +313,7 @@ describe('WorkbenchAnalytics', () => { await service.sendCommandExecutedEvent( mockSessionMetadata, instanceId, + CommandExecutionType.Workbench, { response: 'Error', error: redisReplyError, @@ -270,11 +337,16 @@ describe('WorkbenchAnalytics', () => { ); }); it('should emit WorkbenchCommandError event without additional data', async () => { - await service.sendCommandExecutedEvent(mockSessionMetadata, instanceId, { - response: 'Error', - error: redisReplyError, - status: CommandExecutionStatus.Fail, - }); + await service.sendCommandExecutedEvent( + mockSessionMetadata, + instanceId, + CommandExecutionType.Workbench, + { + response: 'Error', + error: redisReplyError, + status: CommandExecutionStatus.Fail, + }, + ); expect(sendEventMethod).toHaveBeenCalledWith( mockSessionMetadata, @@ -288,11 +360,16 @@ describe('WorkbenchAnalytics', () => { }); it('should emit WorkbenchCommandError event for custom error', async () => { const error: any = CommandParsingError; - await service.sendCommandExecutedEvent(mockSessionMetadata, instanceId, { - response: 'Error', - status: CommandExecutionStatus.Fail, - error, - }); + await service.sendCommandExecutedEvent( + mockSessionMetadata, + instanceId, + CommandExecutionType.Workbench, + { + response: 'Error', + status: CommandExecutionStatus.Fail, + error, + }, + ); expect(sendEventMethod).toHaveBeenCalledWith( mockSessionMetadata, @@ -306,11 +383,16 @@ describe('WorkbenchAnalytics', () => { }); it('should emit WorkbenchCommandError event for HttpException', async () => { const error = new ServiceUnavailableException(); - await service.sendCommandExecutedEvent(mockSessionMetadata, instanceId, { - response: 'Error', - status: CommandExecutionStatus.Fail, - error, - }); + await service.sendCommandExecutedEvent( + mockSessionMetadata, + instanceId, + CommandExecutionType.Workbench, + { + response: 'Error', + status: CommandExecutionStatus.Fail, + error, + }, + ); expect(sendFailedEventMethod).toHaveBeenCalledWith( mockSessionMetadata, @@ -319,6 +401,54 @@ describe('WorkbenchAnalytics', () => { { databaseId: instanceId }, ); }); + it('should emit SearchCommandExecuted event', async () => { + await service.sendCommandExecutedEvent( + mockSessionMetadata, + instanceId, + CommandExecutionType.Search, + { response: 'OK', status: CommandExecutionStatus.Success }, + { command: 'set' }, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + mockSessionMetadata, + TelemetryEvents.SearchCommandExecuted, + { + databaseId: instanceId, + command: 'set', + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', + }, + ); + }); + it('should emit SearchCommandError event', async () => { + await service.sendCommandExecutedEvent( + mockSessionMetadata, + instanceId, + CommandExecutionType.Search, + { + response: 'Error', + error: redisReplyError, + status: CommandExecutionStatus.Fail, + }, + { command: 'set', data: 'Some data' }, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + mockSessionMetadata, + TelemetryEvents.SearchCommandErrorReceived, + { + databaseId: instanceId, + error: ReplyError.name, + command: 'set', + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', + data: 'Some data', + }, + ); + }); }); describe('sendCommandDeletedEvent', () => { it('should emit WorkbenchCommandDeleted event', () => { diff --git a/redisinsight/api/src/modules/workbench/workbench.analytics.ts b/redisinsight/api/src/modules/workbench/workbench.analytics.ts index b142e9f09c..ac55d7e19a 100644 --- a/redisinsight/api/src/modules/workbench/workbench.analytics.ts +++ b/redisinsight/api/src/modules/workbench/workbench.analytics.ts @@ -6,6 +6,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { CommandsService } from 'src/modules/commands/commands.service'; import { CommandTelemetryBaseService } from 'src/modules/analytics/command.telemetry.base.service'; import { SessionMetadata } from 'src/common/models'; +import { CommandExecutionType } from './models/command-execution'; export interface IExecResult { response: any; @@ -25,6 +26,7 @@ export class WorkbenchAnalytics extends CommandTelemetryBaseService { sendIndexInfoEvent( sessionMetadata: SessionMetadata, databaseId: string, + commandExecutionType: CommandExecutionType, additionalData: object, ): void { if (!additionalData) { @@ -32,14 +34,15 @@ export class WorkbenchAnalytics extends CommandTelemetryBaseService { } try { - this.sendEvent( - sessionMetadata, - TelemetryEvents.WorkbenchIndexInfoSubmitted, - { - databaseId, - ...additionalData, - }, - ); + const event = + commandExecutionType === CommandExecutionType.Search + ? TelemetryEvents.SearchIndexInfoSubmitted + : TelemetryEvents.WorkbenchIndexInfoSubmitted; + + this.sendEvent(sessionMetadata, event, { + databaseId, + ...additionalData, + }); } catch (e) { // ignore error } @@ -48,6 +51,7 @@ export class WorkbenchAnalytics extends CommandTelemetryBaseService { public async sendCommandExecutedEvents( sessionMetadata: SessionMetadata, databaseId: string, + commandExecutionType: CommandExecutionType, results: IExecResult[], additionalData: object = {}, ): Promise { @@ -57,6 +61,7 @@ export class WorkbenchAnalytics extends CommandTelemetryBaseService { this.sendCommandExecutedEvent( sessionMetadata, databaseId, + commandExecutionType, result, additionalData, ), @@ -70,28 +75,36 @@ export class WorkbenchAnalytics extends CommandTelemetryBaseService { public async sendCommandExecutedEvent( sessionMetadata: SessionMetadata, databaseId: string, + commandExecutionType: CommandExecutionType, result: IExecResult, additionalData: object = {}, ): Promise { const { status } = result; try { if (status === CommandExecutionStatus.Success) { - this.sendEvent( + const event = + commandExecutionType === CommandExecutionType.Search + ? TelemetryEvents.SearchCommandExecuted + : TelemetryEvents.WorkbenchCommandExecuted; + + this.sendEvent(sessionMetadata, event, { + databaseId, + ...(await this.getCommandAdditionalInfo(additionalData['command'])), + ...additionalData, + }); + } + if (status === CommandExecutionStatus.Fail) { + this.sendCommandErrorEvent( sessionMetadata, - TelemetryEvents.WorkbenchCommandExecuted, + databaseId, + result.error, + commandExecutionType, { - databaseId, ...(await this.getCommandAdditionalInfo(additionalData['command'])), ...additionalData, }, ); } - if (status === CommandExecutionStatus.Fail) { - this.sendCommandErrorEvent(sessionMetadata, databaseId, result.error, { - ...(await this.getCommandAdditionalInfo(additionalData['command'])), - ...additionalData, - }); - } } catch (e) { // continue regardless of error } @@ -112,30 +125,27 @@ export class WorkbenchAnalytics extends CommandTelemetryBaseService { sessionMetadata: SessionMetadata, databaseId: string, error: any, + commandExecutionType: CommandExecutionType, additionalData: object = {}, ): void { try { + const event = + commandExecutionType === CommandExecutionType.Search + ? TelemetryEvents.SearchCommandErrorReceived + : TelemetryEvents.WorkbenchCommandErrorReceived; + if (error instanceof HttpException) { - this.sendFailedEvent( - sessionMetadata, - TelemetryEvents.WorkbenchCommandErrorReceived, - error, - { - databaseId, - ...additionalData, - }, - ); + this.sendFailedEvent(sessionMetadata, event, error, { + databaseId, + ...additionalData, + }); } else { - this.sendEvent( - sessionMetadata, - TelemetryEvents.WorkbenchCommandErrorReceived, - { - databaseId, - error: error.name, - command: error?.command?.name, - ...additionalData, - }, - ); + this.sendEvent(sessionMetadata, event, { + databaseId, + error: error.name, + command: error?.command?.name, + ...additionalData, + }); } } catch (e) { // continue regardless of error