-
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 9 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,13 @@ | ||
| 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; | ||
| } | ||
| const sentByVisitor = Boolean(message.token); | ||
| if (sentByVisitor) { | ||
|
renatobecker-zz marked this conversation as resolved.
Outdated
|
||
| 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 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -435,8 +435,33 @@ export const Livechat = { | |||||||||
| } | ||||||||||
| }, | ||||||||||
|
|
||||||||||
| saveTransferHistory(room, transferData) { | ||||||||||
| const transferedToDepartment = Boolean(transferData.departmentId); | ||||||||||
|
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. I would use destructuring assignment here:
Suggested change
|
||||||||||
| const transferedToAgent = Boolean(transferData.userId); | ||||||||||
| const transfer = { | ||||||||||
|
renatobecker-zz marked this conversation as resolved.
|
||||||||||
| transferedBy: transferData.transferedBy, | ||||||||||
| ts: new Date(), | ||||||||||
| }; | ||||||||||
| if (transferedToDepartment) { | ||||||||||
| transfer.scope = 'department'; | ||||||||||
| transfer.previousDepartment = room.departmentId; | ||||||||||
| transfer.nextDepartment = transferData.departmentId; | ||||||||||
| } | ||||||||||
| if (transferedToAgent) { | ||||||||||
| transfer.scope = 'agent'; | ||||||||||
| transfer.previousAgent = room.servedBy && room.servedBy._id; | ||||||||||
| transfer.nextAgent = transferData.userId; | ||||||||||
| } | ||||||||||
| 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) { | ||||||||||
|
|
@@ -842,6 +867,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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ Meteor.methods({ | |||||
| const transferData = { | ||||||
| roomId, | ||||||
| departmentId, | ||||||
| transferedBy: visitor._id, | ||||||
|
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
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. the |
||||||
| }; | ||||||
|
|
||||||
| return Livechat.transfer(room, visitor, transferData); | ||||||
|
|
||||||
| 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 { LivechatSessions, Sessions, Users } from '../../../models/server'; | ||
|
|
||
| const formatDate = (dateTime = new Date()) => ({ | ||
| date: parseInt(moment(dateTime).format('YYYYMMDD')), | ||
| }); | ||
|
|
||
| export class LivechatSessionsMonitor { | ||
|
renatobecker-zz marked this conversation as resolved.
Outdated
|
||
| constructor() { | ||
| this._handleMeteorConnection = this._handleMeteorConnection.bind(this); | ||
| this._handleAgentStatusChanged = this._handleAgentStatusChanged.bind(this); | ||
| this._handleUserStatusLivechatChanged = this._handleUserStatusLivechatChanged.bind(this); | ||
| } | ||
|
|
||
| start() { | ||
| this._setupListeners(); | ||
| } | ||
|
|
||
| _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 }; | ||
| LivechatSessions.createOrUpdate(data); | ||
| } | ||
|
|
||
| _updateSessionWhenAgentStop(userId) { | ||
| const data = { ...formatDate(), agentId: userId }; | ||
| const livechatSession = LivechatSessions.findOne(data); | ||
| if (livechatSession) { | ||
| const stopedAt = new Date(); | ||
| const availableTime = moment(stopedAt).diff(moment(new Date(livechatSession.lastStartedAt)), 'seconds'); | ||
| LivechatSessions.updateLastStoppedAt({ ...data, availableTime, lastStopedAt: stopedAt }); | ||
| LivechatSessions.updateServiceHistory({ ...data, historyObject: { startedAt: livechatSession.lastStartedAt, stopedAt } }); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -438,6 +438,45 @@ export class LivechatRooms extends Base { | |||||
|
|
||||||
| return this.remove(query); | ||||||
| } | ||||||
|
|
||||||
| updateTransferHistoryByRoomId(roomId, 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
|
||||||
| const query = { | ||||||
| _id: roomId, | ||||||
| }; | ||||||
| const update = { | ||||||
| $addToSet: { | ||||||
| transferHistory: 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
|
||||||
| }, | ||||||
| }; | ||||||
|
|
||||||
| return this.update(query, update); | ||||||
| } | ||||||
|
|
||||||
| setVisitorLastMessageTimestampByRoomId(roomId, lastMessageTs) { | ||||||
| const query = { | ||||||
| _id: roomId, | ||||||
| }; | ||||||
| const update = { | ||||||
| $set: { | ||||||
| 'v.lastMessageTs': lastMessageTs, | ||||||
| }, | ||||||
| }; | ||||||
|
|
||||||
| return this.update(query, update); | ||||||
| } | ||||||
|
|
||||||
| setVisitorInactivityInSecondsByRoomId(roomId, visitorInactivity) { | ||||||
| const query = { | ||||||
| _id: roomId, | ||||||
| }; | ||||||
| const update = { | ||||||
| $set: { | ||||||
| 'metrics.visitorInactivity': visitorInactivity, | ||||||
| }, | ||||||
| }; | ||||||
|
|
||||||
| return this.update(query, update); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| export default new LivechatRooms(Rooms.model, true); | ||||||
Uh oh!
There was an error while loading. Please reload this page.