Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions redisinsight/api/src/__mocks__/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
5 changes: 5 additions & 0 deletions redisinsight/api/src/constants/telemetry-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Original file line number Diff line number Diff line change
@@ -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<PubSubAnalyticsService, unknown[]>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EventEmitter2,
PubSubAnalyticsService,
],
}).compile();

service = module.get<PubSubAnalyticsService>(PubSubAnalyticsService);
sendEventMethod = jest.spyOn<PubSubAnalyticsService, any>(
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,
},
);
});
});
});
Original file line number Diff line number Diff line change
@@ -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
}
}
}
2 changes: 2 additions & 0 deletions redisinsight/api/src/modules/pub-sub/pub-sub.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 14 additions & 5 deletions redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -81,6 +86,10 @@ describe('PubSubService', () => {
removeUserSession: jest.fn(),
}),
},
{
provide: PubSubAnalyticsService,
useFactory: mockPubSubAnalyticsService,
},
{
provide: RedisService,
useFactory: () => ({
Expand Down
9 changes: 8 additions & 1 deletion redisinsight/api/src/modules/pub-sub/pub-sub.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -18,6 +19,7 @@ export class PubSubService {
private readonly subscriptionProvider: SubscriptionProvider,
private redisService: RedisService,
private instancesBusinessService: InstancesBusinessService,
private analyticsService: PubSubAnalyticsService,
) {}

/**
Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);
Expand Down
28 changes: 26 additions & 2 deletions redisinsight/ui/src/pages/pubSub/PubSubPage.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 (
<>
<InstanceHeader />
Expand Down
3 changes: 2 additions & 1 deletion redisinsight/ui/src/telemetry/pageViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}