Skip to content

Commit

Permalink
fix: System messages are counted as agents' first responses in livech…
Browse files Browse the repository at this point in the history
…at rooms (#32846)
  • Loading branch information
matheusbsilva137 authored Aug 21, 2024
1 parent eb5e60e commit 7937ff7
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 99 deletions.
6 changes: 6 additions & 0 deletions .changeset/rotten-camels-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-typings": patch
---

Fixed issue with system messages being counted as agents' first responses in livechat rooms (which caused the "best first response time" and "average first response time" metrics to be unreliable for all agents)
6 changes: 3 additions & 3 deletions apps/meteor/app/livechat/server/hooks/markRoomResponded.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IOmnichannelRoom, IMessage } from '@rocket.chat/core-typings';
import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings';
import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import { LivechatRooms, LivechatVisitors, LivechatInquiry } from '@rocket.chat/models';
import moment from 'moment';
Expand All @@ -12,7 +12,7 @@ export async function markRoomResponded(
room: IOmnichannelRoom,
roomUpdater: Updater<IOmnichannelRoom>,
): Promise<IOmnichannelRoom['responseBy'] | undefined> {
if (message.t || isEditedMessage(message) || isMessageFromVisitor(message)) {
if (isSystemMessage(message) || isEditedMessage(message) || isMessageFromVisitor(message)) {
return;
}

Expand Down Expand Up @@ -62,7 +62,7 @@ export async function markRoomResponded(
callbacks.add(
'afterOmnichannelSaveMessage',
async (message, { room, roomUpdater }) => {
if (!message || message.t || isEditedMessage(message) || isMessageFromVisitor(message)) {
if (!message || isEditedMessage(message) || isMessageFromVisitor(message) || isSystemMessage(message)) {
return;
}

Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings';
import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket.chat/core-typings';
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';

Expand Down Expand Up @@ -62,7 +62,7 @@ const getAnalyticsData = (room: IOmnichannelRoom, now: Date): Record<string, str
callbacks.add(
'afterOmnichannelSaveMessage',
async (message, { room, roomUpdater }) => {
if (!message || isEditedMessage(message)) {
if (!message || isEditedMessage(message) || isSystemMessage(message)) {
return message;
}

Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { OmnichannelAnalytics } from '@rocket.chat/core-services';
import mem from 'mem';

export const getAgentOverviewDataCached = mem(OmnichannelAnalytics.getAgentOverviewData, { maxAge: 60000, cacheKey: JSON.stringify });
export const getAgentOverviewDataCached = mem(OmnichannelAnalytics.getAgentOverviewData, {
maxAge: process.env.TEST_MODE === 'true' ? 1 : 60000,
cacheKey: JSON.stringify,
});
// Agent overview data on realtime is cached for 5 seconds
// while the data on the overview page is cached for 1 minute
export const getAnalyticsOverviewDataCached = mem(OmnichannelAnalytics.getAnalyticsOverviewData, {
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/tests/data/livechat/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,11 @@ export const uploadFile = (roomId: string, visitorToken: string): Promise<IMessa
};

// Sends a message using sendMessage method from agent
export const sendAgentMessage = (roomId: string, msg?: string): Promise<IMessage> => {
export const sendAgentMessage = (roomId: string, msg?: string, userCredentials: Credentials = credentials): Promise<IMessage> => {
return new Promise((resolve, reject) => {
void request
.post(methodCall('sendMessage'))
.set(credentials)
.set(userCredentials)
.send({
message: JSON.stringify({
method: 'sendMessage',
Expand Down
201 changes: 197 additions & 4 deletions apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Credentials } from '@rocket.chat/api-client';
import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { expect } from 'chai';
import { before, describe, it } from 'mocha';
import { before, after, describe, it } from 'mocha';
import moment from 'moment';
import type { Response } from 'supertest';

Expand All @@ -19,6 +19,7 @@ import {
import { createAnOnlineAgent } from '../../../data/livechat/users';
import { sleep } from '../../../data/livechat/utils';
import { removePermissionFromAllRoles, restorePermissionToRoles, updateSetting } from '../../../data/permissions.helper';
import { deleteUser } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';

describe('LIVECHAT - dashboards', function () {
Expand Down Expand Up @@ -777,6 +778,198 @@ describe('LIVECHAT - dashboards', function () {
});
});

describe('[livechat/analytics/agent-overview] - Average first response time', () => {
let agent: { credentials: Credentials; user: IUser & { username: string } };
let originalFirstResponseTimeInSeconds: number;
let roomId: string;
const firstDelayInSeconds = 4;
const secondDelayInSeconds = 8;

before(async () => {
agent = await createAnOnlineAgent();
});

after(async () => {
await deleteUser(agent.user);
});

it('should return no average response time for an agent if no response has been sent in the period', async () => {
await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });

const today = moment().startOf('day').format('YYYY-MM-DD');

const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Avg_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);

expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
expect(result.body.data).to.not.deep.include({ name: agent.user.username });
});

it("should not consider system messages in agents' first response time metric", async () => {
const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomId = response.room._id;

await sleep(firstDelayInSeconds * 1000);
await sendAgentMessage(roomId, 'first response from agent', agent.credentials);

const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Avg_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);

expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');

const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
originalFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(originalFirstResponseTimeInSeconds).to.be.greaterThanOrEqual(firstDelayInSeconds);
});

it('should correctly calculate the average time of first responses for an agent', async () => {
const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomId = response.room._id;

await sleep(secondDelayInSeconds * 1000);
await sendAgentMessage(roomId, 'first response from agent', agent.credentials);

const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Avg_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);

expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.not.empty;

const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
const averageFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(averageFirstResponseTimeInSeconds).to.be.greaterThan(originalFirstResponseTimeInSeconds);
expect(averageFirstResponseTimeInSeconds).to.be.greaterThanOrEqual((firstDelayInSeconds + secondDelayInSeconds) / 2);
expect(averageFirstResponseTimeInSeconds).to.be.lessThan(secondDelayInSeconds);
});
});

describe('[livechat/analytics/agent-overview] - Best first response time', () => {
let agent: { credentials: Credentials; user: IUser & { username: string } };
let originalBestFirstResponseTimeInSeconds: number;
let roomId: string;

before(async () => {
agent = await createAnOnlineAgent();
});

after(() => deleteUser(agent.user));

it('should return no best response time for an agent if no response has been sent in the period', async () => {
await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });

const today = moment().startOf('day').format('YYYY-MM-DD');

const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Best_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);

expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');
expect(result.body.data).to.not.deep.include({ name: agent.user.username });
});

it("should not consider system messages in agents' best response time metric", async () => {
const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomId = response.room._id;

const delayInSeconds = 4;
await sleep(delayInSeconds * 1000);

await sendAgentMessage(roomId, 'first response from agent', agent.credentials);

const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Best_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);

expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.not.empty;

const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
originalBestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(originalBestFirstResponseTimeInSeconds).to.be.greaterThanOrEqual(delayInSeconds);
});

it('should correctly calculate the best first response time for an agent and there are multiple first responses in the period', async () => {
const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
roomId = response.room._id;

const delayInSeconds = 6;
await sleep(delayInSeconds * 1000);

await sendAgentMessage(roomId, 'first response from agent', agent.credentials);

const today = moment().startOf('day').format('YYYY-MM-DD');
const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: today, to: today, name: 'Best_first_response_time' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);

expect(result.body).to.have.property('success', true);
expect(result.body).to.have.property('head');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array');

const agentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === agent.user.username,
);
expect(agentData).to.not.be.undefined;
expect(agentData).to.have.property('name', agent.user.username);
expect(agentData).to.have.property('value');
const bestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(bestFirstResponseTimeInSeconds).to.be.equal(originalBestFirstResponseTimeInSeconds);
});
});

describe('livechat/analytics/overview', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
Expand Down Expand Up @@ -835,12 +1028,12 @@ describe('LIVECHAT - dashboards', function () {
expect(result.body).to.be.an('array');

const expectedResult = [
{ title: 'Total_conversations', value: 7 },
{ title: 'Open_conversations', value: 4 },
{ title: 'Total_conversations', value: 13 },
{ title: 'Open_conversations', value: 10 },
{ title: 'On_Hold_conversations', value: 1 },
// { title: 'Total_messages', value: 6 },
// { title: 'Busiest_day', value: moment().format('dddd') },
{ title: 'Conversations_per_day', value: '3.50' },
{ title: 'Conversations_per_day', value: '6.50' },
// { title: 'Busiest_time', value: '' },
];

Expand Down
Loading

0 comments on commit 7937ff7

Please sign in to comment.