fix: First response time livechat metrics are associated with the last agent who served the room (#34156)
Some checks are pending
Deploy GitHub Pages / deploy-preview (push) Waiting to run
CI / ⚙️ Variables Setup (push) Waiting to run
CI / 🚀 Notify external services - draft (push) Blocked by required conditions
CI / 📦 Build Packages (push) Blocked by required conditions
CI / deploy-preview (push) Blocked by required conditions
CI / 📦 Meteor Build - coverage (push) Blocked by required conditions
CI / 📦 Meteor Build - official (push) Blocked by required conditions
CI / Builds matrix rust bindings against alpine (push) Waiting to run
CI / 🚢 Build Docker Images for Testing (alpine) (push) Blocked by required conditions
CI / 🚢 Build Docker Images for Production (alpine) (push) Blocked by required conditions
CI / 🔎 Code Check (push) Blocked by required conditions
CI / 🔨 Test Unit (push) Blocked by required conditions
CI / 🔨 Test API (CE) (push) Blocked by required conditions
CI / 🔨 Test UI (CE) (push) Blocked by required conditions
CI / 🔨 Test API (EE) (push) Blocked by required conditions
CI / 🔨 Test UI (EE) (push) Blocked by required conditions
CI / ✅ Tests Done (push) Blocked by required conditions
CI / 🚀 Publish build assets (push) Blocked by required conditions
CI / 🚀 Publish Docker Image (main) (alpine) (push) Blocked by required conditions
CI / 🚀 Publish Docker Image (services) (account) (push) Blocked by required conditions
CI / 🚀 Publish Docker Image (services) (authorization) (push) Blocked by required conditions
CI / 🚀 Publish Docker Image (services) (ddp-streamer) (push) Blocked by required conditions
CI / 🚀 Publish Docker Image (services) (omnichannel-transcript) (push) Blocked by required conditions
CI / 🚀 Publish Docker Image (services) (presence) (push) Blocked by required conditions
CI / 🚀 Publish Docker Image (services) (queue-worker) (push) Blocked by required conditions
CI / 🚀 Publish Docker Image (services) (stream-hub) (push) Blocked by required conditions
CI / 🚀 Notify external services (push) Blocked by required conditions
CI / trigger-dependent-workflows (push) Blocked by required conditions
CI / Update Version Durability (push) Blocked by required conditions
Code scanning - action / CodeQL-Build (push) Waiting to run

This commit is contained in:
Matheus Barbosa Silva 2024-12-16 11:21:11 -03:00 committed by GitHub
parent b32c629765
commit 47f24c2fb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 395 additions and 44 deletions

View File

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
---
Fixes "Average first response time" and "Best first response time" metrics being associated with the last agent who served the room (instead of the first one)

View File

@ -2147,7 +2147,7 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
};
return this.find(query, {
projection: { ts: 1, departmentId: 1, open: 1, servedBy: 1, metrics: 1, msgs: 1 },
projection: { ts: 1, departmentId: 1, open: 1, servedBy: 1, responseBy: 1, metrics: 1, msgs: 1 },
});
}

View File

@ -235,15 +235,15 @@ export class AgentOverviewData {
data: [],
};
await this.roomsModel.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => {
if (servedBy && metrics && metrics.response && metrics.response.ft) {
if (agentAvgRespTime.has(servedBy.username)) {
agentAvgRespTime.set(servedBy.username, {
frt: agentAvgRespTime.get(servedBy.username).frt + metrics.response.ft,
total: agentAvgRespTime.get(servedBy.username).total + 1,
await this.roomsModel.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, responseBy }) => {
if (responseBy && metrics && metrics.response && metrics.response.ft) {
if (agentAvgRespTime.has(responseBy.username)) {
agentAvgRespTime.set(responseBy.username, {
frt: agentAvgRespTime.get(responseBy.username).frt + metrics.response.ft,
total: agentAvgRespTime.get(responseBy.username).total + 1,
});
} else {
agentAvgRespTime.set(servedBy.username, {
agentAvgRespTime.set(responseBy.username, {
frt: metrics.response.ft,
total: 1,
});
@ -267,7 +267,7 @@ export class AgentOverviewData {
}
async Best_first_response_time(from: moment.Moment, to: moment.Moment, departmentId?: string, extraQuery: Filter<IOmnichannelRoom> = {}) {
const agentFirstRespTime = new Map(); // stores avg response time for each agent
const agentFirstRespTime = new Map(); // stores best response time for each agent
const date = {
gte: from.toDate(),
lte: to.toDate(),
@ -285,12 +285,12 @@ export class AgentOverviewData {
data: [],
};
await this.roomsModel.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => {
if (servedBy && metrics && metrics.response && metrics.response.ft) {
if (agentFirstRespTime.has(servedBy.username)) {
agentFirstRespTime.set(servedBy.username, Math.min(agentFirstRespTime.get(servedBy.username), metrics.response.ft));
await this.roomsModel.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, responseBy }) => {
if (responseBy && metrics && metrics.response && metrics.response.ft) {
if (agentFirstRespTime.has(responseBy.username)) {
agentFirstRespTime.set(responseBy.username, Math.min(agentFirstRespTime.get(responseBy.username), metrics.response.ft));
} else {
agentFirstRespTime.set(servedBy.username, metrics.response.ft);
agentFirstRespTime.set(responseBy.username, metrics.response.ft);
}
}
});

View File

@ -922,6 +922,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 originalFirstResponseTimeInSeconds: number;
let roomId: string;
const firstDelayInSeconds = 4;
@ -929,11 +930,10 @@ describe('LIVECHAT - dashboards', function () {
before(async () => {
agent = await createAnOnlineAgent();
forwardAgent = await createAnOnlineAgent();
});
after(async () => {
await deleteUser(agent.user);
});
after(async () => Promise.all([deleteUser(agent.user), deleteUser(forwardAgent.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 });
@ -984,6 +984,62 @@ describe('LIVECHAT - dashboards', function () {
expect(originalFirstResponseTimeInSeconds).to.be.greaterThanOrEqual(firstDelayInSeconds);
});
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;
await sendAgentMessage(roomId, 'first response from agent', forwardAgent.credentials);
await request
.post(api('livechat/room.forward'))
.set(credentials)
.send({
roomId,
userId: agent.user._id,
comment: 'test comment',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
await sendAgentMessage(roomId, 'first response from forwarded 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');
// The agent to whom the room has been forwarded shouldn't have their average first response time changed
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(originalFirstResponseTimeInSeconds).to.be.equal(averageFirstResponseTimeInSeconds);
// A room's first response time should be attached to the agent who first responded to it even if it has been forwarded
const forwardAgentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === forwardAgent.user.username,
);
expect(forwardAgentData).to.not.be.undefined;
expect(forwardAgentData).to.have.property('name', forwardAgent.user.username);
expect(forwardAgentData).to.have.property('value');
const forwardAgentAverageFirstResponseTimeInSeconds = moment.duration(forwardAgentData.value).asSeconds();
expect(originalFirstResponseTimeInSeconds).to.be.greaterThan(forwardAgentAverageFirstResponseTimeInSeconds);
});
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;
@ -1019,14 +1075,16 @@ describe('LIVECHAT - dashboards', function () {
describe('[livechat/analytics/agent-overview] - Best first response time', () => {
let agent: { credentials: Credentials; user: IUser & { username: string } };
let forwardAgent: { credentials: Credentials; user: IUser & { username: string } };
let originalBestFirstResponseTimeInSeconds: number;
let roomId: string;
before(async () => {
agent = await createAnOnlineAgent();
forwardAgent = await createAnOnlineAgent();
});
after(() => deleteUser(agent.user));
after(() => Promise.all([deleteUser(agent.user), deleteUser(forwardAgent.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 });
@ -1110,6 +1168,62 @@ describe('LIVECHAT - dashboards', function () {
const bestFirstResponseTimeInSeconds = moment.duration(agentData.value).asSeconds();
expect(bestFirstResponseTimeInSeconds).to.be.equal(originalBestFirstResponseTimeInSeconds);
});
it('should correctly associate best first response time to the first agent who responded the room', async () => {
const response = await startANewLivechatRoomAndTakeIt({ agent: forwardAgent.credentials });
roomId = response.room._id;
await sendAgentMessage(roomId, 'first response from agent', forwardAgent.credentials);
await request
.post(api('livechat/room.forward'))
.set(credentials)
.send({
roomId,
userId: agent.user._id,
comment: 'test comment',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
await sendAgentMessage(roomId, 'first response from forwarded 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');
// The agent to whom the room has been forwarded shouldn't have their best first response time changed
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);
// A room's first response time should be attached to the agent who first responded to it even if it has been forwarded
const forwardAgentData = result.body.data.find(
(agentOverviewData: { name: string; value: string }) => agentOverviewData.name === forwardAgent.user.username,
);
expect(forwardAgentData).to.not.be.undefined;
expect(forwardAgentData).to.have.property('name', forwardAgent.user.username);
expect(forwardAgentData).to.have.property('value');
const forwardAgentBestFirstResponseTimeInSeconds = moment.duration(forwardAgentData.value).asSeconds();
expect(forwardAgentBestFirstResponseTimeInSeconds).to.be.lessThan(originalBestFirstResponseTimeInSeconds);
});
});
describe('livechat/analytics/overview', () => {
@ -1170,12 +1284,12 @@ describe('LIVECHAT - dashboards', function () {
expect(result.body).to.be.an('array');
const expectedResult = [
{ title: 'Total_conversations', value: 13 },
{ title: 'Open_conversations', value: 10 },
{ title: 'Total_conversations', value: 15 },
{ title: 'Open_conversations', value: 12 },
{ title: 'On_Hold_conversations', value: 1 },
// { title: 'Total_messages', value: 6 },
// { title: 'Busiest_day', value: moment().format('dddd') },
{ title: 'Conversations_per_day', value: '6.50' },
{ title: 'Conversations_per_day', value: '7.50' },
// { title: 'Busiest_time', value: '' },
];

View File

@ -735,7 +735,7 @@ describe('AgentData Analytics', () => {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
servedBy: {
responseBy: {
username: 'agent 1',
},
metrics: {
@ -772,7 +772,7 @@ describe('AgentData Analytics', () => {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
servedBy: {
responseBy: {
username: 'agent 1',
},
metrics: {
@ -782,7 +782,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 2',
},
metrics: {
@ -818,12 +818,15 @@ describe('AgentData Analytics', () => {
],
});
});
it('should calculate correctly when agents have multiple conversations', async () => {
it('should associate average first response time with the agent who first responded to the room', async () => {
const modelMock = {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
servedBy: {
username: 'agent 3',
},
responseBy: {
username: 'agent 1',
},
metrics: {
@ -834,6 +837,9 @@ describe('AgentData Analytics', () => {
},
{
servedBy: {
username: 'agent 4',
},
responseBy: {
username: 'agent 2',
},
metrics: {
@ -844,6 +850,9 @@ describe('AgentData Analytics', () => {
},
{
servedBy: {
username: 'agent 5',
},
responseBy: {
username: 'agent 1',
},
metrics: {
@ -879,12 +888,106 @@ describe('AgentData Analytics', () => {
],
});
});
it('should ignore conversations not being served by any agent', async () => {
it('should calculate correctly when agents have multiple conversations', async () => {
const modelMock = {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
servedBy: undefined,
responseBy: {
username: 'agent 1',
},
metrics: {
response: {
ft: 100,
},
},
},
{
responseBy: {
username: 'agent 2',
},
metrics: {
response: {
ft: 200,
},
},
},
{
responseBy: {
username: 'agent 1',
},
metrics: {
response: {
ft: 200,
},
},
},
];
},
};
const agentOverview = new AgentOverviewData(modelMock as any);
const result = await agentOverview.Avg_first_response_time(moment(), moment(), 'departmentId');
expect(result).to.be.deep.equal({
data: [
{
name: 'agent 1',
value: '00:02:30',
},
{
name: 'agent 2',
value: '00:03:20',
},
],
head: [
{
name: 'Agent',
},
{ name: 'Avg_first_response_time' },
],
});
});
it('should ignore conversations not responded by any agent', async () => {
const modelMock = {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
responseBy: undefined,
metrics: {
response: {
ft: 100,
},
},
},
];
},
};
const agentOverview = new AgentOverviewData(modelMock as any);
const result = await agentOverview.Avg_first_response_time(moment(), moment(), 'departmentId');
expect(result).to.be.deep.equal({
data: [],
head: [
{
name: 'Agent',
},
{ name: 'Avg_first_response_time' },
],
});
});
it('should ignore conversations served, but not responded by any agent', async () => {
const modelMock = {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
servedBy: {
username: 'agent 1',
},
responseBy: undefined,
metrics: {
response: {
ft: 100,
@ -914,7 +1017,7 @@ describe('AgentData Analytics', () => {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
servedBy: {
responseBy: {
username: 'agent 1',
},
metrics: undefined,
@ -966,7 +1069,7 @@ describe('AgentData Analytics', () => {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
servedBy: {
responseBy: {
username: 'agent 1',
},
metrics: {
@ -976,7 +1079,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 2',
},
metrics: {
@ -986,7 +1089,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 3',
},
metrics: {
@ -996,7 +1099,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 4',
},
metrics: {
@ -1006,7 +1109,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 5',
},
metrics: {
@ -1016,9 +1119,116 @@ describe('AgentData Analytics', () => {
},
},
{
responseBy: {
username: 'agent 6',
},
metrics: {
response: {
ft: 300,
},
},
},
];
},
};
const agentOverview = new AgentOverviewData(modelMock as any);
const result = await agentOverview.Best_first_response_time(moment(), moment(), 'departmentId');
expect(result).to.be.deep.equal({
data: [
{ name: 'agent 1', value: '00:01:40' },
{ name: 'agent 2', value: '00:03:20' },
{ name: 'agent 3', value: '00:00:50' },
{ name: 'agent 4', value: '00:02:30' },
{ name: 'agent 5', value: '00:04:10' },
{ name: 'agent 6', value: '00:05:00' },
],
head: [
{
name: 'Agent',
},
{ name: 'Best_first_response_time' },
],
});
});
it('should associate best first response time with the agent who first responded to the room', async () => {
const modelMock = {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
responseBy: {
username: 'agent 1',
},
servedBy: {
username: 'agent 2',
},
metrics: {
response: {
ft: 100,
},
},
},
{
responseBy: {
username: 'agent 2',
},
servedBy: {
username: 'agent 3',
},
metrics: {
response: {
ft: 200,
},
},
},
{
responseBy: {
username: 'agent 3',
},
servedBy: {
username: 'agent 4',
},
metrics: {
response: {
ft: 50,
},
},
},
{
responseBy: {
username: 'agent 4',
},
servedBy: {
username: 'agent 5',
},
metrics: {
response: {
ft: 150,
},
},
},
{
responseBy: {
username: 'agent 5',
},
servedBy: {
username: 'agent 6',
},
metrics: {
response: {
ft: 250,
},
},
},
{
responseBy: {
username: 'agent 6',
},
servedBy: {
username: 'agent 7',
},
metrics: {
response: {
ft: 300,
@ -1055,7 +1265,7 @@ describe('AgentData Analytics', () => {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [
{
servedBy: {
responseBy: {
username: 'agent 1',
},
metrics: {
@ -1065,7 +1275,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 2',
},
metrics: {
@ -1075,7 +1285,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 3',
},
metrics: {
@ -1085,7 +1295,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 4',
},
metrics: {
@ -1095,7 +1305,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 5',
},
metrics: {
@ -1105,7 +1315,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 6',
},
metrics: {
@ -1115,7 +1325,7 @@ describe('AgentData Analytics', () => {
},
},
{
servedBy: {
responseBy: {
username: 'agent 1',
},
metrics: {
@ -1149,10 +1359,31 @@ describe('AgentData Analytics', () => {
],
});
});
it('should ignore conversations not being served by any agent', async () => {
it('should ignore conversations not responded by any agent', async () => {
const modelMock = {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [{ servedBy: undefined, metrics: { response: { ft: 100 } } }];
return [{ responseBy: undefined, metrics: { response: { ft: 100 } } }];
},
};
const agentOverview = new AgentOverviewData(modelMock as any);
const result = await agentOverview.Best_first_response_time(moment(), moment(), 'departmentId');
expect(result).to.be.deep.equal({
data: [],
head: [
{
name: 'Agent',
},
{ name: 'Best_first_response_time' },
],
});
});
it('should ignore conversations served, but not responded by any agent', async () => {
const modelMock = {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [{ servedBy: { username: 'agent1' }, responseBy: undefined, metrics: { response: { ft: 100 } } }];
},
};
@ -1173,7 +1404,7 @@ describe('AgentData Analytics', () => {
it('should ignore conversations with no metrics', async () => {
const modelMock = {
getAnalyticsMetricsBetweenDate(_params: ILivechatRoomsModel['getAnalyticsMetricsBetweenDate']) {
return [{ servedBy: { username: 'agent 1' }, metrics: undefined }];
return [{ responseBy: { username: 'agent 1' }, metrics: undefined }];
},
};

View File

@ -238,7 +238,7 @@ export interface ILivechatRoomsModel extends IBaseModel<IOmnichannelRoom> {
date: { gte: Date; lte: Date },
data?: { departmentId?: string },
extraQuery?: Filter<IOmnichannelRoom>,
): FindCursor<Pick<IOmnichannelRoom, 'ts' | 'departmentId' | 'open' | 'servedBy' | 'metrics' | 'msgs'>>;
): FindCursor<Pick<IOmnichannelRoom, 'ts' | 'departmentId' | 'open' | 'servedBy' | 'responseBy' | 'metrics' | 'msgs'>>;
getAnalyticsMetricsBetweenDateWithMessages(
t: string,
date: { gte: Date; lte: Date },