From fb026bbcfea1f7dfea88820be40ee17f64cfca0a Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Thu, 1 Aug 2024 22:41:20 -0300 Subject: [PATCH 1/6] chore: add fd property on room response and reaction model objects --- packages/core-typings/src/IRoom.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index ac31c2cc6a3a..92a2762993a7 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -275,9 +275,11 @@ export interface IOmnichannelRoom extends IOmnichannelGenericRoom { total: number; avg: number; ft: number; + fd?: number; }; reaction?: { ft: number; + fd?: number; }; }; From e5118adfd36cafb86946b9570877bb85d6132ac5 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Thu, 1 Aug 2024 23:41:29 -0300 Subject: [PATCH 2/6] chore: saveAnalyticsDataByRoomId with updater --- .../server/hooks/saveAnalyticsData.ts | 117 +++++++++-------- .../meteor/server/models/raw/LivechatRooms.ts | 122 ++++++++++++------ 2 files changed, 140 insertions(+), 99 deletions(-) diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index 7c7bff067f9a..356c2c23032f 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -1,79 +1,78 @@ -import { isEditedMessage, isMessageFromVisitor } from '@rocket.chat/core-typings'; +import { isEditedMessage } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom } 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 = (metric: T | undefined, defaultValue: T): T => metric ?? defaultValue; +const calculateTimeDifference = (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 | 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( 'afterOmnichannelSaveMessage', async (message, { room }) => { - // 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 (isMessageFromVisitor(message)) { - // 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); - 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, diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index 648af95ed180..44215999728a 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -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 { @@ -2020,54 +2021,95 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ); } - saveAnalyticsDataByRoomId(room: IOmnichannelRoom, message: IMessage, analyticsData: Record) { - const update: DeepWritable> = { - $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, + updater: Updater = 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, + ) { + 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, + ) { + 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, + ) { + return isMessageFromVisitor(message) + ? this.getAnalyticsUpdateQueryBySentByVisitor(room, message, analyticsData) + : this.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData); + } + + async saveAnalyticsDataByRoomId( + room: IOmnichannelRoom, + message: IMessage, + analyticsData: Record, + ): Promise { + const updater = this.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData); + return updater.persist({ _id: room._id }); + } + + // saveAnalyticsDataByRoomIdAndLastMessageFromVisitor( + // room: IOmnichannelRoom, + // message: IMessage, + // analyticsData: Record, + // ) { + // 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 = { t, From a329f876a254cc819cafe50158188d0bbf322666 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 2 Aug 2024 00:47:34 -0300 Subject: [PATCH 3/6] add reaction.tt --- packages/core-typings/src/IRoom.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 92a2762993a7..4bc07c5a8ad4 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -278,6 +278,7 @@ export interface IOmnichannelRoom extends IOmnichannelGenericRoom { fd?: number; }; reaction?: { + tt: number; ft: number; fd?: number; }; From 5cd073d3309e3de2a9f80db13f7a9280181211f6 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 2 Aug 2024 00:47:45 -0300 Subject: [PATCH 4/6] saveAnalyticsDataByRoomId return type temp --- packages/model-typings/src/models/ILivechatRoomsModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index d4da1d7d8159..c6a369e4caf1 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -212,7 +212,7 @@ export interface ILivechatRoomsModel extends IBaseModel { room: IOmnichannelRoom, message: IMessage, analyticsData?: Record, - ): Promise; + ): Promise; // Promise; getTotalConversationsBetweenDate(t: 'l', date: { gte: Date; lt: Date }, data?: { departmentId: string }): Promise; getAnalyticsMetricsBetweenDate( t: 'l', From 7108f917213ea1500128baa2cf1c7e1706689cc5 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 2 Aug 2024 11:36:24 -0300 Subject: [PATCH 5/6] fix type --- apps/meteor/server/models/raw/LivechatRooms.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index 44215999728a..68e95b45c96d 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -2022,7 +2022,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive } private getAnalyticsUpdateQuery( - analyticsData: Record, + analyticsData: Record | undefined, updater: Updater = this.getUpdater(), ) { if (analyticsData) { @@ -2032,7 +2032,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive updater.inc('metrics.reaction.tt', analyticsData.reactionTime as number); } - if (analyticsData.firstResponseTime) { + if (analyticsData?.firstResponseTime) { updater.set('metrics.reaction.fd', analyticsData.firstReactionDate); updater.set('metrics.reaction.ft', analyticsData.firstReactionTime); updater.set('metrics.response.fd', analyticsData.firstResponseDate); @@ -2045,7 +2045,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive private getAnalyticsUpdateQueryBySentByAgent( room: IOmnichannelRoom, message: IMessage, - analyticsData: Record, + analyticsData: Record | undefined, ) { const updater = this.getAnalyticsUpdateQuery(analyticsData); @@ -2063,7 +2063,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive private getAnalyticsUpdateQueryBySentByVisitor( room: IOmnichannelRoom, message: IMessage, - analyticsData: Record, + analyticsData: Record | undefined, ) { const updater = this.getAnalyticsUpdateQuery(analyticsData); @@ -2082,7 +2082,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive private getAnalyticsUpdateQueryByRoomId( room: IOmnichannelRoom, message: IMessage, - analyticsData: Record, + analyticsData: Record | undefined, ) { return isMessageFromVisitor(message) ? this.getAnalyticsUpdateQueryBySentByVisitor(room, message, analyticsData) @@ -2092,7 +2092,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive async saveAnalyticsDataByRoomId( room: IOmnichannelRoom, message: IMessage, - analyticsData: Record, + analyticsData?: Record, ): Promise { const updater = this.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData); return updater.persist({ _id: room._id }); From db5e20a5bb048ec6c342deea956f10b845372540 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 2 Aug 2024 15:44:56 -0300 Subject: [PATCH 6/6] saveAnalyticsDataByRoomId -> getAnalyticsUpdateQueryByRoomId --- .../server/hooks/saveAnalyticsData.ts | 8 +++- .../meteor/server/models/raw/LivechatRooms.ts | 38 ++++--------------- .../src/models/ILivechatRoomsModel.ts | 8 ++-- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts index 356c2c23032f..07802b75553c 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.ts @@ -71,7 +71,13 @@ callbacks.add( } const analyticsData = getAnalyticsData(room, new Date()); - await LivechatRooms.saveAnalyticsDataByRoomId(room, message, analyticsData); + const updater = await LivechatRooms.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData); + + if (updater.hasChanges()) { + await updater.persist({ + _id: room._id, + }); + } return message; }, diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index 68e95b45c96d..dfb0143cb984 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -2046,15 +2046,14 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive room: IOmnichannelRoom, message: IMessage, analyticsData: Record | undefined, + updater: Updater = this.getUpdater(), ) { - 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 (visitorLastQuery > agentLastReply) { - updater.set('metrics.servedBy.lr', message.ts); + return this.getAnalyticsUpdateQuery(analyticsData, updater).set('metrics.servedBy.lr', message.ts); } return updater; @@ -2064,52 +2063,31 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive room: IOmnichannelRoom, message: IMessage, analyticsData: Record | undefined, + updater: Updater = this.getUpdater(), ) { - 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 this.getAnalyticsUpdateQuery(analyticsData).set('metrics.v.lq', message.ts); } return updater; } - private getAnalyticsUpdateQueryByRoomId( + async getAnalyticsUpdateQueryByRoomId( room: IOmnichannelRoom, message: IMessage, analyticsData: Record | undefined, + updater: Updater = this.getUpdater(), ) { return isMessageFromVisitor(message) - ? this.getAnalyticsUpdateQueryBySentByVisitor(room, message, analyticsData) - : this.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData); - } - - async saveAnalyticsDataByRoomId( - room: IOmnichannelRoom, - message: IMessage, - analyticsData?: Record, - ): Promise { - const updater = this.getAnalyticsUpdateQueryByRoomId(room, message, analyticsData); - return updater.persist({ _id: room._id }); + ? this.getAnalyticsUpdateQueryBySentByVisitor(room, message, analyticsData, updater) + : this.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, updater); } - // saveAnalyticsDataByRoomIdAndLastMessageFromVisitor( - // room: IOmnichannelRoom, - // message: IMessage, - // analyticsData: Record, - // ) { - // 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 = { t, diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index c6a369e4caf1..8f364ad66a89 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -9,6 +9,7 @@ import type { import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions, DeleteResult, Filter } from 'mongodb'; import type { FindPaginated } from '..'; +import type { Updater } from '../updater'; import type { IBaseModel } from './IBaseModel'; type Period = { @@ -208,11 +209,12 @@ export interface ILivechatRoomsModel extends IBaseModel { setResponseByRoomId(roomId: string, responseBy: IOmnichannelRoom['responseBy']): Promise; setNotResponseByRoomId(roomId: string): Promise; setAgentLastMessageTs(roomId: string): Promise; - saveAnalyticsDataByRoomId( + getAnalyticsUpdateQueryByRoomId( room: IOmnichannelRoom, message: IMessage, - analyticsData?: Record, - ): Promise; // Promise; + analyticsData: Record | undefined, + updater?: Updater, + ): Promise>; getTotalConversationsBetweenDate(t: 'l', date: { gte: Date; lt: Date }, data?: { departmentId: string }): Promise; getAnalyticsMetricsBetweenDate( t: 'l',