Skip to content

Commit

Permalink
chore: saveAnalyticsDataByRoomId with updater
Browse files Browse the repository at this point in the history
  • Loading branch information
ricardogarim authored and ggazzo committed Aug 2, 2024
1 parent f254e85 commit b2dcf34
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 104 deletions.
122 changes: 58 additions & 64 deletions apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,78 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isEditedMessage, isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';

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

const getMetricValue = <T>(metric: T | undefined, defaultValue: T): T => metric ?? defaultValue;
const calculateTimeDifference = <T extends Date | number>(startTime: T, now: Date): number =>
(now.getTime() - new Date(startTime).getTime()) / 1000;
const calculateAvgResponseTime = (totalResponseTime: number, newResponseTime: number, responseCount: number) =>
(totalResponseTime + newResponseTime) / (responseCount + 1);

const getFirstResponseAnalytics = (
visitorLastQuery: Date,
agentJoinTime: Date,
totalResponseTime: number,
responseCount: number,
now: Date,
) => {
const responseTime = calculateTimeDifference(visitorLastQuery, now);
const reactionTime = calculateTimeDifference(agentJoinTime, now);
const avgResponseTime = calculateAvgResponseTime(totalResponseTime, responseTime, responseCount);

return {
firstResponseDate: now,
firstResponseTime: responseTime,
responseTime,
avgResponseTime,
firstReactionDate: now,
firstReactionTime: reactionTime,
reactionTime,
};
};

const getSubsequentResponseAnalytics = (visitorLastQuery: Date, totalResponseTime: number, responseCount: number, now: Date) => {
const responseTime = calculateTimeDifference(visitorLastQuery, now);
const avgResponseTime = calculateAvgResponseTime(totalResponseTime, responseTime, responseCount);

return {
responseTime,
avgResponseTime,
reactionTime: responseTime,
};
};

const getAnalyticsData = (room: IOmnichannelRoom, now: Date): Record<string, string | number | Date> | undefined => {
const visitorLastQuery = getMetricValue(room.metrics?.v?.lq, room.ts);
const agentLastReply = getMetricValue(room.metrics?.servedBy?.lr, room.ts);
const agentJoinTime = getMetricValue(room.servedBy?.ts, room.ts);
const totalResponseTime = getMetricValue(room.metrics?.response?.tt, 0);
const responseCount = getMetricValue(room.metrics?.response?.total, 0);

if (agentLastReply === room.ts) {
return getFirstResponseAnalytics(visitorLastQuery, agentJoinTime, totalResponseTime, responseCount, now);
}
if (visitorLastQuery > agentLastReply) {
return getSubsequentResponseAnalytics(visitorLastQuery, totalResponseTime, responseCount, now);
}
};

callbacks.add(
'afterSaveMessage',
async (message, room) => {
// check if room is livechat
if (!isOmnichannelRoom(room)) {
return message;
}

// skips this callback if the message was edited
if (!message || isEditedMessage(message)) {
return message;
}

// if the message has a token, it was sent by the visitor
if (message.token) {
// When visitor sends a mesage, most metrics wont be calculated/served.
// But, v.lq (last query) will be updated to the message time. This has to be done
// As not doing it will cause the metrics to be crazy and not have real values.
await LivechatRooms.saveAnalyticsDataByRoomId(room, message);
if (!isOmnichannelRoom(room) || !message || isEditedMessage(message)) {
return message;
}

if (message.file) {
message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } };
}

const now = new Date();
let analyticsData;

const visitorLastQuery = room.metrics?.v ? room.metrics.v.lq : room.ts;
const agentLastReply = room.metrics?.servedBy ? room.metrics.servedBy.lr : room.ts;
const agentJoinTime = room.servedBy?.ts ? room.servedBy.ts : room.ts;

const isResponseTt = room.metrics?.response?.tt;
const isResponseTotal = room.metrics?.response?.total;

if (agentLastReply === room.ts) {
// first response
const firstResponseDate = now;
const firstResponseTime = (now.getTime() - new Date(visitorLastQuery).getTime()) / 1000;
const responseTime = (now.getTime() - new Date(visitorLastQuery).getTime()) / 1000;
const avgResponseTime =
((isResponseTt ? room.metrics?.response?.tt : 0) || 0 + responseTime) /
((isResponseTotal ? room.metrics?.response?.total : 0) || 0 + 1);

const firstReactionDate = now;
const firstReactionTime = (now.getTime() - new Date(agentJoinTime).getTime()) / 1000;
const reactionTime = (now.getTime() - new Date(agentJoinTime).getTime()) / 1000;

analyticsData = {
firstResponseDate,
firstResponseTime,
responseTime,
avgResponseTime,
firstReactionDate,
firstReactionTime,
reactionTime,
};
} else if (visitorLastQuery > agentLastReply) {
// response, not first
const responseTime = (now.getTime() - new Date(visitorLastQuery).getTime()) / 1000;
const avgResponseTime =
((isResponseTt ? room.metrics?.response?.tt : 0) || 0 + responseTime) /
((isResponseTotal ? room.metrics?.response?.total : 0) || 0 + 1);

const reactionTime = (now.getTime() - new Date(visitorLastQuery).getTime()) / 1000;

analyticsData = {
responseTime,
avgResponseTime,
reactionTime,
};
} // ignore, its continuing response

const analyticsData = getAnalyticsData(room, new Date());
await LivechatRooms.saveAnalyticsDataByRoomId(room, message, analyticsData);

return message;
},
callbacks.priority.LOW,
Expand Down
122 changes: 82 additions & 40 deletions apps/meteor/server/models/raw/LivechatRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import type {
ReportResult,
MACStats,
} from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';
import { isMessageFromVisitor, UserStatus } from '@rocket.chat/core-typings';
import type { ILivechatRoomsModel } from '@rocket.chat/model-typings';
import type { Updater } from '@rocket.chat/models';
import { Settings } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type {
Expand Down Expand Up @@ -2020,54 +2021,95 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
);
}

saveAnalyticsDataByRoomId(room: IOmnichannelRoom, message: IMessage, analyticsData: Record<string, string | number | Date>) {
const update: DeepWritable<UpdateFilter<IOmnichannelRoom>> = {
$set: {
...(analyticsData && {
'metrics.response.avg': analyticsData.avgResponseTime,
}),
...(analyticsData?.firstResponseTime && {
'metrics.reaction.fd': analyticsData.firstReactionDate,
'metrics.reaction.ft': analyticsData.firstReactionTime,
'metrics.response.fd': analyticsData.firstResponseDate,
'metrics.response.ft': analyticsData.firstResponseTime,
}),
},
...(analyticsData && {
$inc: {
'metrics.response.total': 1,
'metrics.response.tt': analyticsData.responseTime as number,
'metrics.reaction.tt': analyticsData.reactionTime as number,
},
}),
};
private getAnalyticsUpdateQuery(
analyticsData: Record<string, string | number | Date>,
updater: Updater<IOmnichannelRoom> = this.getUpdater(),
) {
if (analyticsData) {
updater.set('metrics.response.avg', analyticsData.avgResponseTime);
updater.inc('metrics.response.total', 1);
updater.inc('metrics.response.tt', analyticsData.responseTime as number);
updater.inc('metrics.reaction.tt', analyticsData.reactionTime as number);
}

if (analyticsData.firstResponseTime) {
updater.set('metrics.reaction.fd', analyticsData.firstReactionDate);
updater.set('metrics.reaction.ft', analyticsData.firstReactionTime);
updater.set('metrics.response.fd', analyticsData.firstResponseDate);
updater.set('metrics.response.ft', analyticsData.firstResponseTime);
}

return updater;
}

private getAnalyticsUpdateQueryBySentByAgent(
room: IOmnichannelRoom,
message: IMessage,
analyticsData: Record<string, string | number | Date>,
) {
const updater = this.getAnalyticsUpdateQuery(analyticsData);

// livechat analytics : update last message timestamps
const visitorLastQuery = room.metrics?.v ? room.metrics.v.lq : room.ts;
const agentLastReply = room.metrics?.servedBy ? room.metrics.servedBy.lr : room.ts;

if (message.token) {
// update visitor timestamp, only if its new inquiry and not continuing message
if (agentLastReply >= visitorLastQuery) {
// if first query, not continuing query from visitor
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
update.$set!['metrics.v.lq'] = message.ts;
}
} else if (visitorLastQuery > agentLastReply) {
// update agent timestamp, if first response, not continuing
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
update.$set!['metrics.servedBy.lr'] = message.ts;
if (visitorLastQuery > agentLastReply) {
updater.set('metrics.servedBy.lr', message.ts);
}

return this.updateOne(
{
_id: room._id,
t: 'l',
},
update,
);
return updater;
}

private getAnalyticsUpdateQueryBySentByVisitor(
room: IOmnichannelRoom,
message: IMessage,
analyticsData: Record<string, string | number | Date>,
) {
const updater = this.getAnalyticsUpdateQuery(analyticsData);

// livechat analytics : update last message timestamps
const visitorLastQuery = room.metrics?.v ? room.metrics.v.lq : room.ts;
const agentLastReply = room.metrics?.servedBy ? room.metrics.servedBy.lr : room.ts;

// update visitor timestamp, only if its new inquiry and not continuing message
if (agentLastReply >= visitorLastQuery) {
updater.set('metrics.v.lq', message.ts);
}

return updater;
}

private getAnalyticsUpdateQueryByRoomId(
room: IOmnichannelRoom,
message: IMessage,
analyticsData: Record<string, string | number | Date>,
) {
return isMessageFromVisitor(message)
? this.getAnalyticsUpdateQueryBySentByVisitor(room, message, analyticsData)
: this.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData);
}

async saveAnalyticsDataByRoomId(
room: IOmnichannelRoom,
message: IMessage,
analyticsData: Record<string, string | number | Date>,
): Promise<void> {
const updater = this.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData);
return updater.persist({ _id: room._id });
}

// saveAnalyticsDataByRoomIdAndLastMessageFromVisitor(
// room: IOmnichannelRoom,
// message: IMessage,
// analyticsData: Record<string, string | number | Date>,
// ) {
// const updater = this.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData);

// updater.set('v.lastMessageTs', message.ts);

// return updater.persist({ _id: room._id });
// }

getTotalConversationsBetweenDate(t: 'l', date: { gte: Date; lt: Date }, { departmentId }: { departmentId?: string } = {}) {
const query: Filter<IOmnichannelRoom> = {
t,
Expand Down

0 comments on commit b2dcf34

Please sign in to comment.