Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4d837ad
feat: adds new setting to ignore automatic messages
lucas-a-pelegrino Dec 19, 2024
90852de
feat: adds logic to skip metrics for automatic messages
lucas-a-pelegrino Dec 19, 2024
6fe8e5b
Merge branch 'develop' into CORE-668
lucas-a-pelegrino Dec 30, 2024
2bb55d2
refactor: improves logic for isMessageFromBot method
lucas-a-pelegrino Dec 30, 2024
ed8facd
tests: adds unit test for isMessageFromBot method
lucas-a-pelegrino Dec 30, 2024
e0fff07
refactor: inverts order of if statement validation to avoid unnecessa…
lucas-a-pelegrino Jan 7, 2025
3d1a785
refactor: adds validation for null returns from Users.findOneById
lucas-a-pelegrino Jan 7, 2025
15f46d1
test: adds e2e test to validate bot messages are ignored when setting…
lucas-a-pelegrino Jan 8, 2025
6eb89ab
test: adds unit test to check for setting and bot message on markRoom…
lucas-a-pelegrino Jan 8, 2025
b45e411
fix: merge conflicts en.i18n.json
lucas-a-pelegrino Jan 17, 2025
8139644
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into CORE…
lucas-a-pelegrino Jan 20, 2025
297a649
test: fixes e2e and unit tests
lucas-a-pelegrino Jan 20, 2025
b47ad58
test: fixes mocks used in the unit tests
lucas-a-pelegrino Jan 20, 2025
1f77cd8
docs: adds changeset
lucas-a-pelegrino Jan 20, 2025
67ee91f
chore: updates number of total conversations testing
lucas-a-pelegrino Jan 20, 2025
2bb0522
Merge branch 'CORE-668' of github.com:RocketChat/Rocket.Chat into COR…
lucas-a-pelegrino Jan 20, 2025
3ef8969
tests: fixes setting logic at saveAnalyticsData
lucas-a-pelegrino Jan 21, 2025
9fe3e3b
fix: updates Open_conversations expected values
lucas-a-pelegrino Jan 21, 2025
0857b09
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into CORE…
lucas-a-pelegrino Jan 22, 2025
cfc9ddb
fix: updates Conversations_per_day value
lucas-a-pelegrino Jan 22, 2025
58158b0
chore: updates en.i18n with setting label
lucas-a-pelegrino Jan 22, 2025
1d4bfbb
Merge branch 'develop' into CORE-668
lucas-a-pelegrino Jan 23, 2025
c70ec88
chore: updates .changeset/funny-turtles-hunt.md
lucas-a-pelegrino Jan 23, 2025
bc84edf
fix: readds labels mistakenly removed
lucas-a-pelegrino Jan 23, 2025
803da66
fix: readds labels mistakenly removed
lucas-a-pelegrino Jan 23, 2025
6089fee
Merge branch 'CORE-668' of github.com:RocketChat/Rocket.Chat into COR…
lucas-a-pelegrino Jan 23, 2025
10abc1a
refactor: moves setting update to before/after tests
lucas-a-pelegrino Jan 23, 2025
4221773
refactor: replaces Users.findOne for Users.isUserInRole
lucas-a-pelegrino Jan 24, 2025
741c929
Merge branch 'develop' into CORE-668
kodiakhq[bot] Feb 17, 2025
20e4ecd
Merge branch 'develop' into CORE-668
kodiakhq[bot] Feb 18, 2025
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 .changeset/funny-turtles-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
---

Adds a new setting that if enabled, will not count bot messages in the average response time metrics
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ import moment from 'moment';

import { callbacks } from '../../../../lib/callbacks';
import { notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
import { isMessageFromBot } from '../lib/isMessageFromBot';

export async function markRoomResponded(
message: IMessage,
room: IOmnichannelRoom,
roomUpdater: Updater<IOmnichannelRoom>,
): Promise<IOmnichannelRoom['responseBy'] | undefined> {
if (isSystemMessage(message) || isEditedMessage(message) || isMessageFromVisitor(message)) {
if (
isSystemMessage(message) ||
isEditedMessage(message) ||
isMessageFromVisitor(message) ||
(settings.get<boolean>('Omnichannel_Metrics_Ignore_Automatic_Messages') && (await isMessageFromBot(message)))
) {
return;
}

Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';

import { callbacks } from '../../../../lib/callbacks';
import { settings } from '../../../settings/server';
import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload';
import { isMessageFromBot } from '../lib/isMessageFromBot';

const getMetricValue = <T>(metric: T | undefined, defaultValue: T): T => metric ?? defaultValue;
const calculateTimeDifference = <T extends Date | number>(startTime: T, now: Date): number =>
Expand Down Expand Up @@ -73,6 +75,10 @@ callbacks.add(
if (isMessageFromVisitor(message)) {
LivechatRooms.getAnalyticsUpdateQueryBySentByVisitor(room, message, roomUpdater);
} else {
if (settings.get<boolean>('Omnichannel_Metrics_Ignore_Automatic_Messages') && (await isMessageFromBot(message))) {
return message;
}

const analyticsData = getAnalyticsData(room, new Date());
LivechatRooms.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, roomUpdater);
}
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/app/livechat/server/lib/isMessageFromBot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';

export async function isMessageFromBot(message: IMessage): Promise<boolean> {
Comment thread
MarcosSpessatto marked this conversation as resolved.
return Users.isUserInRole(message.u._id, 'bot');
}
8 changes: 8 additions & 0 deletions apps/meteor/server/settings/omnichannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,14 @@ export const createOmniSettings = () =>
i18nLabel: 'Call_provider',
enableQuery: omnichannelEnabledQuery,
});

await this.add('Omnichannel_Metrics_Ignore_Automatic_Messages', false, {
type: 'boolean',
public: true,
group: 'Omnichannel',
section: 'Analytics',
i18nLabel: 'Omnichannel_Ignore_automatic_responses_for_performance_metrics',
});
});
await settingsRegistry.addGroup('SMS', async function () {
await this.add('SMS_Enabled', false, {
Expand Down
48 changes: 43 additions & 5 deletions apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
sendMessage,
startANewLivechatRoomAndTakeIt,
} from '../../../data/livechat/rooms';
import { createAnOnlineAgent } from '../../../data/livechat/users';
import { createAnOnlineAgent, createBotAgent } from '../../../data/livechat/users';
import { sleep } from '../../../data/livechat/utils';
import { removePermissionFromAllRoles, restorePermissionToRoles, updateEESetting, updateSetting } from '../../../data/permissions.helper';
import { deleteUser } from '../../../data/users.helper';
Expand Down Expand Up @@ -923,6 +923,7 @@ describe('LIVECHAT - dashboards', function () {
describe('[livechat/analytics/agent-overview] - Average first response time', () => {
let agent: { credentials: Credentials; user: IUser & { username: string } };
let forwardAgent: { credentials: Credentials; user: IUser & { username: string } };
let botAgent: { credentials: Credentials; user: IUser };
let originalFirstResponseTimeInSeconds: number;
let roomId: string;
const firstDelayInSeconds = 4;
Expand All @@ -931,9 +932,18 @@ describe('LIVECHAT - dashboards', function () {
before(async () => {
agent = await createAnOnlineAgent();
forwardAgent = await createAnOnlineAgent();
botAgent = await createBotAgent();

await updateSetting('Omnichannel_Metrics_Ignore_Automatic_Messages', true);
});

after(async () => Promise.all([deleteUser(agent.user), deleteUser(forwardAgent.user)]));
after(async () =>
Promise.all([
deleteUser(agent.user),
deleteUser(forwardAgent.user),
updateSetting('Omnichannel_Metrics_Ignore_Automatic_Messages', false),
]),
);

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 });
Expand Down Expand Up @@ -984,6 +994,34 @@ describe('LIVECHAT - dashboards', function () {
expect(originalFirstResponseTimeInSeconds).to.be.greaterThanOrEqual(firstDelayInSeconds);
});

it("should not consider bot messages in agent's first response time metric if setting is enabled", async () => {
const response = await startANewLivechatRoomAndTakeIt({ agent: botAgent.credentials });

roomId = response.room._id;

await sleep(firstDelayInSeconds * 1000);

await sendAgentMessage(roomId, 'first response from agent', botAgent.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 === botAgent.user.username,
);
expect(agentData).to.be.undefined;
});

it('should correctly associate the first response time to the first agent who responded the room', async () => {
const response = await startANewLivechatRoomAndTakeIt({ agent: forwardAgent.credentials });
roomId = response.room._id;
Expand Down Expand Up @@ -1284,12 +1322,12 @@ describe('LIVECHAT - dashboards', function () {
expect(result.body).to.be.an('array');

const expectedResult = [
{ title: 'Total_conversations', value: 15 },
{ title: 'Open_conversations', value: 12 },
{ title: 'Total_conversations', value: 16 },
{ title: 'Open_conversations', value: 13 },
{ title: 'On_Hold_conversations', value: 1 },
// { title: 'Total_messages', value: 6 },
// { title: 'Busiest_day', value: moment().format('dddd') },
{ title: 'Conversations_per_day', value: '7.50' },
{ title: 'Conversations_per_day', value: '8.00' },
// { title: 'Busiest_time', value: '' },
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect } from 'chai';
import p from 'proxyquire';
import sinon from 'sinon';

import { createFakeMessage } from '../../../../../mocks/data';

const modelsMock = {
Users: {
isUserInRole: sinon.stub(),
},
};

const { isMessageFromBot } = p.noCallThru().load('../../../../../../app/livechat/server/lib/isMessageFromBot', {
'@rocket.chat/models': modelsMock,
});

describe('isMessageFromBot', () => {
Comment thread
lucas-a-pelegrino marked this conversation as resolved.
const mockMessage = createFakeMessage();

beforeEach(() => {
modelsMock.Users.isUserInRole.reset();
});

it('Should return true if user has bot role', async () => {
modelsMock.Users.isUserInRole.resolves(true);
const result = await isMessageFromBot(mockMessage);
expect(result).to.be.true;
});

it('Should return false if user does not have bot role', async () => {
modelsMock.Users.isUserInRole.resolves(false);
const result = await isMessageFromBot(mockMessage);
expect(result).to.be.false;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { describe, it, beforeEach } from 'mocha';
import proxyquire from 'proxyquire';
import Sinon from 'sinon';

import { createFakeMessage, createFakeUser } from '../../../../mocks/data';

const models = {
LivechatContacts: { isContactActiveOnPeriod: Sinon.stub(), markContactActiveForPeriod: Sinon.stub() },
LivechatInquiry: { markInquiryActiveForPeriod: Sinon.stub() },
Expand All @@ -13,10 +15,18 @@ const models = {
},
};

const { markRoomResponded } = proxyquire.load('../../../../../app/livechat/server/hooks/markRoomResponded.ts', {
const settingsGetMock = {
get: Sinon.stub(),
};

const isMessageFromBotMock = { isMessageFromBot: Sinon.stub() };

const { markRoomResponded } = proxyquire.noCallThru().load('../../../../../app/livechat/server/hooks/markRoomResponded.ts', {
'../../../../lib/callbacks': { callbacks: { add: Sinon.stub(), priority: { HIGH: 'high' } } },
'../../../lib/server/lib/notifyListener': { notifyOnLivechatInquiryChanged: Sinon.stub() },
'@rocket.chat/models': models,
'../../../settings/server': { settings: settingsGetMock },
'../lib/isMessageFromBot': isMessageFromBotMock,
});

describe('markRoomResponded', () => {
Expand All @@ -27,6 +37,8 @@ describe('markRoomResponded', () => {
models.LivechatRooms.getVisitorActiveForPeriodUpdateQuery.reset();
models.LivechatRooms.getAgentLastMessageTsUpdateQuery.reset();
models.LivechatRooms.getResponseByRoomIdUpdateQuery.reset();

settingsGetMock.get.reset();
});

it('should return void if message is system message', async () => {
Expand Down Expand Up @@ -66,6 +78,21 @@ describe('markRoomResponded', () => {
expect(res).to.be.undefined;
});

it('should return void if message is from bot and setting is enabled', async () => {
Comment thread
lucas-a-pelegrino marked this conversation as resolved.
settingsGetMock.get.withArgs('Omnichannel_Metrics_Ignore_Automatic_Messages').resolves(true);

const user = createFakeUser({ roles: ['bot'] });

isMessageFromBotMock.isMessageFromBot.resolves(user);

const message = createFakeMessage();
const room = {};

const res = await markRoomResponded(message, room, user);

expect(res).to.be.undefined;
});

it('should try to mark visitor as active for current period', async () => {
const message = {};
const room = { v: { _id: '1234' } };
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -3393,6 +3393,7 @@
"Omnichannel_allow_force_close_conversations": "Force close conversation API",
"Omnichannel_allow_force_close_conversations_Description": "Allow agents and managers to force close conversations via API.",
"Omnichannel_allow_force_close_conversations_alert": "Only enable if your workspace has issues with rooms with invalid states.",
"Omnichannel_Ignore_automatic_responses_for_performance_metrics": "Ignore bots activities for performance metrics",
"Livechat_Block_Unknown_Contacts": "Block unknown contacts",
"Livechat_Block_Unknown_Contacts_Description": "Conversations from people who are not on the contact list will not be able to be taken.",
"Livechat_Block_Unverified_Contacts": "Block unverified contacts",
Expand Down