Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 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": patch
"@rocket.chat/i18n": patch
Comment thread
lucas-a-pelegrino marked this conversation as resolved.
Outdated
---

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
12 changes: 12 additions & 0 deletions apps/meteor/app/livechat/server/lib/isMessageFromBot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { IMessage, IUser } 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.
const user = await Users.findOneById<Pick<IUser, 'roles'>>(message.u._id, { projection: { roles: 1 } });

if (!user) {
throw new Error('User not found');
}

return !!user.roles.includes('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 @@ -673,6 +673,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
40 changes: 36 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 @@ -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,6 +932,7 @@ describe('LIVECHAT - dashboards', function () {
before(async () => {
agent = await createAnOnlineAgent();
forwardAgent = await createAnOnlineAgent();
botAgent = await createBotAgent();
});

after(async () => Promise.all([deleteUser(agent.user), deleteUser(forwardAgent.user)]));
Expand Down Expand Up @@ -984,6 +986,36 @@ 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 () => {
await updateSetting('Omnichannel_Metrics_Ignore_Automatic_Messages', true);
Comment thread
lucas-a-pelegrino marked this conversation as resolved.
Outdated

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 +1316,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,43 @@
import { expect } from 'chai';
import p from 'proxyquire';
import sinon from 'sinon';

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

const modelsMock = {
Users: {
findOneById: 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 mockUser = createFakeUser({ roles: ['bot'] });
const mockMessage = createFakeMessage();

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

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

it('Should return false if user does not have bot role', async () => {
mockUser.roles = ['user'];
modelsMock.Users.findOneById.resolves(mockUser);
const result = await isMessageFromBot(mockMessage);
expect(result).to.be.false;
});

it('Should throw error when user is not found', async () => {
modelsMock.Users.findOneById.resolves(null);

await expect(isMessageFromBot(mockMessage)).to.be.rejectedWith(Error, 'User not found');
});
});
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 = {
LivechatVisitors: { isVisitorActiveOnPeriod: Sinon.stub(), markVisitorActiveForPeriod: 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
10 changes: 3 additions & 7 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,8 @@
"Agree": "Agree",
"AI_Actions": "AI actions",
"AirGapped_Restriction_Warning": "**Your air-gapped workspace will enter read-only mode in {{remainingDays}} days.** \n Users will still be able to access rooms and read existing messages but will be unable to send new messages. \n Reconnect it to the internet or [upgrade to a premium license](https://go.rocket.chat/i/air-gapped) to prevent this.",
"Airgapped_workspace_warning": "This air-gapped workspace will enter read-only mode in {{remainingDays}} days. <1>Connect it to the internet or upgrade to a premium plan to prevent this.</1>",
"Airgapped_workspace_restriction": "This air-gapped workspace is in read-only mode. <1>Connect it to the internet or upgrade to a premium plan to restore full functionality.</1>",
"Airgapped_workspace_warning": "This air-gapped workspace will enter read-only mode in {{remainingDays}} days. <2>Connect it to the internet or upgrade to a premium plan to prevent this.</1>",
"Airgapped_workspace_restriction": "This air-gapped workspace is in read-only mode. <2>Connect it to the internet or upgrade to a premium plan to restore full functionality.</1>",
Comment thread
lucas-a-pelegrino marked this conversation as resolved.
Outdated
"Alerts": "Alerts",
"Alias": "Alias",
"Alias_Format": "Alias Format",
Expand Down Expand Up @@ -3374,9 +3374,7 @@
"Omnichannel_placed_chat_on_hold": "Chat On Hold: {{comment}}",
"Omnichannel_hide_conversation_after_closing": "Hide conversation after closing",
"Omnichannel_hide_conversation_after_closing_description": "After closing the conversation you will be redirected to Home.",
"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.",
Comment thread
lucas-a-pelegrino marked this conversation as resolved.
"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 Expand Up @@ -5519,9 +5517,7 @@
"This_is_a_desktop_notification": "This is a desktop notification",
"This_is_a_deprecated_feature_alert": "This is a deprecated feature. It may not work as expected and will not get new updates.",
"Zapier_integration_has_been_deprecated": "The Zapier integration has been deprecated, may not work as expected and will not receive updates",
"Zapier_integration_is_not_available": "The Zapier integration has been deprecated and is no longer available for new Rocket.Chat workspaces",
"Install_Zapier_from_marketplace": "Install the Zapier app from Marketplace to avoid disruptions",
"Install_Zapier_from_marketplace_new_workspaces": "Install the Zapier app from Marketplace to configure new integrations",
Comment thread
lucas-a-pelegrino marked this conversation as resolved.
"Input": "Input",
"This_is_a_push_test_messsage": "This is a push test message",
"This_message_was_rejected_by__peer__peer": "This message was rejected by <em>{{peer}}</em> peer.",
Expand Down