diff --git a/redisinsight/api/src/__mocks__/analytics.ts b/redisinsight/api/src/__mocks__/analytics.ts index c59c0dca53..8a3e4debc7 100644 --- a/redisinsight/api/src/__mocks__/analytics.ts +++ b/redisinsight/api/src/__mocks__/analytics.ts @@ -1,3 +1,14 @@ +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { BulkActionsAnalyticsService } from 'src/modules/bulk-actions/bulk-actions-analytics.service'; + +const mockEmitter = new EventEmitter2(); + +class AnalyticsService extends BulkActionsAnalyticsService { + constructor(protected eventEmitter: EventEmitter2) { + super(eventEmitter); + } +} + export const mockInstancesAnalyticsService = () => ({ sendInstanceListReceivedEvent: jest.fn(), sendInstanceAddedEvent: jest.fn(), @@ -29,6 +40,8 @@ export const mockSettingsAnalyticsService = () => ({ sendSettingsUpdatedEvent: jest.fn(), }); +export const mockBulActionsAnalyticsService = new AnalyticsService(mockEmitter); + export const mockPubSubAnalyticsService = () => ({ sendMessagePublishedEvent: jest.fn(), sendChannelSubscribeEvent: jest.fn(), diff --git a/redisinsight/api/src/constants/telemetry-events.ts b/redisinsight/api/src/constants/telemetry-events.ts index 786911b245..a85dbc5ac8 100644 --- a/redisinsight/api/src/constants/telemetry-events.ts +++ b/redisinsight/api/src/constants/telemetry-events.ts @@ -64,6 +64,8 @@ export enum TelemetryEvents { // Bulk Actions BulkActionsStarted = 'BULK_ACTIONS_STARTED', BulkActionsStopped = 'BULK_ACTIONS_STOPPED', + BulkActionsSucceed = 'BULK_ACTIONS_SUCCEED', + BulkActionsFailed = 'BULK_ACTIONS_FAILED', } export enum CommandType { diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-actions-analytics.service.ts b/redisinsight/api/src/modules/bulk-actions/bulk-actions-analytics.service.ts index a2b88fc890..210d087857 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-actions-analytics.service.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-actions-analytics.service.ts @@ -1,7 +1,8 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { TelemetryEvents } from 'src/constants'; import { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service'; +import { getRangeForNumber, BULK_ACTIONS_BREAKPOINTS } from 'src/utils'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { RedisError, ReplyError } from 'src/models'; import { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface'; @@ -20,6 +21,8 @@ export class BulkActionsAnalyticsService extends TelemetryBaseService { super(eventEmitter); this.events.set(TelemetryEvents.BulkActionsStarted, this.sendActionStarted.bind(this)); this.events.set(TelemetryEvents.BulkActionsStopped, this.sendActionStopped.bind(this)); + this.events.set(TelemetryEvents.BulkActionsSucceed, this.sendActionSucceed.bind(this)); + this.events.set(TelemetryEvents.BulkActionsFailed, this.sendActionFailed.bind(this)); } sendActionStarted(overview: IBulkActionOverview): void { @@ -28,7 +31,7 @@ export class BulkActionsAnalyticsService extends TelemetryBaseService { TelemetryEvents.BulkActionsStarted, { databaseId: overview.databaseId, - type: overview.type, + action: overview.type, duration: overview.duration, filter: { match: overview.filter?.match === '*' ? '*' : 'PATTERN', @@ -36,7 +39,9 @@ export class BulkActionsAnalyticsService extends TelemetryBaseService { }, progress: { scanned: overview.progress?.scanned, + scannedRange: getRangeForNumber(overview.progress?.scanned, BULK_ACTIONS_BREAKPOINTS), total: overview.progress?.total, + totalRange: getRangeForNumber(overview.progress?.total, BULK_ACTIONS_BREAKPOINTS), }, }, ); @@ -51,7 +56,7 @@ export class BulkActionsAnalyticsService extends TelemetryBaseService { TelemetryEvents.BulkActionsStopped, { databaseId: overview.databaseId, - type: overview.type, + action: overview.type, duration: overview.duration, filter: { match: overview.filter?.match === '*' ? '*' : 'PATTERN', @@ -59,12 +64,17 @@ export class BulkActionsAnalyticsService extends TelemetryBaseService { }, progress: { scanned: overview.progress?.scanned, + scannedRange: getRangeForNumber(overview.progress?.scanned, BULK_ACTIONS_BREAKPOINTS), total: overview.progress?.total, + totalRange: getRangeForNumber(overview.progress?.total, BULK_ACTIONS_BREAKPOINTS), }, summary: { processed: overview.summary?.processed, + processedRange: getRangeForNumber(overview.summary?.processed, BULK_ACTIONS_BREAKPOINTS), succeed: overview.summary?.succeed, + succeedRange: getRangeForNumber(overview.summary?.succeed, BULK_ACTIONS_BREAKPOINTS), failed: overview.summary.failed, + failedRange: getRangeForNumber(overview.summary.failed, BULK_ACTIONS_BREAKPOINTS), }, }, ); @@ -73,6 +83,48 @@ export class BulkActionsAnalyticsService extends TelemetryBaseService { } } + sendActionSucceed(overview: IBulkActionOverview): void { + try { + this.sendEvent( + TelemetryEvents.BulkActionsSucceed, + { + databaseId: overview.databaseId, + action: overview.type, + duration: overview.duration, + filter: { + match: overview.filter?.match === '*' ? '*' : 'PATTERN', + type: overview.filter?.type, + }, + summary: { + processed: overview.summary?.processed, + processedRange: getRangeForNumber(overview.summary?.processed, BULK_ACTIONS_BREAKPOINTS), + succeed: overview.summary?.succeed, + succeedRange: getRangeForNumber(overview.summary?.succeed, BULK_ACTIONS_BREAKPOINTS), + failed: overview.summary.failed, + failedRange: getRangeForNumber(overview.summary.failed, BULK_ACTIONS_BREAKPOINTS), + }, + }, + ); + } catch (e) { + // continue regardless of error + } + } + + sendActionFailed(overview: IBulkActionOverview, error: HttpException | Error): void { + try { + this.sendEvent( + TelemetryEvents.BulkActionsFailed, + { + databaseId: overview.databaseId, + action: overview.type, + error, + }, + ); + } catch (e) { + // continue regardless of error + } + } + getEventsEmitters(): Map { return this.events; } diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.spec.ts b/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.spec.ts index 5caf13c15f..7ed7e0fd0e 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.spec.ts @@ -3,6 +3,7 @@ import * as MockedSocket from 'socket.io-mock'; import { Test, TestingModule } from '@nestjs/testing'; import { MockType, + mockBulActionsAnalyticsService, } from 'src/__mocks__'; import { BulkActionsProvider } from 'src/modules/bulk-actions/providers/bulk-actions.provider'; import { RedisDataType } from 'src/modules/browser/dto'; @@ -43,6 +44,7 @@ const mockBulkAction = new BulkAction( mockCreateBulkActionDto.type, mockBulkActionFilter, mockSocket1, + mockBulActionsAnalyticsService, ); const mockOverview = 'mocked overview...'; @@ -51,6 +53,7 @@ mockBulkAction['getOverview'] = jest.fn().mockReturnValue(mockOverview); describe('BulkActionsService', () => { let service: BulkActionsService; let bulkActionProvider: MockType; + let analyticsService: MockType; beforeEach(async () => { jest.clearAllMocks(); @@ -72,6 +75,8 @@ describe('BulkActionsService', () => { useFactory: () => ({ sendActionStarted: jest.fn(), sendActionStopped: jest.fn(), + sendActionSucceed: jest.fn(), + sendActionFailed: jest.fn(), }), }, ], @@ -79,12 +84,14 @@ describe('BulkActionsService', () => { service = module.get(BulkActionsService); bulkActionProvider = module.get(BulkActionsProvider); + analyticsService = module.get(BulkActionsAnalyticsService); }); describe('create', () => { it('should create and return overview', async () => { expect(await service.create(mockCreateBulkActionDto, mockSocket1)).toEqual(mockOverview); expect(bulkActionProvider.create).toHaveBeenCalledTimes(1); + expect(analyticsService.sendActionStarted).toHaveBeenCalledTimes(1); }); }); describe('get', () => { diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.ts b/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.ts index 203eac7f67..d38086e44b 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.ts @@ -28,11 +28,8 @@ export class BulkActionsService { async abort(dto: BulkActionIdDto) { const bulkAction = await this.bulkActionsProvider.abort(dto.id); - const overview = bulkAction.getOverview(); - - this.analyticsService.sendActionStopped(overview); - return overview; + return bulkAction.getOverview(); } disconnect(socketId: string) { diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts index 48b56afc8a..10bfbd4784 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts @@ -16,6 +16,7 @@ import { BulkActionsAnalyticsService } from 'src/modules/bulk-actions/bulk-actio import * as fs from 'fs-extra'; import config from 'src/utils/config'; import { join } from 'path'; +import { wrapHttpError } from 'src/common/utils'; const PATH_CONFIG = config.get('dir_path'); @@ -34,6 +35,13 @@ const mockSummary: BulkActionSummary = Object.assign(new BulkActionSummary(), { errors: [], }); +const mockEmptySummary: BulkActionSummary = Object.assign(new BulkActionSummary(), { + processed: 0, + succeed: 0, + failed: 0, + errors: [], +}); + const mockSummaryWithErrors = Object.assign(new BulkActionSummary(), { processed: 100, succeed: 99, @@ -44,7 +52,7 @@ const mockSummaryWithErrors = Object.assign(new BulkActionSummary(), { const mockImportResult: IBulkActionOverview = { id: 'empty', databaseId: mockClientMetadata.databaseId, - type: BulkActionType.Import, + type: BulkActionType.Upload, summary: mockSummary.getOverview(), progress: null, filter: null, @@ -52,6 +60,17 @@ const mockImportResult: IBulkActionOverview = { duration: 100, }; +const mockEmptyImportResult: IBulkActionOverview = { + id: 'empty', + databaseId: mockClientMetadata.databaseId, + type: BulkActionType.Upload, + summary: mockEmptySummary.getOverview(), + progress: null, + filter: null, + status: BulkActionStatus.Completed, + duration: 0, +}; + const mockUploadImportFileDto = { file: { originalname: 'filename', @@ -88,6 +107,8 @@ describe('BulkImportService', () => { useFactory: () => ({ sendActionStarted: jest.fn(), sendActionStopped: jest.fn(), + sendActionSucceed: jest.fn(), + sendActionFailed: jest.fn(), }), }, ], @@ -135,7 +156,7 @@ describe('BulkImportService', () => { ...mockImportResult, duration: jasmine.anything(), }); - expect(analytics.sendActionStopped).toHaveBeenCalledWith({ + expect(analytics.sendActionSucceed).toHaveBeenCalledWith({ ...mockImportResult, duration: jasmine.anything(), }); @@ -220,6 +241,10 @@ describe('BulkImportService', () => { fail(); } catch (e) { expect(mockIORedisClient.disconnect).not.toHaveBeenCalled(); + expect(analytics.sendActionFailed).toHaveBeenCalledWith( + { ...mockEmptyImportResult }, + wrapHttpError(e), + ); expect(e).toBeInstanceOf(NotFoundException); } }); diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts index 039a6cbf59..a71b6d5203 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts @@ -1,4 +1,4 @@ -import { join, resolve } from 'path'; +import { join } from 'path'; import * as fs from 'fs-extra'; import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { Readable } from 'stream'; @@ -70,7 +70,7 @@ export class BulkImportService { const result: IBulkActionOverview = { id: 'empty', databaseId: clientMetadata.databaseId, - type: BulkActionType.Import, + type: BulkActionType.Upload, summary: { processed: 0, succeed: 0, @@ -115,6 +115,7 @@ export class BulkImportService { rl.on('error', (error) => { result.summary.errors.push(error); result.status = BulkActionStatus.Failed; + this.analyticsService.sendActionFailed(result, error); res(null); }); rl.on('close', () => { @@ -133,15 +134,19 @@ export class BulkImportService { result.summary.processed += parseErrors; result.summary.failed += parseErrors; - this.analyticsService.sendActionStopped(result); + if (result.status === BulkActionStatus.Completed) { + this.analyticsService.sendActionSucceed(result); + } client.disconnect(); return result; } catch (e) { this.logger.error('Unable to process an import file', e); + const exception = wrapHttpError(e); + this.analyticsService.sendActionFailed(result, exception); client?.disconnect(); - throw wrapHttpError(e); + throw exception; } } diff --git a/redisinsight/api/src/modules/bulk-actions/constants/index.ts b/redisinsight/api/src/modules/bulk-actions/constants/index.ts index 0ff821b918..a9e3c937ef 100644 --- a/redisinsight/api/src/modules/bulk-actions/constants/index.ts +++ b/redisinsight/api/src/modules/bulk-actions/constants/index.ts @@ -6,7 +6,7 @@ export enum BulkActionsServerEvents { export enum BulkActionType { Delete = 'delete', - Import = 'import', + Upload = 'upload', } export enum BulkActionStatus { diff --git a/redisinsight/api/src/modules/bulk-actions/models/bulk-action.spec.ts b/redisinsight/api/src/modules/bulk-actions/models/bulk-action.spec.ts index 728bf286d7..c72649955c 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/bulk-action.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/bulk-action.spec.ts @@ -2,6 +2,7 @@ import IORedis from 'ioredis'; import { omit } from 'lodash'; import { mockSocket, + mockBulActionsAnalyticsService, } from 'src/__mocks__'; import { DeleteBulkActionSimpleRunner, @@ -34,6 +35,23 @@ const mockCreateBulkActionDto = { type: BulkActionType.Delete, }; +const mockOverview = { + ...mockCreateBulkActionDto, + duration: 0, + filter: { match: '*', type: null }, + progress: { + scanned: 0, + total: 0, + }, + status: 'completed', + summary: { + failed: 0, + processed: 0, + succeed: 0, + errors: [], + }, +}; + let bulkAction; let mockRunner; let mockSummary; @@ -85,6 +103,7 @@ describe('AbstractBulkActionSimpleRunner', () => { mockCreateBulkActionDto.type, mockBulkActionFilter, mockSocket, + mockBulActionsAnalyticsService, ); }); @@ -303,8 +322,14 @@ describe('AbstractBulkActionSimpleRunner', () => { }); describe('sendOverview', () => { let sendOverviewSpy; + let sendActionSucceedSpy; + let sendActionFailedSpy; + let sendActionStoppedSpy; beforeEach(() => { + sendActionSucceedSpy = jest.spyOn(bulkAction['analyticsService'], 'sendActionSucceed'); + sendActionFailedSpy = jest.spyOn(bulkAction['analyticsService'], 'sendActionFailed'); + sendActionStoppedSpy = jest.spyOn(bulkAction['analyticsService'], 'sendActionStopped'); sendOverviewSpy = jest.spyOn(bulkAction, 'sendOverview'); }); @@ -320,6 +345,57 @@ describe('AbstractBulkActionSimpleRunner', () => { expect(sendOverviewSpy).toHaveBeenCalledTimes(1); }); + + it('Should call sendActionSucceed', () => { + mockSocket.emit.mockReturnValue(); + + bulkAction['status'] = BulkActionStatus.Completed; + + bulkAction.sendOverview(); + + expect(sendOverviewSpy).toHaveBeenCalledTimes(1); + expect(sendActionFailedSpy).not.toHaveBeenCalled(); + expect(sendActionStoppedSpy).not.toHaveBeenCalled(); + expect(sendActionSucceedSpy).toHaveBeenCalledWith(mockOverview); + }); + + it('Should call sendActionFailed', () => { + mockSocket.emit.mockReturnValue(); + + bulkAction['status'] = BulkActionStatus.Failed; + bulkAction['error'] = 'some error'; + + bulkAction.sendOverview(); + + expect(sendOverviewSpy).toHaveBeenCalledTimes(1); + expect(sendActionSucceedSpy).not.toHaveBeenCalled(); + expect(sendActionStoppedSpy).not.toHaveBeenCalled(); + expect(sendActionFailedSpy).toHaveBeenCalledWith( + { + ...mockOverview, + status: 'failed', + }, + 'some error', + ); + }); + + it('Should call sendActionStopped', () => { + mockSocket.emit.mockReturnValue(); + + bulkAction['status'] = BulkActionStatus.Aborted; + + bulkAction.sendOverview(); + + expect(sendOverviewSpy).toHaveBeenCalledTimes(1); + expect(sendActionSucceedSpy).not.toHaveBeenCalled(); + expect(sendActionFailedSpy).not.toHaveBeenCalled(); + expect(sendActionStoppedSpy).toHaveBeenCalledWith( + { + ...mockOverview, + status: 'aborted', + }, + ); + }); }); describe('Other', () => { it('getters', () => { diff --git a/redisinsight/api/src/modules/bulk-actions/models/bulk-action.ts b/redisinsight/api/src/modules/bulk-actions/models/bulk-action.ts index 0d36d0c109..f333307cf0 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/bulk-action.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/bulk-action.ts @@ -6,6 +6,7 @@ import { Socket } from 'socket.io'; import { Logger } from '@nestjs/common'; import { IBulkAction, IBulkActionRunner } from 'src/modules/bulk-actions/interfaces'; import { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface'; +import { BulkActionsAnalyticsService } from 'src/modules/bulk-actions/bulk-actions-analytics.service'; export class BulkAction implements IBulkAction { private logger: Logger = new Logger('BulkAction'); @@ -18,6 +19,8 @@ export class BulkAction implements IBulkAction { private endTime: number; + private error: Error; + private readonly socket: Socket; private readonly type: BulkActionType; @@ -30,7 +33,16 @@ export class BulkAction implements IBulkAction { private readonly debounce: Function; - constructor(id, databaseId, type, filter, socket) { + private readonly analyticsService: BulkActionsAnalyticsService; + + constructor( + id, + databaseId, + type, + filter, + socket, + analyticsService: BulkActionsAnalyticsService, + ) { this.id = id; this.databaseId = databaseId; this.type = type; @@ -38,6 +50,7 @@ export class BulkAction implements IBulkAction { this.socket = socket; this.debounce = debounce(this.sendOverview.bind(this), 1000, { maxWait: 1000 }); this.status = BulkActionStatus.Initialized; + this.analyticsService = analyticsService; } /** @@ -97,6 +110,7 @@ export class BulkAction implements IBulkAction { this.setStatus(BulkActionStatus.Completed); } catch (e) { this.logger.error('Error on BulkAction Runner', e); + this.error = e; this.setStatus(BulkActionStatus.Failed); } } @@ -194,7 +208,15 @@ export class BulkAction implements IBulkAction { */ sendOverview() { const overview = this.getOverview(); - + if (overview.status === BulkActionStatus.Completed) { + this.analyticsService.sendActionSucceed(overview); + } + if (overview.status === BulkActionStatus.Failed) { + this.analyticsService.sendActionFailed(overview, this.error); + } + if (overview.status === BulkActionStatus.Aborted) { + this.analyticsService.sendActionStopped(overview); + } try { this.socket.emit('overview', overview); } catch (e) { diff --git a/redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.spec.ts b/redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.spec.ts index 01f2825a48..ef45509653 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.spec.ts @@ -1,6 +1,7 @@ import IORedis from 'ioredis'; import { mockSocket, + mockBulActionsAnalyticsService, } from 'src/__mocks__'; import { DeleteBulkActionSimpleRunner, @@ -38,6 +39,7 @@ describe('AbstractBulkActionRunner', () => { mockCreateBulkActionDto.type, mockBulkActionFilter, mockSocket, + mockBulActionsAnalyticsService, ); deleteRunner = new DeleteBulkActionSimpleRunner(bulkAction, nodeClient); diff --git a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.spec.ts b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.spec.ts index 3d5bd2d41b..b6f9e30603 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.spec.ts @@ -1,6 +1,7 @@ import IORedis from 'ioredis'; import { mockSocket, + mockBulActionsAnalyticsService, } from 'src/__mocks__'; import { DeleteBulkActionSimpleRunner, @@ -48,6 +49,7 @@ describe('AbstractBulkActionSimpleRunner', () => { mockCreateBulkActionDto.type, mockBulkActionFilter, mockSocket, + mockBulActionsAnalyticsService, ); deleteRunner = new DeleteBulkActionSimpleRunner(bulkAction, nodeClient); diff --git a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.spec.ts b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.spec.ts index 22bb4c79cc..b5e8cc90f0 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.spec.ts @@ -1,6 +1,7 @@ import IORedis from 'ioredis'; import { mockSocket, + mockBulActionsAnalyticsService, } from 'src/__mocks__'; import { DeleteBulkActionSimpleRunner, @@ -29,6 +30,7 @@ const bulkAction = new BulkAction( mockCreateBulkActionDto.type, mockBulkActionFilter, mockSocket, + mockBulActionsAnalyticsService, ); const mockKey = 'mockedKey'; diff --git a/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.spec.ts b/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.spec.ts index 533a572598..80ae41bedd 100644 --- a/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.spec.ts @@ -3,7 +3,7 @@ import * as MockedSocket from 'socket.io-mock'; import { Test, TestingModule } from '@nestjs/testing'; import { mockDatabaseConnectionService, - MockType + MockType, } from 'src/__mocks__'; import { BulkActionsProvider } from 'src/modules/bulk-actions/providers/bulk-actions.provider'; import { RedisService } from 'src/modules/redis/redis.service'; @@ -14,6 +14,7 @@ import { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-fi import { BulkAction } from 'src/modules/bulk-actions/models/bulk-action'; import { NotFoundException } from '@nestjs/common'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { BulkActionsAnalyticsService } from 'src/modules/bulk-actions/bulk-actions-analytics.service'; export const mockSocket1 = new MockedSocket(); mockSocket1.id = '1'; @@ -59,6 +60,15 @@ describe('BulkActionsProvider', () => { provide: DatabaseConnectionService, useFactory: mockDatabaseConnectionService, }, + { + provide: BulkActionsAnalyticsService, + useFactory: () => ({ + sendActionStarted: jest.fn(), + sendActionStopped: jest.fn(), + sendActionSucceed: jest.fn(), + sendActionFailed: jest.fn(), + }), + }, ], }).compile(); diff --git a/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.ts b/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.ts index 4ee9c03b1b..07ced3016a 100644 --- a/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.ts +++ b/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.ts @@ -7,6 +7,7 @@ import { Socket } from 'socket.io'; import { BulkActionStatus, BulkActionType } from 'src/modules/bulk-actions/constants'; import { DeleteBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { BulkActionsAnalyticsService } from 'src/modules/bulk-actions/bulk-actions-analytics.service'; import { ClientContext } from 'src/common/models'; @Injectable() @@ -17,6 +18,7 @@ export class BulkActionsProvider { constructor( private readonly databaseConnectionService: DatabaseConnectionService, + private readonly analyticsService: BulkActionsAnalyticsService, ) {} /** @@ -29,7 +31,7 @@ export class BulkActionsProvider { throw new Error('You already have bulk action with such id'); } - const bulkAction = new BulkAction(dto.id, dto.databaseId, dto.type, dto.filter, socket); + const bulkAction = new BulkAction(dto.id, dto.databaseId, dto.type, dto.filter, socket, this.analyticsService); this.bulkActions.set(dto.id, bulkAction); diff --git a/redisinsight/api/src/utils/analytics-helper.ts b/redisinsight/api/src/utils/analytics-helper.ts index 4ca7a8ba20..5b083b5545 100644 --- a/redisinsight/api/src/utils/analytics-helper.ts +++ b/redisinsight/api/src/utils/analytics-helper.ts @@ -17,6 +17,14 @@ export const SCAN_THRESHOLD_BREAKPOINTS = [ 1000000, ]; +export const BULK_ACTIONS_BREAKPOINTS = [ + 5000, + 10000, + 50000, + 100000, + 1000000, +]; + const numberWithSpaces = (x: number): string => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); export const getRangeForNumber = ( diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.spec.tsx b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.spec.tsx index a3df4f08fc..2a8803d3e2 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.spec.tsx @@ -139,8 +139,10 @@ describe('BulkActions', () => { event: TelemetryEvent.BULK_ACTIONS_OPENED, eventData: { databaseId: 'instanceId', - match: '*', - filterType: 'hash', + filter: { + match: '*', + filter: 'hash', + }, action: 'delete' } }); @@ -154,8 +156,10 @@ describe('BulkActions', () => { eventData: { databaseId: 'instanceId', action: BulkActionsType.Delete, - match: '*', - filterType: 'hash' + filter: { + match: '*', + filterType: 'hash' + } } }) }) diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.tsx b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.tsx index 9d74631930..1bca73a827 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.tsx +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.tsx @@ -46,8 +46,10 @@ const BulkActions = (props: Props) => { event: TelemetryEvent.BULK_ACTIONS_OPENED, eventData: { databaseId: instanceId, - filterType: filter, - match: (search && search !== DEFAULT_SEARCH_MATCH) ? getMatchType(search) : DEFAULT_SEARCH_MATCH, + filter: { + filter, + match: (search && search !== DEFAULT_SEARCH_MATCH) ? getMatchType(search) : DEFAULT_SEARCH_MATCH, + }, action: type } }) @@ -69,8 +71,10 @@ const BulkActions = (props: Props) => { } if (type === BulkActionsType.Delete) { - eventData.match = (search && search !== DEFAULT_SEARCH_MATCH) ? getMatchType(search) : DEFAULT_SEARCH_MATCH - eventData.filterType = filter + eventData.filter = { + match: (search && search !== DEFAULT_SEARCH_MATCH) ? getMatchType(search) : DEFAULT_SEARCH_MATCH, + type: filter, + } } sendEventTelemetry({ diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.spec.tsx b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.spec.tsx index b35b9114b8..f30fdc1532 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.spec.tsx @@ -60,8 +60,10 @@ describe('BulkActionsTabs', () => { eventData: { databaseId: '', action: BulkActionsType.Delete, - match: 'PATTERN', - filterType: 'set' + filter: { + match: 'PATTERN', + filter: 'set' + } } }); diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.tsx b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.tsx index 0096ee39fa..d7f3165417 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.tsx +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.tsx @@ -30,8 +30,10 @@ const BulkActionsTabs = (props: Props) => { } if (id === BulkActionsType.Delete) { - eventData.match = (search && search !== DEFAULT_SEARCH_MATCH) ? getMatchType(search) : DEFAULT_SEARCH_MATCH - eventData.filterType = filter + eventData.filter = { + match: (search && search !== DEFAULT_SEARCH_MATCH) ? getMatchType(search) : DEFAULT_SEARCH_MATCH, + type: filter, + } } sendEventTelemetry({ diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteFooter/BulkDeleteFooter.tsx b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteFooter/BulkDeleteFooter.tsx index 73f1948af6..26a4177883 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteFooter/BulkDeleteFooter.tsx +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteFooter/BulkDeleteFooter.tsx @@ -13,6 +13,7 @@ import { import { keysDataSelector, keysSelector } from 'uiSrc/slices/browser/keys' import { getMatchType, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { BulkActionsType } from 'uiSrc/constants' +import { getRangeForNumber, BULK_THRESHOLD_BREAKPOINTS } from 'uiSrc/utils' import { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api' import BulkDeleteContent from '../BulkDeleteContent' @@ -52,10 +53,16 @@ const BulkDeleteFooter = (props: Props) => { sendEventTelemetry({ event: TelemetryEvent.BULK_ACTIONS_WARNING, eventData: { - filterType: filter, - match: matchValue, - scanned, - total, + filter: { + match: matchValue, + type: filter, + }, + progress: { + scanned, + scannedRange: getRangeForNumber(scanned, BULK_THRESHOLD_BREAKPOINTS), + total, + totalRange: getRangeForNumber(total, BULK_THRESHOLD_BREAKPOINTS), + }, databaseId: instanceId, action: BulkActionsType.Delete } diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.spec.tsx b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.spec.tsx index 2620dab7a7..1b727e61b5 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.spec.tsx @@ -134,18 +134,6 @@ describe('BulkUpload', () => { action: BulkActionsType.Upload, databaseId: '' } - }); - - (sendEventTelemetry as jest.Mock).mockRestore() - - fireEvent.click(screen.getByTestId('bulk-action-apply-btn')) - - expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.BULK_ACTIONS_STARTED, - eventData: { - action: BulkActionsType.Upload, - databaseId: '' - } }) }) }) diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.tsx b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.tsx index 1295143f96..4c9e092949 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.tsx +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.tsx @@ -85,14 +85,6 @@ const BulkUpload = (props: Props) => { const formData = new FormData() formData.append('file', files[0]) dispatch(bulkUploadDataAction(instanceId, { file: formData, fileName: files[0].name })) - - sendEventTelemetry({ - event: TelemetryEvent.BULK_ACTIONS_STARTED, - eventData: { - databaseId: instanceId, - action: BulkActionsType.Upload - } - }) } } diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index fba2513221..4770548628 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -199,7 +199,6 @@ export enum TelemetryEvent { BULK_ACTIONS_OPENED = 'BULK_ACTIONS_OPENED', BULK_ACTIONS_WARNING = 'BULK_ACTIONS_WARNING', BULK_ACTIONS_CANCELLED = 'BULK_ACTIONS_CANCELLED', - BULK_ACTIONS_STARTED = 'BULK_ACTIONS_STARTED', DATABASE_ANALYSIS_STARTED = 'DATABASE_ANALYSIS_STARTED', DATABASE_ANALYSIS_HISTORY_VIEWED = 'DATABASE_ANALYSIS_HISTORY_VIEWED', diff --git a/redisinsight/ui/src/utils/index.ts b/redisinsight/ui/src/utils/index.ts index 198f518023..7fe439e540 100644 --- a/redisinsight/ui/src/utils/index.ts +++ b/redisinsight/ui/src/utils/index.ts @@ -26,6 +26,7 @@ export * from './groupTypes' export * from './modules' export * from './optimizations' export * from './events' +export * from './telemetry' export { Maybe, diff --git a/redisinsight/ui/src/utils/telemetry.ts b/redisinsight/ui/src/utils/telemetry.ts new file mode 100644 index 0000000000..1174a0609c --- /dev/null +++ b/redisinsight/ui/src/utils/telemetry.ts @@ -0,0 +1,33 @@ +import { isNil } from 'lodash' +import { numberWithSpaces } from 'uiSrc/utils/numbers' +import { Maybe } from 'uiSrc/utils' + +export const BULK_THRESHOLD_BREAKPOINTS = [ + 5000, + 10000, + 50000, + 100000, + 1000000, +] + +export const getRangeForNumber = ( + value: Maybe, + breakpoints: number[] = BULK_THRESHOLD_BREAKPOINTS, +): Maybe => { + if (isNil(value)) { + return undefined + } + const index = breakpoints.findIndex( + (threshold: number) => value <= threshold, + ) + if (index === 0) { + return `0 - ${numberWithSpaces(breakpoints[0])}` + } + if (index === -1) { + const lastItem = breakpoints[breakpoints.length - 1] + return `${numberWithSpaces(lastItem + 1)} +` + } + return `${numberWithSpaces( + breakpoints[index - 1] + 1, + )} - ${numberWithSpaces(breakpoints[index])}` +} diff --git a/redisinsight/ui/src/utils/tests/telemetry.spec.ts b/redisinsight/ui/src/utils/tests/telemetry.spec.ts new file mode 100644 index 0000000000..7530a0990b --- /dev/null +++ b/redisinsight/ui/src/utils/tests/telemetry.spec.ts @@ -0,0 +1,64 @@ +import { getRangeForNumber, BULK_THRESHOLD_BREAKPOINTS } from 'uiSrc/utils' + +const testCases = [ + { + number: undefined, + result: undefined, + }, + { + number: 0, + result: '0 - 5 000', + }, + { + number: 10, + result: '0 - 5 000', + }, + { + number: 5_000, + result: '0 - 5 000', + }, + { + number: 5_001, + result: '5 001 - 10 000', + }, + { + number: 7_050, + result: '5 001 - 10 000', + }, + { + number: 10_000, + result: '5 001 - 10 000', + }, + { + number: 10_001, + result: '10 001 - 50 000', + }, + { + number: 50_000, + result: '10 001 - 50 000', + }, + { + number: 50_001, + result: '50 001 - 100 000', + }, + { + number: 100_000, + result: '50 001 - 100 000', + }, + { + number: 1_000_000, + result: '100 001 - 1 000 000', + }, + { + number: 1_000_001, + result: '1 000 001 +', + }, +] +describe('getRangeForNumber', () => { + testCases.forEach((tc) => { + it(`should return ${tc.result} for number:${tc.number}`, () => { + const range = getRangeForNumber(tc.number, BULK_THRESHOLD_BREAKPOINTS) + expect(range).toEqual(tc.result) + }) + }) +})