diff --git a/redisinsight/api/src/__mocks__/analytics.ts b/redisinsight/api/src/__mocks__/analytics.ts index 659e31e891..df985304c6 100644 --- a/redisinsight/api/src/__mocks__/analytics.ts +++ b/redisinsight/api/src/__mocks__/analytics.ts @@ -23,14 +23,14 @@ export const mockBrowserAnalyticsService = () => ({ }); export const mockCliAnalyticsService = () => ({ - sendCliClientCreatedEvent: jest.fn(), - sendCliClientCreationFailedEvent: jest.fn(), - sendCliClientDeletedEvent: jest.fn(), - sendCliClientRecreatedEvent: jest.fn(), - sendCliCommandExecutedEvent: jest.fn(), - sendCliCommandErrorEvent: jest.fn(), - sendCliClusterCommandExecutedEvent: jest.fn(), - sendCliConnectionErrorEvent: jest.fn(), + sendClientCreatedEvent: jest.fn(), + sendClientCreationFailedEvent: jest.fn(), + sendClientDeletedEvent: jest.fn(), + sendClientRecreatedEvent: jest.fn(), + sendCommandExecutedEvent: jest.fn(), + sendCommandErrorEvent: jest.fn(), + sendClusterCommandExecutedEvent: jest.fn(), + sendConnectionErrorEvent: jest.fn(), }); export const mockSettingsAnalyticsService = () => ({ diff --git a/redisinsight/api/src/constants/telemetry-events.ts b/redisinsight/api/src/constants/telemetry-events.ts index b2666b2b1a..16977d5f9e 100644 --- a/redisinsight/api/src/constants/telemetry-events.ts +++ b/redisinsight/api/src/constants/telemetry-events.ts @@ -38,12 +38,12 @@ export enum TelemetryEvents { BrowserJSONPropertyDeleted = 'BROWSER_JSON_PROPERTY_DELETED', // Events for cli tool - CliClientCreated = 'CLI_CLIENT_CREATED', - CliClientCreationFailed = 'CLI_CLIENT_CREATION_FAILED', - CliClientConnectionError = 'CLI_CLIENT_CONNECTION_ERROR', - CliClientDeleted = 'CLI_CLIENT_DELETED', - CliClientRecreated = 'CLI_CLIENT_RECREATED', - CliCommandExecuted = 'CLI_COMMAND_EXECUTED', - CliClusterNodeCommandExecuted = 'CLI_CLUSTER_COMMAND_EXECUTED', - CliCommandErrorReceived = 'CLI_COMMAND_ERROR_RECEIVED', + ClientCreated = 'CLIENT_CREATED', + ClientCreationFailed = 'CLIENT_CREATION_FAILED', + ClientConnectionError = 'CLIENT_CONNECTION_ERROR', + ClientDeleted = 'CLIENT_DELETED', + ClientRecreated = 'CLIENT_RECREATED', + CommandExecuted = 'COMMAND_EXECUTED', + ClusterNodeCommandExecuted = 'CLUSTER_COMMAND_EXECUTED', + CommandErrorReceived = 'COMMAND_ERROR_RECEIVED', } diff --git a/redisinsight/api/src/modules/cli/controllers/cli.controller.ts b/redisinsight/api/src/modules/cli/controllers/cli.controller.ts index 974006cdf9..d99f6feeb0 100644 --- a/redisinsight/api/src/modules/cli/controllers/cli.controller.ts +++ b/redisinsight/api/src/modules/cli/controllers/cli.controller.ts @@ -132,7 +132,8 @@ export class CliController { async reCreateClient( @Param('dbInstance') dbInstance: string, @Param('uuid') uuid: string, + @Body() dto: CreateCliClientDto, ): Promise { - return this.service.reCreateClient(dbInstance, uuid); + return this.service.reCreateClient(dbInstance, uuid, dto.namespace); } } diff --git a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts index 2aec654282..def69fea95 100644 --- a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts +++ b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts @@ -3,7 +3,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { InternalServerErrorException } from '@nestjs/common'; import { mockRedisWrongTypeError, mockStandaloneDatabaseEntity } from 'src/__mocks__'; import { TelemetryEvents } from 'src/constants'; -import { ReplyError } from 'src/models'; +import { AppTool, ReplyError } from 'src/models'; import { CliParsingError } from 'src/modules/cli/constants/errors'; import { ICliExecResultFromNode } from 'src/modules/cli/services/cli-tool/cli-tool.service'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; @@ -42,10 +42,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientCreatedEvent', () => { it('should emit CliClientCreated event', () => { - service.sendCliClientCreatedEvent(instanceId, { data: 'Some data' }); + service.sendClientCreatedEvent(instanceId, AppTool.CLI, { data: 'Some data' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientCreated, + `CLI_${TelemetryEvents.ClientCreated}`, { databaseId: instanceId, data: 'Some data', @@ -53,10 +53,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliClientCreated event without additional data', () => { - service.sendCliClientCreatedEvent(instanceId); + service.sendClientCreatedEvent(instanceId, AppTool.CLI); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientCreated, + `CLI_${TelemetryEvents.ClientCreated}`, { databaseId: instanceId, }, @@ -66,10 +66,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientCreationFailedEvent', () => { it('should emit CliClientCreationFailed event', () => { - service.sendCliClientCreationFailedEvent(instanceId, httpException, { data: 'Some data' }); + service.sendClientCreationFailedEvent(instanceId, AppTool.CLI, httpException, { data: 'Some data' }); expect(sendFailedEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientCreationFailed, + `CLI_${TelemetryEvents.ClientCreationFailed}`, httpException, { databaseId: instanceId, @@ -78,10 +78,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliClientCreationFailed event without additional data', () => { - service.sendCliClientCreationFailedEvent(instanceId, httpException); + service.sendClientCreationFailedEvent(instanceId, AppTool.CLI, httpException); expect(sendFailedEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientCreationFailed, + `CLI_${TelemetryEvents.ClientCreationFailed}`, httpException, { databaseId: instanceId, @@ -92,10 +92,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientRecreatedEvent', () => { it('should emit CliClientRecreated event', () => { - service.sendCliClientRecreatedEvent(instanceId, { data: 'Some data' }); + service.sendClientRecreatedEvent(instanceId, AppTool.CLI, { data: 'Some data' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientRecreated, + `CLI_${TelemetryEvents.ClientRecreated}`, { databaseId: instanceId, data: 'Some data', @@ -103,10 +103,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliClientRecreated event without additional data', () => { - service.sendCliClientRecreatedEvent(instanceId); + service.sendClientRecreatedEvent(instanceId, AppTool.CLI); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientRecreated, + `CLI_${TelemetryEvents.ClientRecreated}`, { databaseId: instanceId, }, @@ -116,10 +116,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientDeletedEvent', () => { it('should emit CliClientDeleted event', () => { - service.sendCliClientDeletedEvent(1, instanceId, { data: 'Some data' }); + service.sendClientDeletedEvent(1, instanceId, AppTool.CLI, { data: 'Some data' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientDeleted, + `CLI_${TelemetryEvents.ClientDeleted}`, { databaseId: instanceId, data: 'Some data', @@ -127,35 +127,35 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliClientDeleted event without additional data', () => { - service.sendCliClientDeletedEvent(1, instanceId); + service.sendClientDeletedEvent(1, instanceId, AppTool.CLI); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientDeleted, + `CLI_${TelemetryEvents.ClientDeleted}`, { databaseId: instanceId, }, ); }); it('should not emit event', () => { - service.sendCliClientDeletedEvent(0, instanceId); + service.sendClientDeletedEvent(0, instanceId, AppTool.CLI); expect(sendEventMethod).not.toHaveBeenCalled(); }); it('should not emit event on invalid input values', () => { const input: any = {}; - service.sendCliClientDeletedEvent(input, instanceId); + service.sendClientDeletedEvent(input, instanceId, AppTool.CLI); - expect(() => service.sendCliClientDeletedEvent(input, instanceId)).not.toThrow(); + expect(() => service.sendClientDeletedEvent(input, instanceId, AppTool.CLI)).not.toThrow(); expect(sendEventMethod).not.toHaveBeenCalled(); }); }); describe('sendCliCommandExecutedEvent', () => { it('should emit CliCommandExecuted event', () => { - service.sendCliCommandExecutedEvent(instanceId, { command: 'info' }); + service.sendCommandExecutedEvent(instanceId, AppTool.CLI, { command: 'info' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandExecuted, + `CLI_${TelemetryEvents.CommandExecuted}`, { databaseId: instanceId, command: 'info', @@ -163,10 +163,42 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliCommandExecuted event without additional data', () => { - service.sendCliCommandExecutedEvent(instanceId); + service.sendCommandExecutedEvent(instanceId, AppTool.CLI); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandExecuted, + `CLI_${TelemetryEvents.CommandExecuted}`, + { + databaseId: instanceId, + }, + ); + }); + it('should emit CliCommandExecuted for undefined namespace', () => { + service.sendCommandExecutedEvent(instanceId, undefined, { command: 'info' }); + + expect(sendEventMethod).toHaveBeenCalledWith( + `CLI_${TelemetryEvents.CommandExecuted}`, + { + databaseId: instanceId, + command: 'info', + }, + ); + }); + it('should emit WorkbenchCommandExecuted event', () => { + service.sendCommandExecutedEvent(instanceId, 'workbench', { command: 'info' }); + + expect(sendEventMethod).toHaveBeenCalledWith( + `WORKBENCH_${TelemetryEvents.CommandExecuted}`, + { + databaseId: instanceId, + command: 'info', + }, + ); + }); + it('should emit WorkbenchCommandExecuted event without additional data', () => { + service.sendCommandExecutedEvent(instanceId, 'workbench'); + + expect(sendEventMethod).toHaveBeenCalledWith( + `WORKBENCH_${TelemetryEvents.CommandExecuted}`, { databaseId: instanceId, }, @@ -176,10 +208,10 @@ describe('CliAnalyticsService', () => { describe('sendCliCommandErrorEvent', () => { it('should emit CliCommandError event', () => { - service.sendCliCommandErrorEvent(instanceId, redisReplyError, { data: 'Some data' }); + service.sendCommandErrorEvent(instanceId, AppTool.CLI, redisReplyError, { data: 'Some data' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: ReplyError.name, @@ -189,10 +221,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliCommandError event without additional data', () => { - service.sendCliCommandErrorEvent(instanceId, redisReplyError); + service.sendCommandErrorEvent(instanceId, AppTool.CLI, redisReplyError); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: ReplyError.name, @@ -202,10 +234,10 @@ describe('CliAnalyticsService', () => { }); it('should emit event for custom error', () => { const error: any = CliParsingError; - service.sendCliCommandErrorEvent(instanceId, error); + service.sendCommandErrorEvent(instanceId, AppTool.CLI, error); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: CliParsingError.name, @@ -216,10 +248,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientCreationFailedEvent', () => { it('should emit CliConnectionError event', () => { - service.sendCliConnectionErrorEvent(instanceId, httpException, { data: 'Some data' }); + service.sendConnectionErrorEvent(instanceId, AppTool.CLI, httpException, { data: 'Some data' }); expect(sendFailedEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientConnectionError, + `CLI_${TelemetryEvents.ClientConnectionError}`, httpException, { databaseId: instanceId, @@ -228,10 +260,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliConnectionError event without additional data', () => { - service.sendCliConnectionErrorEvent(instanceId, httpException); + service.sendConnectionErrorEvent(instanceId, AppTool.CLI, httpException); expect(sendFailedEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientConnectionError, + `CLI_${TelemetryEvents.ClientConnectionError}`, httpException, { databaseId: instanceId, @@ -249,10 +281,10 @@ describe('CliAnalyticsService', () => { status: CommandExecutionStatus.Success, }; - service.sendCliClusterCommandExecutedEvent(instanceId, nodExecResult, { command: 'sadd' }); + service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult, { command: 'sadd' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClusterNodeCommandExecuted, + `CLI_${TelemetryEvents.ClusterNodeCommandExecuted}`, { databaseId: instanceId, command: 'sadd', @@ -268,10 +300,10 @@ describe('CliAnalyticsService', () => { status: CommandExecutionStatus.Fail, }; - service.sendCliClusterCommandExecutedEvent(instanceId, nodExecResult); + service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: redisReplyError.name, @@ -288,10 +320,10 @@ describe('CliAnalyticsService', () => { status: CommandExecutionStatus.Fail, }; - service.sendCliClusterCommandExecutedEvent(instanceId, nodExecResult); + service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: CliParsingError.name, @@ -305,7 +337,7 @@ describe('CliAnalyticsService', () => { port: 7002, status: 'undefined status', }; - service.sendCliClusterCommandExecutedEvent(instanceId, nodExecResult); + service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult); expect(sendEventMethod).not.toHaveBeenCalled(); }); diff --git a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts index a0c67651c9..a3d4ad00fd 100644 --- a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts @@ -2,7 +2,7 @@ import { HttpException, Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { TelemetryEvents } from 'src/constants'; import { TelemetryBaseService } from 'src/modules/shared/services/base/telemetry.base.service'; -import { ReplyError } from 'src/models'; +import { AppTool, ReplyError } from 'src/models'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { ICliExecResultFromNode } from 'src/modules/cli/services/cli-tool/cli-tool.service'; @@ -12,9 +12,13 @@ export class CliAnalyticsService extends TelemetryBaseService { super(eventEmitter); } - sendCliClientCreatedEvent(instanceId: string, additionalData: object = {}): void { + sendClientCreatedEvent( + instanceId: string, + namespace: string, + additionalData: object = {}, + ): void { this.sendEvent( - TelemetryEvents.CliClientCreated, + this.getNamespaceEvent(TelemetryEvents.ClientCreated, namespace), { databaseId: instanceId, ...additionalData, @@ -22,13 +26,14 @@ export class CliAnalyticsService extends TelemetryBaseService { ); } - sendCliClientCreationFailedEvent( + sendClientCreationFailedEvent( instanceId: string, + namespace: string, exception: HttpException, additionalData: object = {}, ): void { this.sendFailedEvent( - TelemetryEvents.CliClientCreationFailed, + this.getNamespaceEvent(TelemetryEvents.ClientCreationFailed, namespace), exception, { databaseId: instanceId, @@ -37,9 +42,13 @@ export class CliAnalyticsService extends TelemetryBaseService { ); } - sendCliClientRecreatedEvent(instanceId: string, additionalData: object = {}): void { + sendClientRecreatedEvent( + instanceId: string, + namespace: string, + additionalData: object = {}, + ): void { this.sendEvent( - TelemetryEvents.CliClientRecreated, + this.getNamespaceEvent(TelemetryEvents.ClientRecreated, namespace), { databaseId: instanceId, ...additionalData, @@ -47,15 +56,16 @@ export class CliAnalyticsService extends TelemetryBaseService { ); } - sendCliClientDeletedEvent( + sendClientDeletedEvent( affected: number, instanceId: string, + namespace: string, additionalData: object = {}, ): void { try { if (affected > 0) { this.sendEvent( - TelemetryEvents.CliClientDeleted, + this.getNamespaceEvent(TelemetryEvents.ClientDeleted, namespace), { databaseId: instanceId, ...additionalData, @@ -67,9 +77,13 @@ export class CliAnalyticsService extends TelemetryBaseService { } } - sendCliCommandExecutedEvent(instanceId: string, additionalData: object = {}): void { + sendCommandExecutedEvent( + instanceId: string, + namespace: string, + additionalData: object = {}, + ): void { this.sendEvent( - TelemetryEvents.CliCommandExecuted, + this.getNamespaceEvent(TelemetryEvents.CommandExecuted, namespace), { databaseId: instanceId, ...additionalData, @@ -77,8 +91,30 @@ export class CliAnalyticsService extends TelemetryBaseService { ); } - sendCliClusterCommandExecutedEvent( + sendCommandErrorEvent( instanceId: string, + namespace: string, + error: ReplyError, + additionalData: object = {}, + ): void { + try { + this.sendEvent( + this.getNamespaceEvent(TelemetryEvents.CommandErrorReceived, namespace), + { + databaseId: instanceId, + error: error?.name, + command: error?.command?.name, + ...additionalData, + }, + ); + } catch (e) { + // continue regardless of error + } + } + + sendClusterCommandExecutedEvent( + instanceId: string, + namespace: string, result: ICliExecResultFromNode, additionalData: object = {}, ): void { @@ -86,7 +122,7 @@ export class CliAnalyticsService extends TelemetryBaseService { try { if (status === CommandExecutionStatus.Success) { this.sendEvent( - TelemetryEvents.CliClusterNodeCommandExecuted, + this.getNamespaceEvent(TelemetryEvents.ClusterNodeCommandExecuted, namespace), { databaseId: instanceId, ...additionalData, @@ -95,7 +131,7 @@ export class CliAnalyticsService extends TelemetryBaseService { } if (status === CommandExecutionStatus.Fail) { this.sendEvent( - TelemetryEvents.CliCommandErrorReceived, + this.getNamespaceEvent(TelemetryEvents.CommandErrorReceived, namespace), { databaseId: instanceId, error: error.name, @@ -108,33 +144,14 @@ export class CliAnalyticsService extends TelemetryBaseService { } } - sendCliCommandErrorEvent( - instanceId: string, - error: ReplyError, - additionalData: object = {}, - ): void { - try { - this.sendEvent( - TelemetryEvents.CliCommandErrorReceived, - { - databaseId: instanceId, - error: error?.name, - command: error?.command?.name, - ...additionalData, - }, - ); - } catch (e) { - // continue regardless of error - } - } - - sendCliConnectionErrorEvent( + sendConnectionErrorEvent( instanceId: string, + namespace: string, exception: HttpException, additionalData: object = {}, ): void { this.sendFailedEvent( - TelemetryEvents.CliClientConnectionError, + this.getNamespaceEvent(TelemetryEvents.ClientConnectionError, namespace), exception, { databaseId: instanceId, @@ -142,4 +159,8 @@ export class CliAnalyticsService extends TelemetryBaseService { }, ); } + + private getNamespaceEvent(event: TelemetryEvents, namespace: string = AppTool.CLI): string { + return namespace.toLowerCase() === 'workbench' ? `WORKBENCH_${event}` : `CLI_${event}`; + } } diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts index 0d181597f2..7a816609d0 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts @@ -48,6 +48,7 @@ const mockRedisConsumer = () => ({ createNewToolClient: jest.fn(), reCreateToolClient: jest.fn(), deleteToolClient: jest.fn(), + getRedisClientNamespace: jest.fn(), }); const mockENotFoundMessage = 'ENOTFOUND some message'; diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts index 9eb1370c5e..297f217b4e 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts @@ -73,11 +73,11 @@ export class CliBusinessService { try { const uuid = await this.cliTool.createNewToolClient(instanceId, namespace); this.logger.log('Succeed to create Redis client for CLI.'); - this.cliAnalyticsService.sendCliClientCreatedEvent(instanceId); + this.cliAnalyticsService.sendClientCreatedEvent(instanceId, namespace); return { uuid }; } catch (error) { this.logger.error('Failed to create redis client for CLI.', error); - this.cliAnalyticsService.sendCliClientCreationFailedEvent(instanceId, error); + this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, namespace, error); throw error; } } @@ -86,23 +86,22 @@ export class CliBusinessService { * Method to close exist client and create a new one * @param instanceId * @param uuid + * @param namespace */ public async reCreateClient( instanceId: string, uuid: string, + namespace: string = AppTool.CLI, ): Promise { this.logger.log('re-create Redis client for CLI.'); try { - const clientUuid = await this.cliTool.reCreateToolClient( - instanceId, - uuid, - ); + const clientUuid = await this.cliTool.reCreateToolClient(instanceId, uuid, namespace); this.logger.log('Succeed to re-create Redis client for CLI.'); - this.cliAnalyticsService.sendCliClientRecreatedEvent(instanceId); + this.cliAnalyticsService.sendClientRecreatedEvent(instanceId, namespace); return { uuid: clientUuid }; } catch (error) { this.logger.error('Failed to re-create redis client for CLI.', error); - this.cliAnalyticsService.sendCliClientCreationFailedEvent(instanceId, error); + this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, namespace, error); throw error; } } @@ -118,9 +117,12 @@ export class CliBusinessService { ): Promise { this.logger.log('Deleting Redis client for CLI.'); try { + const namespace = this.cliTool.getRedisClientNamespace({ instanceId, uuid }); const affected = await this.cliTool.deleteToolClient(instanceId, uuid); this.logger.log('Succeed to delete Redis client for CLI.'); - this.cliAnalyticsService.sendCliClientDeletedEvent(affected, instanceId); + if (affected) { + this.cliAnalyticsService.sendClientDeletedEvent(affected, instanceId, namespace); + } return { affected }; } catch (error) { this.logger.error('Failed to delete Redis client for CLI.', error); @@ -139,6 +141,7 @@ export class CliBusinessService { ): Promise { this.logger.log('Executing redis CLI command.'); const { command: commandLine } = dto; + let namespace = AppTool.CLI.toString(); const outputFormat = dto.outputFormat || CliOutputFormatterTypes.Text; try { const formatter = this.outputFormatterManager.getStrategy(outputFormat); @@ -147,10 +150,12 @@ export class CliBusinessService { this.checkUnsupportedCommands(`${command} ${args[0]}`); const reply = await this.cliTool.execCommand(clientOptions, command, args, replyEncoding); + namespace = this.cliTool.getRedisClientNamespace(clientOptions); this.logger.log('Succeed to execute redis CLI command.'); - this.cliAnalyticsService.sendCliCommandExecutedEvent( + this.cliAnalyticsService.sendCommandExecutedEvent( clientOptions.instanceId, + namespace, { command, outputFormat, @@ -168,10 +173,10 @@ export class CliBusinessService { || error instanceof CliCommandNotSupportedError || error?.name === 'ReplyError' ) { - this.cliAnalyticsService.sendCliCommandErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, namespace, error); return { response: error.message, status: CommandExecutionStatus.Fail }; } - this.cliAnalyticsService.sendCliConnectionErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, namespace, error); if (error instanceof EncryptionServiceErrorException) { throw error; @@ -213,6 +218,7 @@ export class CliBusinessService { role: ClusterNodeRole, outputFormat: CliOutputFormatterTypes = CliOutputFormatterTypes.Text, ): Promise { + let namespace = AppTool.CLI.toString(); this.logger.log(`Executing redis.cluster CLI command for [${role}] nodes.`); try { const formatter = this.outputFormatterManager.getStrategy(outputFormat); @@ -226,9 +232,12 @@ export class CliBusinessService { role, replyEncoding, ); + + namespace = this.cliTool.getRedisClientNamespace(clientOptions); return result.map((nodeExecReply) => { - this.cliAnalyticsService.sendCliClusterCommandExecutedEvent( + this.cliAnalyticsService.sendClusterCommandExecutedEvent( clientOptions.instanceId, + namespace, nodeExecReply, { command, outputFormat }, ); @@ -245,13 +254,13 @@ export class CliBusinessService { this.logger.error('Failed to execute redis.cluster CLI command.', error); if (error instanceof CliParsingError || error instanceof CliCommandNotSupportedError) { - this.cliAnalyticsService.sendCliCommandErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, namespace, error); return [ { response: error.message, status: CommandExecutionStatus.Fail }, ]; } - this.cliAnalyticsService.sendCliConnectionErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, namespace, error); if (error instanceof EncryptionServiceErrorException) { throw error; @@ -300,8 +309,9 @@ export class CliBusinessService { } else { result.response = formatter.format(result.response); } - this.cliAnalyticsService.sendCliClusterCommandExecutedEvent( + this.cliAnalyticsService.sendClusterCommandExecutedEvent( clientOptions.instanceId, + 'cli', result, { command, outputFormat }, ); @@ -313,11 +323,11 @@ export class CliBusinessService { this.logger.error('Failed to execute redis.cluster CLI command.', error); if (error instanceof CliParsingError || error instanceof CliCommandNotSupportedError) { - this.cliAnalyticsService.sendCliCommandErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, 'cli', error); return { response: error.message, status: CommandExecutionStatus.Fail }; } - this.cliAnalyticsService.sendCliConnectionErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, 'cli', error); if (error instanceof EncryptionServiceErrorException) { throw error; diff --git a/redisinsight/api/src/modules/cli/services/cli-tool/cli-tool.service.ts b/redisinsight/api/src/modules/cli/services/cli-tool/cli-tool.service.ts index 6fb98ade4f..d3866d8f8b 100644 --- a/redisinsight/api/src/modules/cli/services/cli-tool/cli-tool.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-tool/cli-tool.service.ts @@ -159,13 +159,13 @@ export class CliToolService extends RedisConsumerAbstractService { return uuid; } - async reCreateToolClient(instanceId: string, uuid: string): Promise { + async reCreateToolClient(instanceId: string, uuid: string, namespace: string): Promise { this.redisService.removeClientInstance({ instanceId, uuid, tool: this.consumer, }); - await this.createNewClient(instanceId, uuid); + await this.createNewClient(instanceId, uuid, namespace); return uuid; } diff --git a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.spec.ts b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.spec.ts index 8e283cb7f8..c76fb431d8 100644 --- a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.spec.ts +++ b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.spec.ts @@ -13,6 +13,7 @@ import { import { InstancesBusinessService } from 'src/modules/shared/services/instances-business/instances-business.service'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { DatabaseInstanceEntity } from 'src/modules/core/models/database-instance.entity'; +import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; const mockClientOptions: IFindRedisClientInstanceByOptions = { instanceId: mockStandaloneDatabaseEntity.id, @@ -203,4 +204,33 @@ describe('RedisConsumerAbstractService', () => { ]); }); }); + + describe('getRedisClientNamespace', () => { + const mockClient = Object.create(Redis.prototype); + mockClient.options = { + ...mockClient.options, + connectionName: `${CONNECTION_NAME_GLOBAL_PREFIX}-common-235e72f4`, + }; + + it('succeed to get client namespace', async () => { + redisService.getClientInstance.mockReturnValue({ ...mockRedisClientInstance, client: mockClient }); + + const namespace = consumerInstance.getRedisClientNamespace({ + uuid: mockClient.uuid, + instanceId: mockClient.instanceId, + }); + + expect(namespace).toEqual('common'); + }); + it('failed to get client namespace', () => { + redisService.getClientInstance.mockReturnValue(undefined); + + const namespace = consumerInstance.getRedisClientNamespace({ + uuid: mockClient.uuid, + instanceId: mockClient.instanceId, + }); + + expect(namespace).toEqual(''); + }); + }); }); diff --git a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts index f1fd1892eb..f82b987795 100644 --- a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts +++ b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts @@ -1,7 +1,11 @@ import IORedis from 'ioredis'; import { v4 as uuidv4 } from 'uuid'; import { AppTool, ReplyError, IRedisConsumer } from 'src/models'; -import { catchRedisConnectionError, generateRedisConnectionName } from 'src/utils'; +import { + catchRedisConnectionError, + generateRedisConnectionName, + getConnectionNamespace, +} from 'src/utils'; import { IFindRedisClientInstanceByOptions, RedisService, @@ -116,6 +120,18 @@ export abstract class RedisConsumerAbstractService implements IRedisConsumer { return redisClientInstance.client; } + getRedisClientNamespace(options: IFindRedisClientInstanceByOptions): string { + try { + const clientInstance = this.redisService.getClientInstance({ + ...options, + tool: this.consumer, + }); + return clientInstance?.client ? getConnectionNamespace(clientInstance.client) : ''; + } catch (e) { + return ''; + } + } + protected async createNewClient( instanceId: string, uuid = uuidv4(), diff --git a/redisinsight/api/src/utils/redis-connection-helper.spec.ts b/redisinsight/api/src/utils/redis-connection-helper.spec.ts new file mode 100644 index 0000000000..aecf9aab1e --- /dev/null +++ b/redisinsight/api/src/utils/redis-connection-helper.spec.ts @@ -0,0 +1,65 @@ +import * as Redis from 'ioredis'; +import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; +import { + generateRedisConnectionName, + getConnectionName, + getConnectionNamespace, +} from './redis-connection-helper'; + +const CLIENT_ID = '235e72f4-601f-4d01-8399-b5c51b617dc4'; + +const mockClient = Object.create(Redis.prototype); +mockClient.options = { + ...mockClient.options, + host: 'localhost', + port: 6379, + connectionName: `${CONNECTION_NAME_GLOBAL_PREFIX}-common-235e72f4`, +}; + +const mockCluster = Object.create(Redis.Cluster.prototype); +mockCluster.options = { + redisOptions: mockClient.options, +}; + +const generateRedisConnectionNameTests = [ + { input: ['CLI', CLIENT_ID], expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-cli-235e72f4` }, + { input: ['CLI', CLIENT_ID, '_'], expected: `${CONNECTION_NAME_GLOBAL_PREFIX}_cli_235e72f4` }, + { input: ['workbench', CLIENT_ID], expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-workbench-235e72f4` }, + { input: ['Browser', CLIENT_ID], expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-browser-235e72f4` }, + { input: ['Browser', undefined], expected: CONNECTION_NAME_GLOBAL_PREFIX }, + { input: [], expected: CONNECTION_NAME_GLOBAL_PREFIX }, +]; + +describe('generateRedisConnectionName', () => { + test.each(generateRedisConnectionNameTests)('%j', ({ input, expected }) => { + // @ts-ignore + const result = generateRedisConnectionName(...input); + expect(result).toEqual(expected); + }); +}); + +const getConnectionNameTests = [ + { input: mockClient, expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-common-235e72f4` }, + { input: mockCluster, expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-common-235e72f4` }, + { input: undefined, expected: CONNECTION_NAME_GLOBAL_PREFIX }, +]; + +describe('getConnectionName', () => { + test.each(getConnectionNameTests)('%j', ({ input, expected }) => { + const result = getConnectionName(input); + expect(result).toEqual(expected); + }); +}); + +const getConnectionNamespaceTests = [ + { input: mockClient, expected: 'common' }, + { input: mockCluster, expected: 'common' }, + { input: undefined, expected: '' }, +]; + +describe('getConnectionNamespace', () => { + test.each(getConnectionNamespaceTests)('%j', ({ input, expected }) => { + const result = getConnectionNamespace(input); + expect(result).toEqual(expected); + }); +}); diff --git a/redisinsight/api/src/utils/redis-connection-helper.ts b/redisinsight/api/src/utils/redis-connection-helper.ts index e89bae0d3c..7b49d1c095 100644 --- a/redisinsight/api/src/utils/redis-connection-helper.ts +++ b/redisinsight/api/src/utils/redis-connection-helper.ts @@ -4,13 +4,13 @@ import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; export const generateRedisConnectionName = (namespace: string, id: string, separator = '-') => { try { - return [CONNECTION_NAME_GLOBAL_PREFIX, namespace, id?.substr(0, 8)].join(separator).toLowerCase(); + return [CONNECTION_NAME_GLOBAL_PREFIX, namespace, id.substr(0, 8)].join(separator).toLowerCase(); } catch (e) { return CONNECTION_NAME_GLOBAL_PREFIX; } }; -export const getConnectionName = (client: IORedis.Redis | IORedis.Cluster) => { +export const getConnectionName = (client: IORedis.Redis | IORedis.Cluster): string => { try { if (client instanceof IORedis.Cluster) { return get(client, 'options.redisOptions.connectionName', CONNECTION_NAME_GLOBAL_PREFIX); @@ -20,3 +20,12 @@ export const getConnectionName = (client: IORedis.Redis | IORedis.Cluster) => { return CONNECTION_NAME_GLOBAL_PREFIX; } }; + +export const getConnectionNamespace = (client: IORedis.Redis | IORedis.Cluster, separator = '-'): string => { + try { + const connectionName = getConnectionName(client); + return connectionName.split(separator)[1] || ''; + } catch (e) { + return ''; + } +}; diff --git a/redisinsight/ui/src/slices/workbench/wb-settings.ts b/redisinsight/ui/src/slices/workbench/wb-settings.ts index 9f0e13ac14..8587cf9314 100644 --- a/redisinsight/ui/src/slices/workbench/wb-settings.ts +++ b/redisinsight/ui/src/slices/workbench/wb-settings.ts @@ -98,7 +98,8 @@ export function updateWBClientAction( try { const { data, status } = await apiService.patch( - getUrl(instanceId, ApiEndpoints.CLI, uuid) + getUrl(instanceId, ApiEndpoints.CLI, uuid), + { namespace: 'workbench' }, ) if (isStatusSuccessful(status)) {