diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index e65bb362df..f02e3163dc 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -23,7 +23,7 @@ "start": "nest start", "start:dev": "cross-env NODE_ENV=development BUILD_TYPE=DOCKER_ON_PREMISE SERVER_STATIC_CONTENT=1 nest start --watch", "start:debug": "nest start --debug --watch", - "start:stage": "cross-env NODE_ENV=staging SERVER_STATIC_CONTENT=true node dist/src/main", + "start:stage": "cross-env NODE_ENV=staging BUILD_TYPE=DOCKER_ON_PREMISE SERVER_STATIC_CONTENT=true node dist/src/main", "start:prod": "cross-env NODE_ENV=production node dist/src/main", "test": "cross-env NODE_ENV=test ./node_modules/.bin/jest -w 1", "test:watch": "cross-env NODE_ENV=test jest --watch -w 1", diff --git a/redisinsight/api/src/constants/telemetry-events.ts b/redisinsight/api/src/constants/telemetry-events.ts index 9d39221de1..88c3983d01 100644 --- a/redisinsight/api/src/constants/telemetry-events.ts +++ b/redisinsight/api/src/constants/telemetry-events.ts @@ -72,6 +72,9 @@ export enum TelemetryEvents { FeatureFlagConfigUpdateError = 'FEATURE_FLAG_CONFIG_UPDATE_ERROR', FeatureFlagInvalidRemoteConfig = 'FEATURE_FLAG_INVALID_REMOTE_CONFIG', FeatureFlagRecalculated = 'FEATURE_FLAG_RECALCULATED', + + // Insights + InsightsRecommendationGenerated = 'INSIGHTS_RECOMMENDATION_GENERATED', } export enum CommandType { diff --git a/redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.spec.ts b/redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.spec.ts new file mode 100644 index 0000000000..48afe0d8d7 --- /dev/null +++ b/redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.spec.ts @@ -0,0 +1,48 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + mockDatabase, + mockDatabaseRecommendation, + mockDatabaseWithTlsAuth, +} from 'src/__mocks__'; +import { TelemetryEvents } from 'src/constants'; +import { DatabaseRecommendationAnalytics } from './database-recommendation.analytics'; + +const provider = 'cloud'; + +describe('DatabaseRecommendationAnalytics', () => { + let service: DatabaseRecommendationAnalytics; + let sendEventSpy; + let sendFailedEventSpy; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + EventEmitter2, + DatabaseRecommendationAnalytics, + ], + }).compile(); + + service = await module.get(DatabaseRecommendationAnalytics); + sendEventSpy = jest.spyOn(service as any, 'sendEvent'); + sendFailedEventSpy = jest.spyOn(service as any, 'sendFailedEvent'); + }); + + describe('sendInstanceAddedEvent', () => { + it('should emit event with recommendationName and provider', () => { + service.sendCreatedRecommendationEvent( + mockDatabaseRecommendation, + mockDatabaseWithTlsAuth, + ); + + expect(sendEventSpy).toHaveBeenCalledWith( + TelemetryEvents.InsightsRecommendationGenerated, + { + recommendationName: mockDatabaseRecommendation.name, + databaseId: mockDatabase.id, + provider: mockDatabaseWithTlsAuth.provider, + }, + ); + }); + }); +}); diff --git a/redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.ts b/redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.ts new file mode 100644 index 0000000000..961ca2ec88 --- /dev/null +++ b/redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service'; +import { TelemetryEvents } from 'src/constants'; +import { DatabaseRecommendation } from './models'; +import { Database } from '../database/models/database'; + +@Injectable() +export class DatabaseRecommendationAnalytics extends TelemetryBaseService { + constructor(protected eventEmitter: EventEmitter2) { + super(eventEmitter); + } + + sendCreatedRecommendationEvent(recommendation: DatabaseRecommendation, database: Database): void { + try { + this.sendEvent( + TelemetryEvents.InsightsRecommendationGenerated, + { + recommendationName: recommendation.name, + databaseId: database.id, + provider: database.provider, + }, + ); + } catch (e) { + // ignore + } + } +} diff --git a/redisinsight/api/src/modules/database-recommendation/database-recommendation.module.ts b/redisinsight/api/src/modules/database-recommendation/database-recommendation.module.ts index 5894183d20..b8a26bb254 100644 --- a/redisinsight/api/src/modules/database-recommendation/database-recommendation.module.ts +++ b/redisinsight/api/src/modules/database-recommendation/database-recommendation.module.ts @@ -10,6 +10,7 @@ import { DatabaseRecommendationGateway } from 'src/modules/database-recommendati import { DatabaseRecommendationEmitter, } from 'src/modules/database-recommendation/providers/database-recommendation.emitter'; +import { DatabaseRecommendationAnalytics } from 'src/modules/database-recommendation/database-recommendation.analytics'; @Module({}) export class DatabaseRecommendationModule { @@ -25,6 +26,7 @@ export class DatabaseRecommendationModule { RecommendationProvider, DatabaseRecommendationGateway, DatabaseRecommendationEmitter, + DatabaseRecommendationAnalytics, { provide: DatabaseRecommendationRepository, useClass: databaseRecommendationRepository, diff --git a/redisinsight/api/src/modules/database-recommendation/database-recommendation.service.ts b/redisinsight/api/src/modules/database-recommendation/database-recommendation.service.ts index 6358062fed..7693b91203 100644 --- a/redisinsight/api/src/modules/database-recommendation/database-recommendation.service.ts +++ b/redisinsight/api/src/modules/database-recommendation/database-recommendation.service.ts @@ -11,6 +11,7 @@ import { } from 'src/modules/database-recommendation/dto/database-recommendations.response'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { ModifyDatabaseRecommendationDto, DeleteDatabaseRecommendationResponse } from './dto'; +import { DatabaseRecommendationAnalytics } from './database-recommendation.analytics'; import { DatabaseService } from '../database/database.service'; @Injectable() @@ -21,19 +22,22 @@ export class DatabaseRecommendationService { private readonly databaseRecommendationRepository: DatabaseRecommendationRepository, private readonly scanner: RecommendationScanner, private readonly databaseService: DatabaseService, + private readonly analytics: DatabaseRecommendationAnalytics, ) {} /** * Create recommendation entity * @param clientMetadata - * @param recommendationName + * @param entity */ - public async create(clientMetadata: ClientMetadata, recommendationName: string): Promise { - const entity = plainToClass( - DatabaseRecommendation, - { databaseId: clientMetadata?.databaseId, name: recommendationName }, - ); - return this.databaseRecommendationRepository.create(entity); + public async create(clientMetadata: ClientMetadata, entity: DatabaseRecommendation): Promise { + const recommendation = await this.databaseRecommendationRepository.create(entity); + + const database = await this.databaseService.get(clientMetadata?.databaseId); + + this.analytics.sendCreatedRecommendationEvent(recommendation, database); + + return recommendation; } /** @@ -74,7 +78,7 @@ export class DatabaseRecommendationService { { databaseId: newClientMetadata?.databaseId, ...recommendation }, ); - return await this.databaseRecommendationRepository.create(entity); + return await this.create(newClientMetadata, entity); } } diff --git a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx index bbed498995..e91ac53084 100644 --- a/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx +++ b/redisinsight/ui/src/components/live-time-recommendations/components/recommendation/Recommendation.tsx @@ -135,12 +135,27 @@ const Recommendation = ({ setIsLoading(false) } + const onRecommendationLinkClick = () => { + sendEventTelemetry({ + event: TelemetryEvent.INSIGHTS_RECOMMENDATION_LINK_CLICKED, + eventData: { + databaseId: instanceId, + name: recommendationsContent[name]?.telemetryEvent ?? name, + provider + } + }) + setIsLoading(false) + } + const recommendationContent = () => ( {renderRecommendationContent( recommendationsContent[name]?.content, params, - recommendationsContent[name]?.telemetryEvent ?? name, + { + onClickLink: onRecommendationLinkClick, + telemetryName: recommendationsContent[name]?.telemetryEvent ?? name, + }, true )} {!!params?.keys?.length && ( diff --git a/redisinsight/ui/src/components/recommendation-voting/RecommendationVoting.tsx b/redisinsight/ui/src/components/recommendation-voting/RecommendationVoting.tsx index 2e0f2b3d8d..cc5bd99017 100644 --- a/redisinsight/ui/src/components/recommendation-voting/RecommendationVoting.tsx +++ b/redisinsight/ui/src/components/recommendation-voting/RecommendationVoting.tsx @@ -31,7 +31,7 @@ const RecommendationVoting = ({ vote, name, id = '', live = false, containerClas gutterSize={live ? 'none' : 'l'} data-testid="recommendation-voting" > - Is this useful? + Is this useful?
{Object.values(Vote).map((option) => ( void +} + const badgesContent = [ { id: 'code_changes', icon: , name: 'Code Changes' }, { id: 'configuration_changes', icon: , name: 'Configuration Changes' }, @@ -87,7 +92,7 @@ const addUtmToLink = (href: string, telemetryName: string): string => { const renderContentElement = ( { type, value: jsonValue, parameter }: IRecommendationContent, params: any, - telemetryName: string, + telemetry: ITelemetry, insights: boolean, idx: number ) => { @@ -96,8 +101,8 @@ const renderContentElement = ( case 'paragraph': return ( @@ -121,8 +126,8 @@ const renderContentElement = ( case 'span': return ( @@ -132,11 +137,12 @@ const renderContentElement = ( case 'link': return ( telemetry.onClickLink?.()} > {value.name} @@ -144,11 +150,11 @@ const renderContentElement = ( case 'code-link': return ( ) case 'list': return ( -
    +
      {isArray(jsonValue) && jsonValue.map((listElement: IRecommendationContent[], idx) => (
    • - {renderRecommendationContent(listElement, params, telemetryName, insights)} + {renderRecommendationContent(listElement, params, telemetry, insights)}
    • ))}
    @@ -190,10 +200,10 @@ const renderContentElement = ( const renderRecommendationContent = ( elements: IRecommendationContent[] = [], params: any, - telemetryName: string, + telemetry: ITelemetry, insights: boolean = false ) => ( - elements?.map((item, idx) => renderContentElement(item, params, telemetryName, insights, idx))) + elements?.map((item, idx) => renderContentElement(item, params, telemetry, insights, idx))) const sortRecommendations = (recommendations: any[]) => sortBy(recommendations, [ ({ name }) => name !== 'searchJSON', diff --git a/redisinsight/ui/src/utils/tests/recommendation/utils.spec.tsx b/redisinsight/ui/src/utils/tests/recommendation/utils.spec.tsx index c5119464a9..1b19567c1d 100644 --- a/redisinsight/ui/src/utils/tests/recommendation/utils.spec.tsx +++ b/redisinsight/ui/src/utils/tests/recommendation/utils.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from 'uiSrc/utils/test-utils' +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' import { addUtmToLink, sortRecommendations, @@ -198,7 +198,7 @@ describe('replaceVariables', () => { describe('renderRecommendationContent', () => { it('should render content', () => { - const renderedContent = renderRecommendationContent(mockContent, undefined, mockTelemetryName) + const renderedContent = renderRecommendationContent(mockContent, undefined, { telemetryName: mockTelemetryName }) render(renderedContent) expect(screen.queryByTestId(`paragraph-${mockTelemetryName}-0`)).toBeInTheDocument() @@ -210,4 +210,18 @@ describe('renderRecommendationContent', () => { expect(screen.queryByTestId(`code-link-${mockTelemetryName}-7`)).toBeInTheDocument() expect(screen.getByText('unknown')).toBeInTheDocument() }) + + it('click on link should call onClick', () => { + const onClickMock = jest.fn() + const renderedContent = renderRecommendationContent( + mockContent, + null, + { telemetryName: mockTelemetryName, onClickLink: onClickMock }, + ) + const { queryByTestId } = render(renderedContent) + + fireEvent.click(queryByTestId(`link-${mockTelemetryName}-6`)) + + expect(onClickMock).toBeCalled() + }) })