Skip to content

Commit af1eabb

Browse files
Merge pull request #2576 from RedisInsight/be/feature/RI-4865_Send_frontend_events_in_batches
#RI-4865 - [BE] send frontend events in batches
2 parents 4d9646b + 23b1005 commit af1eabb

File tree

9 files changed

+307
-7
lines changed

9 files changed

+307
-7
lines changed

redisinsight/api/src/constants/app-events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export enum AppAnalyticsEvents {
22
Initialize = 'analytics.initialize',
33
Track = 'analytics.track',
4+
Page = 'analytics.page',
45
}
56

67
export enum AppRedisInstanceEvents {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
Body,
3+
Controller, Post, UsePipes, ValidationPipe,
4+
} from '@nestjs/common';
5+
import { ApiTags } from '@nestjs/swagger';
6+
import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';
7+
import { SendEventDto } from 'src/modules/analytics/dto/analytics.dto';
8+
import { AnalyticsService } from 'src/modules/analytics/analytics.service';
9+
10+
@ApiTags('Analytics')
11+
@Controller('analytics')
12+
@UsePipes(new ValidationPipe({ transform: true }))
13+
export class AnalyticsController {
14+
constructor(private service: AnalyticsService) {}
15+
16+
@Post('send-event')
17+
@ApiEndpoint({
18+
description: 'Send telemetry event',
19+
statusCode: 204,
20+
responses: [
21+
{
22+
status: 204,
23+
},
24+
],
25+
})
26+
async sendEvent(
27+
@Body() dto: SendEventDto,
28+
): Promise<void> {
29+
return this.service.sendEvent(dto);
30+
}
31+
32+
@Post('send-page')
33+
@ApiEndpoint({
34+
description: 'Send telemetry page',
35+
statusCode: 204,
36+
responses: [
37+
{
38+
status: 204,
39+
},
40+
],
41+
})
42+
async sendPage(
43+
@Body() dto: SendEventDto,
44+
): Promise<void> {
45+
return this.service.sendPage(dto);
46+
}
47+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { Module } from '@nestjs/common';
22
import { AnalyticsService } from 'src/modules/analytics/analytics.service';
3+
import { AnalyticsController } from './analytics.controller';
34

45
@Module({
56
providers: [
67
AnalyticsService,
78
],
9+
controllers: [
10+
AnalyticsController,
11+
],
812
})
913
export class AnalyticsModule {}

redisinsight/api/src/modules/analytics/analytics.service.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import {
1414
} from './analytics.service';
1515

1616
let mockAnalyticsTrack;
17+
let mockAnalyticsPage;
1718
jest.mock(
1819
'analytics-node',
1920
() => jest.fn()
2021
.mockImplementation(() => ({
2122
track: mockAnalyticsTrack,
23+
page: mockAnalyticsPage,
2224
})),
2325
);
2426

@@ -134,4 +136,71 @@ describe('AnalyticsService', () => {
134136
});
135137
});
136138
});
139+
140+
describe('sendPage', () => {
141+
beforeEach(() => {
142+
mockAnalyticsPage = jest.fn();
143+
service.initialize({
144+
anonymousId: mockAnonymousId,
145+
sessionId,
146+
appType: AppType.Electron,
147+
controlNumber: mockControlNumber,
148+
controlGroup: mockControlGroup,
149+
appVersion: mockAppVersion,
150+
});
151+
});
152+
it('should send page with anonymousId if permission are granted', async () => {
153+
settingsService.getAppSettings.mockResolvedValue(mockAppSettings);
154+
155+
await service.sendPage({
156+
event: TelemetryEvents.ApplicationStarted,
157+
eventData: {},
158+
nonTracking: false,
159+
});
160+
161+
expect(mockAnalyticsPage).toHaveBeenCalledWith({
162+
anonymousId: mockAnonymousId,
163+
integrations: { Amplitude: { session_id: sessionId } },
164+
name: TelemetryEvents.ApplicationStarted,
165+
properties: {
166+
buildType: AppType.Electron,
167+
controlNumber: mockControlNumber,
168+
controlGroup: mockControlGroup,
169+
appVersion: mockAppVersion,
170+
},
171+
});
172+
});
173+
it('should not send page if permission are not granted', async () => {
174+
settingsService.getAppSettings.mockResolvedValue(mockAppSettingsWithoutPermissions);
175+
176+
await service.sendPage({
177+
event: 'SOME_EVENT',
178+
eventData: {},
179+
nonTracking: false,
180+
});
181+
182+
expect(mockAnalyticsPage).not.toHaveBeenCalled();
183+
});
184+
it('should send page for non tracking events event if permission are not granted', async () => {
185+
settingsService.getAppSettings.mockResolvedValue(mockAppSettingsWithoutPermissions);
186+
187+
await service.sendPage({
188+
event: TelemetryEvents.ApplicationStarted,
189+
eventData: {},
190+
nonTracking: true,
191+
});
192+
193+
expect(mockAnalyticsPage).toHaveBeenCalledWith({
194+
anonymousId: mockAnonymousId,
195+
integrations: { Amplitude: { session_id: sessionId } },
196+
name: TelemetryEvents.ApplicationStarted,
197+
properties: {
198+
buildType: AppType.Electron,
199+
controlNumber: mockControlNumber,
200+
controlGroup: mockControlGroup,
201+
appVersion: mockAppVersion,
202+
},
203+
});
204+
});
205+
});
137206
});

redisinsight/api/src/modules/analytics/analytics.service.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,8 @@ export class AnalyticsService {
7575
// for analytics is granted or not.
7676
// If permissions not granted anonymousId includes "UNSET" value without any user identifiers.
7777
const { event, eventData, nonTracking } = payload;
78-
const isAnalyticsGranted = !!get(
79-
// todo: define how to fetch userId?
80-
await this.settingsService.getAppSettings('1'),
81-
'agreements.analytics',
82-
false,
83-
);
78+
const isAnalyticsGranted = await this.checkIsAnalyticsGranted();
79+
8480
if (isAnalyticsGranted || nonTracking) {
8581
this.analytics.track({
8682
anonymousId: this.anonymousId,
@@ -99,4 +95,45 @@ export class AnalyticsService {
9995
// continue regardless of error
10096
}
10197
}
98+
99+
@OnEvent(AppAnalyticsEvents.Page)
100+
async sendPage(payload: ITelemetryEvent) {
101+
try {
102+
// The event is reported only if the user's permission is granted.
103+
// The anonymousId is also sent along with the event.
104+
//
105+
// The `nonTracking` argument can be set to True to mark an event that doesn't track the specific
106+
// user in any way. When `nonTracking` is True, the event is sent regardless of whether the user's permission
107+
// for analytics is granted or not.
108+
// If permissions not granted anonymousId includes "UNSET" value without any user identifiers.
109+
const { event, eventData, nonTracking } = payload;
110+
const isAnalyticsGranted = await this.checkIsAnalyticsGranted();
111+
112+
if (isAnalyticsGranted || nonTracking) {
113+
this.analytics.page({
114+
name: event,
115+
anonymousId: this.anonymousId,
116+
integrations: { Amplitude: { session_id: this.sessionId } },
117+
properties: {
118+
...eventData,
119+
buildType: this.appType,
120+
controlNumber: this.controlNumber,
121+
controlGroup: this.controlGroup,
122+
appVersion: this.appVersion,
123+
},
124+
});
125+
}
126+
} catch (e) {
127+
// continue regardless of error
128+
}
129+
}
130+
131+
private async checkIsAnalyticsGranted() {
132+
return !!get(
133+
// todo: define how to fetch userId?
134+
await this.settingsService.getAppSettings('1'),
135+
'agreements.analytics',
136+
false,
137+
);
138+
}
102139
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import {
3+
IsBoolean,
4+
IsDefined,
5+
IsNotEmpty,
6+
IsOptional,
7+
IsString,
8+
ValidateNested,
9+
} from 'class-validator';
10+
11+
export class SendEventDto {
12+
@ApiProperty({
13+
description: 'Telemetry event name.',
14+
type: String,
15+
example: 'APPLICATION_UPDATED',
16+
})
17+
@IsDefined()
18+
@IsNotEmpty()
19+
@IsString()
20+
event: string;
21+
22+
@ApiPropertyOptional({
23+
description: 'Telemetry event data.',
24+
type: Object,
25+
example: { length: 5 },
26+
})
27+
@IsOptional()
28+
@ValidateNested()
29+
eventData: Object = {};
30+
31+
@ApiPropertyOptional({
32+
description: 'Does not track the specific user in any way?',
33+
type: Boolean,
34+
example: false,
35+
})
36+
@IsOptional()
37+
@IsBoolean()
38+
nonTracking: boolean = false;
39+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
describe,
3+
deps,
4+
Joi,
5+
generateInvalidDataTestCases,
6+
validateInvalidDataTestCase,
7+
getMainCheckFn, _,
8+
} from '../deps';
9+
const { server, request, constants } = deps;
10+
11+
// endpoint to test
12+
const endpoint = () =>
13+
request(server).post('/analytics/send-event');
14+
15+
// input data schema
16+
const dataSchema = Joi.object({
17+
event: Joi.string().required(),
18+
eventData: Joi.object().allow(null),
19+
}).strict();
20+
21+
const validInputData = {
22+
event: constants.TEST_ANALYTICS_EVENT,
23+
eventData: constants.TEST_ANALYTICS_EVENT_DATA,
24+
};
25+
26+
const mainCheckFn = getMainCheckFn(endpoint);
27+
28+
describe('POST /analytics/send-event', () => {
29+
describe('Main', () => {
30+
describe('Validation', () => {
31+
generateInvalidDataTestCases(dataSchema, validInputData).map(
32+
validateInvalidDataTestCase(endpoint, dataSchema),
33+
);
34+
});
35+
36+
describe('Common', () => {
37+
[
38+
{
39+
name: 'Should send telemetry event',
40+
data: {
41+
event: constants.TEST_ANALYTICS_EVENT,
42+
eventData: constants.TEST_ANALYTICS_EVENT_DATA,
43+
},
44+
statusCode: 204,
45+
},
46+
].map(mainCheckFn);
47+
});
48+
});
49+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
describe,
3+
deps,
4+
Joi,
5+
generateInvalidDataTestCases,
6+
validateInvalidDataTestCase,
7+
getMainCheckFn, _,
8+
} from '../deps';
9+
const { server, request, constants } = deps;
10+
11+
// endpoint to test
12+
const endpoint = () =>
13+
request(server).post('/analytics/send-page');
14+
15+
// input data schema
16+
const dataSchema = Joi.object({
17+
event: Joi.string().required(),
18+
eventData: Joi.object().allow(null),
19+
}).strict();
20+
21+
const validInputData = {
22+
event: constants.TEST_ANALYTICS_PAGE,
23+
eventData: constants.TEST_ANALYTICS_EVENT_DATA,
24+
};
25+
26+
const mainCheckFn = getMainCheckFn(endpoint);
27+
28+
describe('POST /analytics/send-page', () => {
29+
describe('Main', () => {
30+
describe('Validation', () => {
31+
generateInvalidDataTestCases(dataSchema, validInputData).map(
32+
validateInvalidDataTestCase(endpoint, dataSchema),
33+
);
34+
});
35+
36+
describe('Common', () => {
37+
[
38+
{
39+
name: 'Should send telemetry page',
40+
data: {
41+
event: constants.TEST_ANALYTICS_PAGE,
42+
eventData: constants.TEST_ANALYTICS_EVENT_DATA,
43+
},
44+
statusCode: 204,
45+
},
46+
].map(mainCheckFn);
47+
});
48+
});
49+
});

redisinsight/api/test/helpers/constants.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { v4 as uuidv4 } from 'uuid';
22
import * as path from 'path';
33
import { randomBytes } from 'crypto';
44
import { getASCIISafeStringFromBuffer, getBufferFromSafeASCIIString } from "src/utils/cli-helper";
5-
import { RECOMMENDATION_NAMES } from 'src/constants';
5+
import { RECOMMENDATION_NAMES, TelemetryEvents } from 'src/constants';
66
import { Compressor } from 'src/modules/database/entities/database.entity';
77
import { Vote } from 'src/modules/database-recommendation/models';
88

@@ -20,6 +20,7 @@ const APP_DEFAULT_SETTINGS = {
2020
agreements: null,
2121
};
2222
const TEST_LIBRARY_NAME = 'lib';
23+
const TEST_ANALYTICS_PAGE = 'Settings';
2324

2425
const unprintableBuf = Buffer.concat([
2526
Buffer.from('acedae', 'hex'),
@@ -590,5 +591,9 @@ export const constants = {
590591
TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME: TEST_LIBRARY_NAME,
591592
TEST_TRIGGERED_FUNCTIONS_CODE: `#!js api_version=1.0 name=${TEST_LIBRARY_NAME}\n redis.registerFunction('foo', ()=>{return 'bar'})`,
592593
TEST_TRIGGERED_FUNCTIONS_CONFIGURATION: "{}",
594+
595+
TEST_ANALYTICS_EVENT: TelemetryEvents.RedisInstanceAdded,
596+
TEST_ANALYTICS_EVENT_DATA: { length: 5 },
597+
TEST_ANALYTICS_PAGE,
593598
// etc...
594599
}

0 commit comments

Comments
 (0)