Skip to content

Commit

Permalink
fix: Livechat analytics in a given date range consider conversation d…
Browse files Browse the repository at this point in the history
…ata from the following day (#33054)
  • Loading branch information
matheusbsilva137 committed Aug 22, 2024
1 parent be5d153 commit a14c067
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 31 deletions.
6 changes: 6 additions & 0 deletions .changeset/strong-terms-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
---

Fixed issue with livechat analytics in a given date range considering conversation data from the following day
16 changes: 8 additions & 8 deletions apps/meteor/server/models/raw/LivechatRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2046,12 +2046,12 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
return updater;
}

getTotalConversationsBetweenDate(t: 'l', date: { gte: Date; lt: Date }, { departmentId }: { departmentId?: string } = {}) {
getTotalConversationsBetweenDate(t: 'l', date: { gte: Date; lte: Date }, { departmentId }: { departmentId?: string } = {}) {
const query: Filter<IOmnichannelRoom> = {
t,
ts: {
$gte: new Date(date.gte), // ISO Date, ts >= date.gte
$lt: new Date(date.lt), // ISODate, ts < date.lt
$lte: new Date(date.lte), // ISODate, ts <= date.lte
},
...(departmentId && departmentId !== 'undefined' && { departmentId }),
};
Expand All @@ -2061,15 +2061,15 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive

getAnalyticsMetricsBetweenDate(
t: 'l',
date: { gte: Date; lt: Date },
date: { gte: Date; lte: Date },
{ departmentId }: { departmentId?: string } = {},
extraQuery: Document = {},
) {
const query: Filter<IOmnichannelRoom> = {
t,
ts: {
$gte: new Date(date.gte), // ISO Date, ts >= date.gte
$lt: new Date(date.lt), // ISODate, ts < date.lt
$lte: new Date(date.lte), // ISODate, ts <= date.lte
},
...(departmentId && departmentId !== 'undefined' && { departmentId }),
...extraQuery,
Expand All @@ -2082,7 +2082,7 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive

getAnalyticsMetricsBetweenDateWithMessages(
t: string,
date: { gte: Date; lt: Date },
date: { gte: Date; lte: Date },
{ departmentId }: { departmentId?: string } = {},
extraQuery: Document = {},
extraMatchers: Document = {},
Expand All @@ -2094,7 +2094,7 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
t,
ts: {
$gte: new Date(date.gte), // ISO Date, ts >= date.gte
$lt: new Date(date.lt), // ISODate, ts < date.lt
$lte: new Date(date.lte), // ISODate, ts <= date.lte
},
...(departmentId && departmentId !== 'undefined' && { departmentId }),
...extraMatchers,
Expand Down Expand Up @@ -2164,15 +2164,15 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
);
}

getAnalyticsBetweenDate(date: { gte: Date; lt: Date }, { departmentId }: { departmentId?: string } = {}) {
getAnalyticsBetweenDate(date: { gte: Date; lte: Date }, { departmentId }: { departmentId?: string } = {}) {
return this.col.aggregate<Pick<IOmnichannelRoom, 'ts' | 'departmentId' | 'open' | 'servedBy' | 'metrics' | 'msgs' | 'onHold'>>(
[
{
$match: {
t: 'l',
ts: {
$gte: new Date(date.gte), // ISO Date, ts >= date.gte
$lt: new Date(date.lt), // ISODate, ts < date.lt
$lte: new Date(date.lte), // ISODate, ts <= date.lte
},
...(departmentId && departmentId !== 'undefined' && { departmentId }),
},
Expand Down
14 changes: 7 additions & 7 deletions apps/meteor/server/services/omnichannel-analytics/AgentData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class AgentOverviewData {
const agentConversations = new Map(); // stores total conversations for each agent
const date = {
gte: from.toDate(),
lt: to.add(1, 'days').toDate(),
lte: to.toDate(),
};

const data: ConversationData = {
Expand Down Expand Up @@ -128,7 +128,7 @@ export class AgentOverviewData {
const agentChatDurations = new Map(); // stores total conversations for each agent
const date = {
gte: from.toDate(),
lt: to.add(1, 'days').toDate(),
lte: to.toDate(),
};

const data: ConversationData = {
Expand Down Expand Up @@ -178,7 +178,7 @@ export class AgentOverviewData {
const agentMessages = new Map(); // stores total conversations for each agent
const date = {
gte: from.toDate(),
lt: to.add(1, 'days').toDate(),
lte: to.toDate(),
};

const data: ConversationData = {
Expand Down Expand Up @@ -220,7 +220,7 @@ export class AgentOverviewData {
const agentAvgRespTime = new Map(); // stores avg response time for each agent
const date = {
gte: from.toDate(),
lt: to.add(1, 'days').toDate(),
lte: to.toDate(),
};

const data: ConversationData = {
Expand Down Expand Up @@ -270,7 +270,7 @@ export class AgentOverviewData {
const agentFirstRespTime = new Map(); // stores avg response time for each agent
const date = {
gte: from.toDate(),
lt: to.add(1, 'days').toDate(),
lte: to.toDate(),
};

const data: ConversationData = {
Expand Down Expand Up @@ -312,7 +312,7 @@ export class AgentOverviewData {
const agentAvgRespTime = new Map(); // stores avg response time for each agent
const date = {
gte: from.toDate(),
lt: to.add(1, 'days').toDate(),
lte: to.toDate(),
};

const data: ConversationData = {
Expand Down Expand Up @@ -362,7 +362,7 @@ export class AgentOverviewData {
const agentAvgReactionTime = new Map(); // stores avg reaction time for each agent
const date = {
gte: from.toDate(),
lt: to.add(1, 'days').toDate(),
lte: to.toDate(),
};

const data: ConversationData = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type ChartDataValidActions =

type DateParam = {
gte: Date;
lt: Date;
lte: Date;
};

export class ChartData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class OverviewData {

const date = {
gte: moment.tz(from, timezone).startOf('day').utc(),
lt: moment.tz(to, timezone).endOf('day').utc(),
lte: moment.tz(to, timezone).endOf('day').utc(),
};

// @ts-expect-error - Check extraquery usage on this func
Expand Down Expand Up @@ -181,7 +181,7 @@ export class OverviewData {

const date = {
gte: from.toDate(),
lt: to.add(1, 'days').toDate(),
lte: to.toDate(),
};

await this.roomsModel.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => {
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/server/services/omnichannel-analytics/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,13 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements
const hour = parseInt(m.add(currentHour ? 1 : 0, 'hour').format('H'));
const label = {
from: moment.utc().set({ hour }).tz(timezone).format('hA'),
to: moment.utc().set({ hour }).add(1, 'hour').tz(timezone).format('hA'),
to: moment.utc().set({ hour }).endOf('hour').tz(timezone).format('hA'),
};
data.dataLabels.push(`${label.from}-${label.to}`);

const date = {
gte: m.toDate(),
lt: moment(m).add(1, 'hours').toDate(),
lte: moment(m).endOf('hour').toDate(),
};

data.dataPoints.push(await this.chart.callAction(chartLabel, date, departmentId, extraQuery));
Expand All @@ -128,7 +128,7 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements

const date = {
gte: m.toDate(),
lt: moment(m).add(1, 'days').toDate(),
lte: moment(m).endOf('day').toDate(),
};

data.dataPoints.push(await this.chart.callAction(chartLabel, date, departmentId, extraQuery));
Expand Down
196 changes: 196 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,147 @@ describe('LIVECHAT - dashboards', function () {
expect(user1Data).to.have.property('value', '28.57%');
expect(user2Data).to.have.property('value', '71.43%');
});
(IS_EE ? it : it.skip)('should only return results in the provided date interval when searching for total conversations', async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');

const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Total_conversations', departmentId: department._id })
.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.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', '%_of_conversations');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
});
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for average chat durations',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');

const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Avg_chat_duration', departmentId: department._id })
.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.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Avg_chat_duration');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
(IS_EE ? it : it.skip)('should only return results in the provided date interval when searching for total messages', async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');

const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Total_messages', departmentId: department._id })
.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.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Total_messages');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
});
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for average first response times',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');

const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Avg_first_response_time', departmentId: department._id })
.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.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Avg_first_response_time');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for best first response times',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');

const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Best_first_response_time', departmentId: department._id })
.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.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Best_first_response_time');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for average response times',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');

const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Avg_response_time', departmentId: department._id })
.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.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Avg_response_time');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
(IS_EE ? it : it.skip)(
'should only return results in the provided date interval when searching for average reaction times',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');

const result = await request
.get(api('livechat/analytics/agent-overview'))
.query({ from: yesterday, to: yesterday, name: 'Avg_reaction_time', departmentId: department._id })
.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.head).to.be.an('array').with.lengthOf(2);
expect(result.body.head[0]).to.have.property('name', 'Agent');
expect(result.body.head[1]).to.have.property('name', 'Avg_reaction_time');
expect(result.body).to.have.property('data');
expect(result.body.data).to.be.an('array').that.is.empty;
},
);
});

describe('[livechat/analytics/agent-overview] - Average first response time', () => {
Expand Down Expand Up @@ -1050,5 +1191,60 @@ describe('LIVECHAT - dashboards', function () {
const totalMessagesValue = parseInt(totalMessages.value);
expect(totalMessagesValue).to.be.greaterThanOrEqual(minMessages);
});
(IS_EE ? it : it.skip)(
'should only consider conversations in the provided time range when returning analytics conversations overview data',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');

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

expect(result.body).to.be.an('array');

const expectedResult = [
{ title: 'Total_conversations', value: 0 },
{ title: 'Open_conversations', value: 0 },
{ title: 'On_Hold_conversations', value: 0 },
{ title: 'Conversations_per_day', value: '0.00' },
];

expectedResult.forEach((expected) => {
const resultItem = result.body.find((item: any) => item.title === expected.title);
expect(resultItem).to.not.be.undefined;
expect(resultItem).to.have.property('value', expected.value);
});
},
);
(IS_EE ? it : it.skip)(
'should only consider conversations in the provided time range when returning analytics productivity overview data',
async () => {
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');

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

expect(result.body).to.be.an('array');

const expectedResult = [
{ title: 'Avg_response_time', value: '00:00:00' },
{ title: 'Avg_first_response_time', value: '00:00:00' },
{ title: 'Avg_reaction_time', value: '00:00:00' },
];

expectedResult.forEach((expected) => {
const resultItem = result.body.find((item: any) => item.title === expected.title);
expect(resultItem).to.not.be.undefined;
expect(resultItem).to.have.property('value', expected.value);
});
},
);
});
});
Loading

0 comments on commit a14c067

Please sign in to comment.