-
Notifications
You must be signed in to change notification settings - Fork 13.7k
[NEW] Livechat analytics #15230
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
[NEW] Livechat analytics #15230
Changes from 18 commits
f3241e8
4493e70
ef4ebff
c15b6a0
1c66120
c6d196b
8b00fe6
50803ca
efcda9e
d579936
f1a7f43
6960c6f
8acf467
6f2ae7b
81c1376
eaf4360
9c51a37
0fd5a96
c5c1dc9
0e7b1a1
4eb2772
8b7bb33
738be31
0aa93f0
032b5ee
e47cc80
2421b94
ab97a6b
29d0500
862e93f
d3be44e
b6dc580
d4ab16e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import moment from 'moment'; | ||
|
|
||
| import { settings } from '../../../settings'; | ||
| import { callbacks } from '../../../callbacks'; | ||
| import { LivechatRooms, Messages, LivechatOfficeHour } from '../../../models'; | ||
|
|
||
| const getSecondsWhenOfficeHoursIsDisabled = (room, agentLastMessage) => moment(new Date(room.closedAt)).diff(moment(new Date(agentLastMessage.ts)), 'seconds'); | ||
| const getOfficeHoursDictionary = () => LivechatOfficeHour.find().fetch().reduce((acc, day) => { | ||
| acc[day.day] = { | ||
| start: day.start, | ||
| finish: day.finish, | ||
| open: day.open, | ||
| }; | ||
| return acc; | ||
| }, {}); | ||
| const getSecondsSinceLastAgentResponse = (room, agentLastMessage) => { | ||
| if (!settings.get('Livechat_enable_office_hours')) { | ||
| return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); | ||
| } | ||
|
|
||
| let totalSeconds = 0; | ||
| const officeDays = getOfficeHoursDictionary(); | ||
| const endOfConversation = moment(new Date(room.closedAt)); | ||
| const startOfInactivity = moment(new Date(agentLastMessage.ts)); | ||
| const daysOfInactivity = endOfConversation.clone().startOf('day').diff(startOfInactivity.clone().startOf('day'), 'days'); | ||
| const inactivityDay = moment(new Date(agentLastMessage.ts)); | ||
|
|
||
| for (let index = 0; index <= daysOfInactivity; index++) { | ||
| const today = inactivityDay.clone().format('dddd'); | ||
| const officeDay = officeDays[today]; | ||
| const startTodaysOfficeHour = moment(officeDay.start, 'HH:mm').add(index, 'days'); | ||
| const endTodaysOfficeHour = moment(officeDay.finish, 'HH:mm').add(index, 'days'); | ||
| if (officeDays[today].open) { | ||
| const firstDayOfInactivity = startOfInactivity.clone().format('D') === inactivityDay.clone().format('D'); | ||
| const lastDayOfInactivity = endOfConversation.clone().format('D') === inactivityDay.clone().format('D'); | ||
|
|
||
| if (!firstDayOfInactivity && !lastDayOfInactivity) { | ||
| totalSeconds += endTodaysOfficeHour.clone().diff(startTodaysOfficeHour, 'seconds'); | ||
| } else { | ||
| const end = endOfConversation.isBefore(endTodaysOfficeHour) ? endOfConversation : endTodaysOfficeHour; | ||
| const start = firstDayOfInactivity ? inactivityDay : startTodaysOfficeHour; | ||
| totalSeconds += end.clone().diff(start, 'seconds'); | ||
| } | ||
| } | ||
| inactivityDay.add(1, 'days'); | ||
| } | ||
| return totalSeconds; | ||
| }; | ||
|
|
||
| callbacks.add('livechat.closeRoom', (room) => { | ||
| const closedByAgent = room.closer !== 'visitor'; | ||
| const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token; | ||
| if (closedByAgent && wasTheLastMessageSentByAgent) { | ||
| const agentLastMessage = Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs).fetch()[0]; | ||
| const secondsSinceLastAgentResponse = getSecondsSinceLastAgentResponse(room, agentLastMessage); | ||
| LivechatRooms.setVisitorInactivityInSecondsByRoomId(room._id, secondsSinceLastAgentResponse); | ||
| } | ||
| }, callbacks.priority.HIGH, 'process-room-abandonment'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { callbacks } from '../../../callbacks'; | ||
| import { LivechatRooms } from '../../../models'; | ||
|
|
||
| callbacks.add('afterSaveMessage', function(message, room) { | ||
| if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) { | ||
| return message; | ||
| } | ||
| if (message.token) { | ||
| LivechatRooms.setVisitorLastMessageTimestampByRoomId(room._id, message.ts); | ||
| } | ||
| return message; | ||
| }, callbacks.priority.HIGH, 'save-last-visitor-message-timestamp'); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -257,3 +257,17 @@ export const forwardRoomToDepartment = async (room, guest, departmentId) => { | |||||||||||||||||
|
|
||||||||||||||||||
| return true; | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| export const normalizeTransferredByData = (transferredBy, room) => { | ||||||||||||||||||
| if (!transferredBy || !room) { | ||||||||||||||||||
| throw new Error('You must provide "transferredBy" and "room" params to "getTransferredByData"'); | ||||||||||||||||||
| } | ||||||||||||||||||
| const { servedBy: { _id: agentId } = {} } = room; | ||||||||||||||||||
| const { _id, username, userType: transferType } = transferredBy; | ||||||||||||||||||
| const type = transferType || (_id === agentId ? 'agent' : 'user'); | ||||||||||||||||||
| return { | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
| _id, | ||||||||||||||||||
| username, | ||||||||||||||||||
| type, | ||||||||||||||||||
| }; | ||||||||||||||||||
| }; | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -461,8 +461,32 @@ export const Livechat = { | |||||
| } | ||||||
| }, | ||||||
|
|
||||||
| saveTransferHistory(room, transferData) { | ||||||
| const { departmentId: previousDepartment } = room; | ||||||
| const { departmentId: nextDepartment, transferredBy, transferredTo } = transferData; | ||||||
| check(transferredBy, Match.ObjectIncluding({ | ||||||
| _id: String, | ||||||
| username: String, | ||||||
| type: String, | ||||||
| })); | ||||||
| const transfer = { | ||||||
|
renatobecker-zz marked this conversation as resolved.
|
||||||
| transferredBy, | ||||||
| ts: new Date(), | ||||||
| scope: nextDepartment ? 'department' : 'agent', | ||||||
| ...previousDepartment && { previousDepartment }, | ||||||
| ...nextDepartment && { nextDepartment }, | ||||||
| ...transferredTo && { transferredTo }, | ||||||
| }; | ||||||
| LivechatRooms.updateTransferHistoryByRoomId(room._id, transfer); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| }, | ||||||
|
|
||||||
| async transfer(room, guest, transferData) { | ||||||
| return RoutingManager.transferRoom(room, guest, transferData); | ||||||
| const result = await RoutingManager.transferRoom(room, guest, transferData); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| if (!result) { | ||||||
| return false; | ||||||
| } | ||||||
| this.saveTransferHistory(room, transferData); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| return true; | ||||||
| }, | ||||||
|
|
||||||
| returnRoomAsInquiry(rid, departmentId) { | ||||||
|
|
@@ -876,6 +900,7 @@ export const Livechat = { | |||||
| status, | ||||||
| }); | ||||||
| }); | ||||||
| callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This callback doesn't depend on the |
||||||
| }, | ||||||
|
|
||||||
| allowAgentChangeServiceStatus(statusLivechat) { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import moment from 'moment'; | ||
| import { Meteor } from 'meteor/meteor'; | ||
|
|
||
| import { callbacks } from '../../../callbacks/server'; | ||
| import { LivechatAgentActivity, Sessions, Users } from '../../../models/server'; | ||
|
|
||
| const formatDate = (dateTime = new Date()) => ({ | ||
| date: parseInt(moment(dateTime).format('YYYYMMDD')), | ||
| }); | ||
|
|
||
| export class LivechatAgentActivityMonitor { | ||
| constructor() { | ||
| this._handleMeteorConnection = this._handleMeteorConnection.bind(this); | ||
| this._handleAgentStatusChanged = this._handleAgentStatusChanged.bind(this); | ||
| this._handleUserStatusLivechatChanged = this._handleUserStatusLivechatChanged.bind(this); | ||
| } | ||
|
|
||
| start() { | ||
| this._setupListeners(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would not it be good to have a property to store when the |
||
| } | ||
|
|
||
| _setupListeners() { | ||
| Meteor.onConnection(this._handleMeteorConnection); | ||
| callbacks.add('livechat.agentStatusChanged', this._handleAgentStatusChanged); | ||
| callbacks.add('livechat.setUserStatusLivechat', this._handleUserStatusLivechatChanged); | ||
| } | ||
|
|
||
| _handleMeteorConnection(connection) { | ||
| const session = Sessions.findOne({ sessionId: connection.id }); | ||
| if (!session) { | ||
| return; | ||
| } | ||
| const user = Users.findOneById(session.userId); | ||
| if (user && user.status !== 'offline' && user.statusLivechat === 'available') { | ||
| this._createOrUpdateSession(user._id); | ||
| } | ||
| connection.onClose(() => { | ||
| if (session) { | ||
| this._updateSessionWhenAgentStop(session.userId); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| _handleAgentStatusChanged({ userId, status }) { | ||
| const user = Users.findOneById(userId); | ||
| if (!user || user.statusLivechat !== 'available') { | ||
| return; | ||
| } | ||
|
|
||
| if (status !== 'offline') { | ||
| this._createOrUpdateSession(userId); | ||
| } else { | ||
| this._updateSessionWhenAgentStop(userId); | ||
| } | ||
| } | ||
|
|
||
| _handleUserStatusLivechatChanged({ userId, status }) { | ||
| const user = Users.findOneById(userId); | ||
| if (user && user.status === 'offline') { | ||
| return; | ||
| } | ||
|
|
||
| if (status === 'available') { | ||
| this._createOrUpdateSession(userId); | ||
| } | ||
| if (status === 'not-available') { | ||
| this._updateSessionWhenAgentStop(userId); | ||
| } | ||
| } | ||
|
|
||
| _createOrUpdateSession(userId) { | ||
| const data = { ...formatDate(), agentId: userId }; | ||
| LivechatAgentActivity.createOrUpdate(data); | ||
| } | ||
|
|
||
| _updateSessionWhenAgentStop(userId) { | ||
| const data = { ...formatDate(), agentId: userId }; | ||
| const livechatSession = LivechatAgentActivity.findOne(data); | ||
| if (livechatSession) { | ||
| const stoppedAt = new Date(); | ||
| const availableTime = moment(stoppedAt).diff(moment(new Date(livechatSession.lastStartedAt)), 'seconds'); | ||
| LivechatAgentActivity.updateLastStoppedAt({ ...data, availableTime, lastStopedAt: stoppedAt }); | ||
| LivechatAgentActivity.updateServiceHistory({ ...data, serviceHistory: { startedAt: livechatSession.lastStartedAt, stopedAt: stoppedAt } }); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.