Skip to content

Commit

Permalink
feat: Save visitor's activity on agent's interaction (#30222)
Browse files Browse the repository at this point in the history
  • Loading branch information
KevLehman authored and debdutdeb committed Oct 26, 2023
1 parent 97ba264 commit cb15007
Show file tree
Hide file tree
Showing 39 changed files with 404 additions and 46 deletions.
8 changes: 8 additions & 0 deletions .changeset/brown-comics-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-typings": patch
"@rocket.chat/model-typings": patch
---

chore: Calculate & Store MAC stats
Added new info to the stats: `omnichannelContactsBySource`, `uniqueContactsOfLastMonth`, `uniqueContactsOfLastWeek`, `uniqueContactsOfYesterday`
5 changes: 5 additions & 0 deletions .changeset/khaki-feet-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

feat: Save visitor's activity on agent's interaction
7 changes: 7 additions & 0 deletions .changeset/warm-melons-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-typings": patch
"@rocket.chat/omnichannel-services": patch
---

feat: Disable and annonimize visitors instead of removing
2 changes: 1 addition & 1 deletion apps/meteor/app/apps/server/bridges/livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export class AppLivechatBridge extends LivechatBridge {
}

return Promise.all(
(await LivechatVisitors.find(query).toArray()).map(
(await LivechatVisitors.findEnabled(query).toArray()).map(
async (visitor) => visitor && this.orch.getConverters()?.get('visitors').convertVisitor(visitor),
),
);
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/apps/server/converters/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class AppRoomsConverter {

let v;
if (room.visitor) {
const visitor = await LivechatVisitors.findOneById(room.visitor.id);
const visitor = await LivechatVisitors.findOneEnabledById(room.visitor.id);

const { lastMessageTs, phone } = room.visitorChannelInfo;

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/apps/server/converters/visitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class AppVisitorsConverter {
}

async convertById(id) {
const visitor = await LivechatVisitors.findOneById(id);
const visitor = await LivechatVisitors.findOneEnabledById(id);

return this.convertVisitor(visitor);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export type WorkspaceRegistrationData<T> = {
setupComplete: boolean;
connectionDisable: boolean;
npsEnabled: string;
// TODO: Evaluate naming
MAC: number;
// activeContactsBillingMonth: number;
// activeContactsYesterday: number;
};

export async function buildWorkspaceRegistrationData<T extends string | undefined>(contactEmail: T): Promise<WorkspaceRegistrationData<T>> {
Expand Down Expand Up @@ -80,7 +83,8 @@ export async function buildWorkspaceRegistrationData<T extends string | undefine
setupComplete: setupWizardState === 'completed',
connectionDisable: !registerServer,
npsEnabled,
// TODO: add MAC count
MAC: 0,
MAC: stats.omnichannelContactsBySource.contactsCount,
// activeContactsBillingMonth: stats.omnichannelContactsBySource.contactsCount,
// activeContactsYesterday: stats.uniqueContactsOfYesterday.contactsCount,
};
}
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/imports/server/rest/sms.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const defineVisitor = async (smsNumber, targetDepartment) => {
}

const id = await LivechatTyped.registerGuest(data);
return LivechatVisitors.findOneById(id);
return LivechatVisitors.findOneEnabledById(id);
};

const normalizeLocationSharing = (payload) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/api/lib/visitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { callbacks } from '../../../../../lib/callbacks';
import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom';

export async function findVisitorInfo({ visitorId }: { visitorId: IVisitor['_id'] }) {
const visitor = await LivechatVisitors.findOneById(visitorId);
const visitor = await LivechatVisitors.findOneEnabledById(visitorId);
if (!visitor) {
throw new Error('visitor-not-found');
}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/api/v1/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ API.v1.addRoute(
contactId: String,
});

const contact = await LivechatVisitors.findOneById(this.queryParams.contactId);
const contact = await LivechatVisitors.findOneEnabledById(this.queryParams.contactId);

return API.v1.success({ contact });
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/api/v1/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ API.v1.addRoute(
guest.connectionData = normalizeHttpHeaderData(this.request.headers);

const visitorId = await LivechatTyped.registerGuest(guest);
visitor = await LivechatVisitors.findOneById(visitorId);
visitor = await LivechatVisitors.findOneEnabledById(visitorId);
}

const sentMessages = await Promise.all(
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/api/v1/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ API.v1.addRoute(
throw new Error('This_conversation_is_already_closed');
}

const guest = await LivechatVisitors.findOneById(room.v?._id);
const guest = await LivechatVisitors.findOneEnabledById(room.v?._id);
const transferedBy = this.user satisfies TransferByData;
transferData.transferredBy = normalizeTransferredByData(transferedBy, room);
if (transferData.userId) {
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/app/livechat/server/api/v1/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ API.v1.addRoute('livechat/visitor', {

const visitorId = await LivechatTyped.registerGuest(guest);

let visitor = await VisitorsRaw.findOneById(visitorId, {});
let visitor = await VisitorsRaw.findOneEnabledById(visitorId, {});
if (visitor) {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
// If it's updating an existing visitor, it must also update the roomInfo
Expand All @@ -65,7 +65,7 @@ API.v1.addRoute('livechat/visitor', {
}
}

visitor = await VisitorsRaw.findOneById(visitorId, {});
visitor = await VisitorsRaw.findOneEnabledById(visitorId, {});
}

if (!visitor) {
Expand Down Expand Up @@ -122,7 +122,7 @@ API.v1.addRoute('livechat/visitor/:token', {

const { _id } = visitor;
const result = await Livechat.removeGuest(_id);
if (!result) {
if (!result.modifiedCount) {
throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor');
}

Expand Down
16 changes: 15 additions & 1 deletion apps/meteor/app/livechat/server/hooks/markRoomResponded.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom, isEditedMessage } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
import { LivechatRooms, LivechatVisitors } from '@rocket.chat/models';
import moment from 'moment';

import { callbacks } from '../../../../lib/callbacks';

Expand All @@ -26,6 +27,19 @@ callbacks.add(
return message;
}

// Return YYYY-MM from moment
const monthYear = moment().format('YYYY-MM');
const isVisitorActive = await LivechatVisitors.isVisitorActiveOnPeriod(room.v._id, monthYear);
if (!isVisitorActive) {
await LivechatVisitors.markVisitorActiveForPeriod(room.v._id, monthYear);
}

await LivechatRooms.markVisitorActiveForPeriod(room._id, monthYear);

if (room.responseBy) {
await LivechatRooms.setAgentLastMessageTs(room._id);
}

// check if room is yet awaiting for response from visitor
if (!room.waitingResponse) {
// case where agent sends second message or any subsequent message in a room before visitor responds to the first message
Expand Down
26 changes: 11 additions & 15 deletions apps/meteor/app/livechat/server/lib/Livechat.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ export const Livechat = {
async forwardOpenChats(userId) {
Livechat.logger.debug(`Transferring open chats for user ${userId}`);
for await (const room of LivechatRooms.findOpenByAgent(userId)) {
const guest = await LivechatVisitors.findOneById(room.v._id);
const guest = await LivechatVisitors.findOneEnabledById(room.v._id);
const user = await Users.findOneById(userId);
const { _id, username, name } = user;
const transferredBy = normalizeTransferredByData({ _id, username, name }, room);
Expand Down Expand Up @@ -462,7 +462,7 @@ export const Livechat = {
},

async getLivechatRoomGuestInfo(room) {
const visitor = await LivechatVisitors.findOneById(room.v._id);
const visitor = await LivechatVisitors.findOneEnabledById(room.v._id);
const agent = await Users.findOneById(room.servedBy && room.servedBy._id);

const ua = new UAParser();
Expand Down Expand Up @@ -604,16 +604,15 @@ export const Livechat = {
},

async removeGuest(_id) {
check(_id, String);
const guest = await LivechatVisitors.findOneById(_id, { projection: { _id: 1 } });
const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } });
if (!guest) {
throw new Meteor.Error('error-invalid-guest', 'Invalid guest', {
method: 'livechat:removeGuest',
});
}

await this.cleanGuestHistory(_id);
return LivechatVisitors.removeById(_id);
await this.cleanGuestHistory(guest);
return LivechatVisitors.disableById(_id);
},

async setUserStatusLivechat(userId, status) {
Expand All @@ -628,16 +627,13 @@ export const Livechat = {
return user;
},

async cleanGuestHistory(_id) {
const guest = await LivechatVisitors.findOneById(_id);
if (!guest) {
throw new Meteor.Error('error-invalid-guest', 'Invalid guest', {
method: 'livechat:cleanGuestHistory',
});
}

async cleanGuestHistory(guest) {
const { token } = guest;
check(token, String);

// This shouldn't be possible, but just in case
if (!token) {
throw new Error('error-invalid-guest');
}

const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const cursor = LivechatRooms.findByVisitorToken(token, extraQuery);
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ class LivechatClass {
!(await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id'>>(guest.department, { projection: { _id: 1 } }))
) {
await LivechatVisitors.removeDepartmentById(guest._id);
const tmpGuest = await LivechatVisitors.findOneById(guest._id);
const tmpGuest = await LivechatVisitors.findOneEnabledById(guest._id);
if (tmpGuest) {
guest = tmpGuest;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/methods/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Meteor.methods<ServerMethods>({
});
}

const guest = await LivechatVisitors.findOneById(room.v?._id);
const guest = await LivechatVisitors.findOneEnabledById(room.v?._id);

const user = await Meteor.userAsync();

Expand Down
32 changes: 32 additions & 0 deletions apps/meteor/app/statistics/server/lib/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import {
LivechatCustomField,
Subscriptions,
Users,
LivechatRooms,
} from '@rocket.chat/models';
import { MongoInternals } from 'meteor/mongo';
import moment from 'moment';

import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server/getStatistics';
import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred';
Expand Down Expand Up @@ -269,6 +271,36 @@ export const statistics = {
}),
);

const defaultValue = { contactsCount: 0, conversationsCount: 0, sources: [] };
const billablePeriod = moment.utc().format('YYYY-MM');
statsPms.push(
LivechatRooms.getMACStatisticsForPeriod(billablePeriod).then(([result]) => {
statistics.omnichannelContactsBySource = result || defaultValue;
}),
);

const monthAgo = moment.utc().subtract(30, 'days').toDate();
const today = moment.utc().toDate();
statsPms.push(
LivechatRooms.getMACStatisticsBetweenDates(monthAgo, today).then(([result]) => {
statistics.uniqueContactsOfLastMonth = result || defaultValue;
}),
);

const weekAgo = moment.utc().subtract(7, 'days').toDate();
statsPms.push(
LivechatRooms.getMACStatisticsBetweenDates(weekAgo, today).then(([result]) => {
statistics.uniqueContactsOfLastWeek = result || defaultValue;
}),
);

const yesterday = moment.utc().subtract(1, 'days').toDate();
statsPms.push(
LivechatRooms.getMACStatisticsBetweenDates(yesterday, today).then(([result]) => {
statistics.uniqueContactsOfYesterday = result || defaultValue;
}),
);

// Message statistics
statistics.totalChannelMessages = (await Rooms.findByType('c', { projection: { msgs: 1 } }).toArray()).reduce(
function _countChannelMessages(num: number, room: IRoom) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ export default {
uniqueOSOfYesterday: { data: [], day: 0, month: 0, year: 0 },
uniqueOSOfLastWeek: { data: [], day: 0, month: 0, year: 0 },
uniqueOSOfLastMonth: { data: [], day: 0, month: 0, year: 0 },
omnichannelContactsBySource: { contactsCount: 0, conversationsCount: 0, sources: [] },
uniqueContactsOfLastMonth: { contactsCount: 0, conversationsCount: 0, sources: [] },
uniqueContactsOfLastWeek: { contactsCount: 0, conversationsCount: 0, sources: [] },
uniqueContactsOfYesterday: { contactsCount: 0, conversationsCount: 0, sources: [] },
apps: {
engineVersion: 'x.y.z',
enabled: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ export default {
uniqueOSOfYesterday: { data: [], day: 0, month: 0, year: 0 },
uniqueOSOfLastWeek: { data: [], day: 0, month: 0, year: 0 },
uniqueOSOfLastMonth: { data: [], day: 0, month: 0, year: 0 },
omnichannelContactsBySource: { contactsCount: 0, conversationsCount: 0, sources: [] },
uniqueContactsOfLastMonth: { contactsCount: 0, conversationsCount: 0, sources: [] },
uniqueContactsOfLastWeek: { contactsCount: 0, conversationsCount: 0, sources: [] },
uniqueContactsOfYesterday: { contactsCount: 0, conversationsCount: 0, sources: [] },
apps: {
engineVersion: 'x.y.z',
enabled: false,
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/client/views/admin/info/UsageCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ export default {
uniqueOSOfYesterday: { data: [], day: 0, month: 0, year: 0 },
uniqueOSOfLastWeek: { data: [], day: 0, month: 0, year: 0 },
uniqueOSOfLastMonth: { data: [], day: 0, month: 0, year: 0 },
omnichannelContactsBySource: { contactsCount: 0, conversationsCount: 0, sources: [] },
uniqueContactsOfLastMonth: { contactsCount: 0, conversationsCount: 0, sources: [] },
uniqueContactsOfLastWeek: { contactsCount: 0, conversationsCount: 0, sources: [] },
uniqueContactsOfYesterday: { contactsCount: 0, conversationsCount: 0, sources: [] },
apps: {
engineVersion: 'x.y.z',
enabled: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const handleBeforeSaveMessage = async (message: IMessage, room?: IOmnichannelRoo
}
const visitorId = room?.v?._id;
const agent = (await Users.findOneById(agentId, { projection: { name: 1, _id: 1, emails: 1 } })) || {};
const visitor = visitorId && ((await LivechatVisitors.findOneById(visitorId, {})) || {});
const visitor = visitorId && ((await LivechatVisitors.findOneEnabledById(visitorId, {})) || {});

Object.keys(placeholderFields).map((field) => {
const templateKey = `{{${field}}}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ callbacks.add(
}

const { _id: guestId } = defaultGuest;
const guest = await LivechatVisitors.findOneById(guestId, {
const guest = await LivechatVisitors.findOneEnabledById(guestId, {
projection: { lastAgent: 1, token: 1, contactManager: 1 },
});
if (!guest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const resumeOnHoldCommentAndUser = async (room: IOmnichannelRoom): Promise<{ com
v: { _id: visitorId },
_id: rid,
} = room;
const visitor = await LivechatVisitors.findOneById<Pick<ILivechatVisitor, 'name' | 'username'>>(visitorId, {
const visitor = await LivechatVisitors.findOneEnabledById<Pick<ILivechatVisitor, 'name' | 'username'>>(visitorId, {
projection: { name: 1, username: 1 },
});
if (!visitor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class VisitorInactivityMonitor {
}

private async getDefaultAbandonedCustomMessage(abandonmentAction: 'close' | 'on-hold', visitorId: string) {
const visitor = await LivechatVisitors.findOneById<Pick<ILivechatVisitor, 'name' | 'username'>>(visitorId, {
const visitor = await LivechatVisitors.findOneEnabledById<Pick<ILivechatVisitor, 'name' | 'username'>>(visitorId, {
projection: {
name: 1,
username: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async function resolveOnHoldCommentInfo(options: { clientAction: boolean }, room
const {
v: { _id: visitorId },
} = room;
const visitor = await LivechatVisitors.findOneById<Pick<ILivechatVisitor, 'name' | 'username'>>(visitorId, {
const visitor = await LivechatVisitors.findOneEnabledById<Pick<ILivechatVisitor, 'name' | 'username'>>(visitorId, {
projection: { name: 1, username: 1 },
});
if (!visitor) {
Expand Down
6 changes: 4 additions & 2 deletions apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr
return guest;
}
await LivechatTyped.setDepartmentForGuest({ token: guest.token, department });
return LivechatVisitors.findOneById(guest._id, {});
return LivechatVisitors.findOneEnabledById(guest._id, {});
}
return guest;
}
Expand All @@ -47,7 +47,9 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr
department,
});

const newGuest = await LivechatVisitors.findOneById(userId);
const newGuest = await LivechatVisitors.findOneEnabledById(userId);
logger.debug(`Guest ${userId} for visitor ${email} created`);

if (newGuest) {
return newGuest;
}
Expand Down
Loading

0 comments on commit cb15007

Please sign in to comment.