diff --git a/redisinsight/api/src/__mocks__/analytics.ts b/redisinsight/api/src/__mocks__/analytics.ts index e1060eb1b3..2c86f04b6f 100644 --- a/redisinsight/api/src/__mocks__/analytics.ts +++ b/redisinsight/api/src/__mocks__/analytics.ts @@ -27,3 +27,9 @@ export const mockSettingsAnalyticsService = () => ({ sendAnalyticsAgreementChange: jest.fn(), sendSettingsUpdatedEvent: jest.fn(), }); + +export const mockPubSubAnalyticsService = () => ({ + sendMessagePublishedEvent: jest.fn(), + sendChannelSubscribeEvent: jest.fn(), + sendChannelUnsubscribeEvent: jest.fn(), +}); diff --git a/redisinsight/api/src/constants/telemetry-events.ts b/redisinsight/api/src/constants/telemetry-events.ts index bbeb4c4f60..5f54af6b05 100644 --- a/redisinsight/api/src/constants/telemetry-events.ts +++ b/redisinsight/api/src/constants/telemetry-events.ts @@ -45,4 +45,9 @@ export enum TelemetryEvents { // Slowlog SlowlogSetLogSlowerThan = 'SLOWLOG_SET_LOG_SLOWER_THAN', SlowlogSetMaxLen = 'SLOWLOG_SET_MAX_LEN', + + // Pub/Sub + PubSubMessagePublished = 'PUBSUB_MESSAGE_PUBLISHED', + PubSubChannelSubscribed = 'PUBSUB_CHANNEL_SUBSCRIBED', + PubSubChannelUnsubscribed = 'PUBSUB_CHANNEL_UNSUBSCRIBED', } diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.spec.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.spec.ts new file mode 100644 index 0000000000..64fba7fe76 --- /dev/null +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.spec.ts @@ -0,0 +1,76 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { mockStandaloneDatabaseEntity } from 'src/__mocks__'; +import { TelemetryEvents } from 'src/constants'; +import { PubSubAnalyticsService } from './pub-sub.analytics.service'; + +const instanceId = mockStandaloneDatabaseEntity.id; + +const affected = 2; + +describe('PubSubAnalyticsService', () => { + let service: PubSubAnalyticsService; + let sendEventMethod: jest.SpyInstance; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + EventEmitter2, + PubSubAnalyticsService, + ], + }).compile(); + + service = module.get(PubSubAnalyticsService); + sendEventMethod = jest.spyOn( + service, + 'sendEvent', + ); + }); + + describe('sendMessagePublishedEvent', () => { + it('should emit sendMessagePublished event', () => { + service.sendMessagePublishedEvent( + instanceId, + affected, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.PubSubMessagePublished, + { + databaseId: instanceId, + clients: affected, + }, + ); + }); + }); + + describe('sendChannelSubscribeEvent', () => { + it('should emit sendChannelSubscribe event', () => { + service.sendChannelSubscribeEvent( + instanceId, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.PubSubChannelSubscribed, + { + databaseId: instanceId, + }, + ); + }); + }); + + describe('sendChannelUnsubscribeEvent', () => { + it('should emit sendChannelUnsubscribe event', () => { + service.sendChannelUnsubscribeEvent( + instanceId, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.PubSubChannelUnsubscribed, + { + databaseId: instanceId, + }, + ); + }); + }); +}); diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.ts new file mode 100644 index 0000000000..d7959861c4 --- /dev/null +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.ts @@ -0,0 +1,59 @@ +import { 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 { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; +import { RedisError, ReplyError } from 'src/models'; + +export interface IExecResult { + response: any; + status: CommandExecutionStatus; + error?: RedisError | ReplyError | Error, +} + +@Injectable() +export class PubSubAnalyticsService extends TelemetryBaseService { + constructor(protected eventEmitter: EventEmitter2) { + super(eventEmitter); + } + + sendMessagePublishedEvent(databaseId: string, affected: number): void { + try { + this.sendEvent( + TelemetryEvents.PubSubMessagePublished, + { + databaseId, + clients: affected, + }, + ); + } catch (e) { + // continue regardless of error + } + } + + sendChannelSubscribeEvent(databaseId: string): void { + try { + this.sendEvent( + TelemetryEvents.PubSubChannelSubscribed, + { + databaseId, + }, + ); + } catch (e) { + // continue regardless of error + } + } + + sendChannelUnsubscribeEvent(databaseId: string): void { + try { + this.sendEvent( + TelemetryEvents.PubSubChannelUnsubscribed, + { + databaseId, + }, + ); + } catch (e) { + // continue regardless of error + } + } +} diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.module.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.module.ts index 5822b3c33c..06839aea06 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.module.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.module.ts @@ -6,12 +6,14 @@ import { UserSessionProvider } from 'src/modules/pub-sub/providers/user-session. import { SubscriptionProvider } from 'src/modules/pub-sub/providers/subscription.provider'; import { RedisClientProvider } from 'src/modules/pub-sub/providers/redis-client.provider'; import { PubSubController } from 'src/modules/pub-sub/pub-sub.controller'; +import { PubSubAnalyticsService } from 'src/modules/pub-sub/pub-sub.analytics.service'; @Module({ imports: [SharedModule], providers: [ PubSubGateway, PubSubService, + PubSubAnalyticsService, UserSessionProvider, SubscriptionProvider, RedisClientProvider, diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts index b22017ebc8..f3e3be8bf8 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts @@ -1,22 +1,27 @@ import { Test, TestingModule } from '@nestjs/testing'; import * as Redis from 'ioredis'; import { - mockLogFile, mockRedisShardObserver, mockSocket, mockStandaloneDatabaseEntity, - MockType + // mockLogFile, + // mockRedisShardObserver, + mockSocket, + mockStandaloneDatabaseEntity, + MockType, + mockPubSubAnalyticsService } from 'src/__mocks__'; import { InstancesBusinessService } from 'src/modules/shared/services/instances-business/instances-business.service'; -import { RedisObserverProvider } from 'src/modules/profiler/providers/redis-observer.provider'; +// import { RedisObserverProvider } from 'src/modules/profiler/providers/redis-observer.provider'; import { IFindRedisClientInstanceByOptions, RedisService } from 'src/modules/core/services/redis/redis.service'; import { mockRedisClientInstance } from 'src/modules/shared/services/base/redis-consumer.abstract.service.spec'; -import { RedisObserverStatus } from 'src/modules/profiler/constants'; +// import { RedisObserverStatus } from 'src/modules/profiler/constants'; import { PubSubService } from 'src/modules/pub-sub/pub-sub.service'; import { UserSessionProvider } from 'src/modules/pub-sub/providers/user-session.provider'; import { SubscriptionProvider } from 'src/modules/pub-sub/providers/subscription.provider'; import { UserClient } from 'src/modules/pub-sub/model/user-client'; import { SubscriptionType } from 'src/modules/pub-sub/constants'; -import { RedisClientProvider } from 'src/modules/pub-sub/providers/redis-client.provider'; +// import { RedisClientProvider } from 'src/modules/pub-sub/providers/redis-client.provider'; import { UserSession } from 'src/modules/pub-sub/model/user-session'; import { RedisClient } from 'src/modules/pub-sub/model/redis-client'; +import { PubSubAnalyticsService } from 'src/modules/pub-sub/pub-sub.analytics.service'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; const nodeClient = Object.create(Redis.prototype); @@ -81,6 +86,10 @@ describe('PubSubService', () => { removeUserSession: jest.fn(), }), }, + { + provide: PubSubAnalyticsService, + useFactory: mockPubSubAnalyticsService, + }, { provide: RedisService, useFactory: () => ({ diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts index 1c5724a92e..7dcb7e83d5 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts @@ -6,6 +6,7 @@ import { SubscriptionProvider } from 'src/modules/pub-sub/providers/subscription import { IFindRedisClientInstanceByOptions, RedisService } from 'src/modules/core/services/redis/redis.service'; import { PublishResponse } from 'src/modules/pub-sub/dto/publish.response'; import { PublishDto } from 'src/modules/pub-sub/dto/publish.dto'; +import { PubSubAnalyticsService } from 'src/modules/pub-sub/pub-sub.analytics.service'; import { InstancesBusinessService } from 'src/modules/shared/services/instances-business/instances-business.service'; import { catchAclError } from 'src/utils'; @@ -18,6 +19,7 @@ export class PubSubService { private readonly subscriptionProvider: SubscriptionProvider, private redisService: RedisService, private instancesBusinessService: InstancesBusinessService, + private analyticsService: PubSubAnalyticsService, ) {} /** @@ -33,6 +35,7 @@ export class PubSubService { await Promise.all(dto.subscriptions.map((subDto) => session.subscribe( this.subscriptionProvider.createSubscription(userClient, subDto), ))); + this.analyticsService.sendChannelSubscribeEvent(userClient.getDatabaseId()); } catch (e) { this.logger.error('Unable to create subscriptions', e); @@ -57,6 +60,7 @@ export class PubSubService { await Promise.all(dto.subscriptions.map((subDto) => session.unsubscribe( this.subscriptionProvider.createSubscription(userClient, subDto), ))); + this.analyticsService.sendChannelUnsubscribeEvent(userClient.getDatabaseId()); } catch (e) { this.logger.error('Unable to unsubscribe', e); @@ -81,9 +85,12 @@ export class PubSubService { this.logger.log('Publishing message.'); const client = await this.getClient(clientOptions); + const affected = await client.publish(dto.channel, dto.message); + + this.analyticsService.sendMessagePublishedEvent(clientOptions.instanceId, affected); return { - affected: await client.publish(dto.channel, dto.message), + affected, }; } catch (e) { this.logger.error('Unable to publish a message', e); diff --git a/redisinsight/ui/src/pages/pubSub/PubSubPage.tsx b/redisinsight/ui/src/pages/pubSub/PubSubPage.tsx index 6a5c805f36..5ef145cf9f 100644 --- a/redisinsight/ui/src/pages/pubSub/PubSubPage.tsx +++ b/redisinsight/ui/src/pages/pubSub/PubSubPage.tsx @@ -1,7 +1,12 @@ import { EuiTitle } from '@elastic/eui' -import React from 'react' +import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import InstanceHeader from 'uiSrc/components/instance-header' import { SubscriptionType } from 'uiSrc/constants/pubSub' +import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import { MessagesListWrapper, PublishMessage, SubscriptionPanel } from './components' @@ -10,7 +15,26 @@ import styles from './styles.module.scss' export const PUB_SUB_DEFAULT_CHANNEL = { channel: '*', type: SubscriptionType.PSubscribe } const PubSubPage = () => { - // + const { identified: analyticsIdentified } = useSelector(appAnalyticsInfoSelector) + const { name: connectedInstanceName } = useSelector(connectedInstanceSelector) + const { instanceId } = useParams<{ instanceId: string }>() + + const [isPageViewSent, setIsPageViewSent] = useState(false) + + useEffect(() => { + if (connectedInstanceName && !isPageViewSent && analyticsIdentified) { + sendPageView(instanceId) + } + }, [connectedInstanceName, isPageViewSent, analyticsIdentified]) + + const sendPageView = (instanceId: string) => { + sendPageViewTelemetry({ + name: TelemetryPageView.PUBSUB_PAGE, + databaseId: instanceId + }) + setIsPageViewSent(true) + } + return ( <> diff --git a/redisinsight/ui/src/telemetry/pageViews.ts b/redisinsight/ui/src/telemetry/pageViews.ts index ae25e594be..70e0582aa7 100644 --- a/redisinsight/ui/src/telemetry/pageViews.ts +++ b/redisinsight/ui/src/telemetry/pageViews.ts @@ -4,5 +4,6 @@ export enum TelemetryPageView { SETTINGS_PAGE = 'Settings', BROWSER_PAGE = 'Browser', WORKBENCH_PAGE = 'Workbench', - SLOWLOG_PAGE = 'Slow Log' + SLOWLOG_PAGE = 'Slow Log', + PUBSUB_PAGE = 'Pub/Sub' }