Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: First response time livechat metrics are associated with the last agent who served the room #34156

Merged
merged 11 commits into from
Dec 16, 2024
Merged
6 changes: 6 additions & 0 deletions .changeset/fair-carrots-trade.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion apps/meteor/server/models/raw/LivechatRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
}

Expand Down
26 changes: 13 additions & 13 deletions apps/meteor/server/services/omnichannel-analytics/AgentData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand All @@ -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(),
Expand All @@ -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);
}
}
});
Expand Down
128 changes: 121 additions & 7 deletions apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,18 +922,18 @@ 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;
const secondDelayInSeconds = 8;

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 });
Expand Down Expand Up @@ -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
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
.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;
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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: '' },
];

Expand Down
Loading
Loading