diff --git a/.changeset/many-tables-love.md b/.changeset/many-tables-love.md new file mode 100644 index 000000000000..8f37283c6a96 --- /dev/null +++ b/.changeset/many-tables-love.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/model-typings": minor +--- + +Fixed Livechat rooms being displayed in the Engagement Dashboard's "Channels" tab diff --git a/.changeset/proud-waves-bathe.md b/.changeset/proud-waves-bathe.md new file mode 100644 index 000000000000..556fa3af80e1 --- /dev/null +++ b/.changeset/proud-waves-bathe.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/model-typings": minor +--- + +Improved Engagement Dashboard's "Channels" tab performance by not returning rooms that had no activity in the analyzed period diff --git a/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts b/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts index cd1a338eeab5..f45c620c8b49 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts @@ -24,6 +24,7 @@ export const useChannelsList = ({ period, offset, count }: UseChannelsListOption end: end.toISOString(), offset, count, + hideRoomsWithNoActivity: true, }); return response diff --git a/apps/meteor/ee/server/api/engagementDashboard/channels.ts b/apps/meteor/ee/server/api/engagementDashboard/channels.ts index b2a655f4a843..0d2d140bd575 100644 --- a/apps/meteor/ee/server/api/engagementDashboard/channels.ts +++ b/apps/meteor/ee/server/api/engagementDashboard/channels.ts @@ -3,14 +3,15 @@ import { check, Match } from 'meteor/check'; import { API } from '../../../../app/api/server'; import { getPaginationItems } from '../../../../app/api/server/helpers/getPaginationItems'; -import { findAllChannelsWithNumberOfMessages } from '../../lib/engagementDashboard/channels'; +import { apiDeprecationLogger } from '../../../../app/lib/server/lib/deprecationWarningLogger'; +import { findChannelsWithNumberOfMessages } from '../../lib/engagementDashboard/channels'; import { isDateISOString, mapDateForAPI } from '../../lib/engagementDashboard/date'; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention interface Endpoints { '/v1/engagement-dashboard/channels/list': { - GET: (params: { start: string; end: string; offset?: number; count?: number }) => { + GET: (params: { start: string; end: string; offset?: number; count?: number; hideRoomsWithNoActivity?: boolean }) => { channels: { room: { _id: IRoom['_id']; @@ -45,17 +46,30 @@ API.v1.addRoute( Match.ObjectIncluding({ start: Match.Where(isDateISOString), end: Match.Where(isDateISOString), + hideRoomsWithNoActivity: Match.Maybe(String), offset: Match.Maybe(String), count: Match.Maybe(String), }), ); - const { start, end } = this.queryParams; + const { start, end, hideRoomsWithNoActivity } = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); - const { channels, total } = await findAllChannelsWithNumberOfMessages({ + if (hideRoomsWithNoActivity === undefined) { + apiDeprecationLogger.deprecatedParameterUsage( + this.request.route, + 'hideRoomsWithNoActivity', + '7.0.0', + this.response, + ({ parameter, endpoint, version }) => + `Returning rooms that had no activity in ${endpoint} is deprecated and will be removed on version ${version} along with the \`${parameter}\` param. Set \`${parameter}\` as \`true\` to check how the endpoint will behave starting on ${version}`, + ); + } + + const { channels, total } = await findChannelsWithNumberOfMessages({ start: mapDateForAPI(start), end: mapDateForAPI(end), + hideRoomsWithNoActivity: hideRoomsWithNoActivity === 'true', options: { offset, count }, }); diff --git a/apps/meteor/ee/server/lib/engagementDashboard/channels.ts b/apps/meteor/ee/server/lib/engagementDashboard/channels.ts index 834284ebb9b7..7d08086ee1e9 100644 --- a/apps/meteor/ee/server/lib/engagementDashboard/channels.ts +++ b/apps/meteor/ee/server/lib/engagementDashboard/channels.ts @@ -1,9 +1,69 @@ import type { IDirectMessageRoom, IRoom } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { Analytics, Rooms } from '@rocket.chat/models'; import moment from 'moment'; +import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { convertDateToInt, diffBetweenDaysInclusive } from './date'; +export const findChannelsWithNumberOfMessages = async ({ + start, + end, + hideRoomsWithNoActivity, + options = {}, +}: { + start: Date; + end: Date; + hideRoomsWithNoActivity: boolean; + options: { + offset?: number; + count?: number; + }; +}): Promise<{ + channels: { + room: { + _id: IRoom['_id']; + name: IRoom['name'] | IRoom['fname']; + ts: IRoom['ts']; + t: IRoom['t']; + _updatedAt: IRoom['_updatedAt']; + usernames?: IDirectMessageRoom['usernames']; + }; + messages: number; + lastWeekMessages: number; + diffFromLastWeek: number; + }[]; + total: number; +}> => { + if (!hideRoomsWithNoActivity) { + return findAllChannelsWithNumberOfMessages({ start, end, options }); + } + + const daysBetweenDates = diffBetweenDaysInclusive(end, start); + const endOfLastWeek = moment(start).subtract(1, 'days').toDate(); + const startOfLastWeek = moment(endOfLastWeek).subtract(daysBetweenDates, 'days').toDate(); + const roomTypes = roomCoordinator.getTypesToShowOnDashboard() as Array; + + const aggregationResult = await Analytics.findRoomsByTypesWithNumberOfMessagesBetweenDate({ + types: roomTypes, + start: convertDateToInt(start), + end: convertDateToInt(end), + startOfLastWeek: convertDateToInt(startOfLastWeek), + endOfLastWeek: convertDateToInt(endOfLastWeek), + options, + }).toArray(); + + // The aggregation result may be undefined if there are no matching analytics or corresponding rooms in the period + if (!aggregationResult.length) { + return { channels: [], total: 0 }; + } + + const [{ channels, total }] = aggregationResult; + return { + channels, + total, + }; +}; + export const findAllChannelsWithNumberOfMessages = async ({ start, end, @@ -34,8 +94,10 @@ export const findAllChannelsWithNumberOfMessages = async ({ const daysBetweenDates = diffBetweenDaysInclusive(end, start); const endOfLastWeek = moment(start).subtract(1, 'days').toDate(); const startOfLastWeek = moment(endOfLastWeek).subtract(daysBetweenDates, 'days').toDate(); + const roomTypes = roomCoordinator.getTypesToShowOnDashboard() as Array; - const channels = await Rooms.findChannelsWithNumberOfMessagesBetweenDate({ + const channels = await Rooms.findChannelsByTypesWithNumberOfMessagesBetweenDate({ + types: roomTypes, start: convertDateToInt(start), end: convertDateToInt(end), startOfLastWeek: convertDateToInt(startOfLastWeek), @@ -43,15 +105,7 @@ export const findAllChannelsWithNumberOfMessages = async ({ options, }).toArray(); - const total = - ( - await Rooms.countChannelsWithNumberOfMessagesBetweenDate({ - start: convertDateToInt(start), - end: convertDateToInt(end), - startOfLastWeek: convertDateToInt(startOfLastWeek), - endOfLastWeek: convertDateToInt(endOfLastWeek), - }).toArray() - )[0]?.total ?? 0; + const total = await Rooms.countDocuments({ t: { $in: roomTypes } }); return { channels, diff --git a/apps/meteor/server/models/raw/Analytics.ts b/apps/meteor/server/models/raw/Analytics.ts index 4e95cadebbcd..de4700834168 100644 --- a/apps/meteor/server/models/raw/Analytics.ts +++ b/apps/meteor/server/models/raw/Analytics.ts @@ -1,7 +1,7 @@ import type { IAnalytic, IRoom } from '@rocket.chat/core-typings'; -import type { IAnalyticsModel } from '@rocket.chat/model-typings'; +import type { IAnalyticsModel, IChannelsWithNumberOfMessagesBetweenDate } from '@rocket.chat/model-typings'; import { Random } from '@rocket.chat/random'; -import type { AggregationCursor, FindCursor, Db, IndexDescription, FindOptions, UpdateResult, Document } from 'mongodb'; +import type { AggregationCursor, FindCursor, Db, IndexDescription, FindOptions, UpdateResult, Document, Collection } from 'mongodb'; import { readSecondaryPreferred } from '../../database/readSecondaryPreferred'; import { BaseRaw } from './BaseRaw'; @@ -14,7 +14,11 @@ export class AnalyticsRaw extends BaseRaw implements IAnalyticsModel } protected modelIndexes(): IndexDescription[] { - return [{ key: { date: 1 } }, { key: { 'room._id': 1, 'date': 1 }, unique: true, partialFilterExpression: { type: 'rooms' } }]; + return [ + { key: { date: 1 } }, + { key: { 'room._id': 1, 'date': 1 }, unique: true, partialFilterExpression: { type: 'rooms' } }, + { key: { 'room.t': 1, 'date': 1 }, partialFilterExpression: { type: 'messages' } }, + ]; } saveMessageSent({ room, date }: { room: IRoom; date: IAnalytic['date'] }): Promise { @@ -211,4 +215,117 @@ export class AnalyticsRaw extends BaseRaw implements IAnalyticsModel findByTypeBeforeDate({ type, date }: { type: IAnalytic['type']; date: IAnalytic['date'] }): FindCursor { return this.find({ type, date: { $lte: date } }); } + + getRoomsWithNumberOfMessagesBetweenDateQuery({ + types, + start, + end, + startOfLastWeek, + endOfLastWeek, + options, + }: { + types: Array; + start: number; + end: number; + startOfLastWeek: number; + endOfLastWeek: number; + options?: any; + }) { + const typeAndDateMatch = { + $match: { + 'type': 'messages', + 'room.t': { $in: types }, + 'date': { $gte: startOfLastWeek, $lte: end }, + }, + }; + const roomsGroup = { + $group: { + _id: '$room._id', + room: { $first: '$room' }, + messages: { $sum: { $cond: [{ $gte: ['$date', start] }, '$messages', 0] } }, + lastWeekMessages: { $sum: { $cond: [{ $lte: ['$date', endOfLastWeek] }, '$messages', 0] } }, + }, + }; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: '_id', + as: 'room', + }, + }; + const roomsUnwind = { + $unwind: { + path: '$room', + preserveNullAndEmptyArrays: false, + }, + }; + const project = { + $project: { + _id: 0, + room: { + _id: '$room._id', + name: { $ifNull: ['$room.name', '$room.fname'] }, + ts: '$room.ts', + t: '$room.t', + _updatedAt: '$room._updatedAt', + usernames: '$room.usernames', + }, + messages: '$messages', + lastWeekMessages: '$lastWeekMessages', + diffFromLastWeek: { $subtract: ['$messages', '$lastWeekMessages'] }, + }, + }; + + const sort = { $sort: options?.sort || { messages: -1 } }; + const sortAndPaginationParams: Exclude['aggregate']>[0], undefined> = [sort]; + if (options?.offset) { + sortAndPaginationParams.push({ $skip: options.offset }); + } + + if (options?.count) { + sortAndPaginationParams.push({ $limit: options.count }); + } + const facet = { + $facet: { + channels: [...sortAndPaginationParams], + total: [{ $count: 'total' }], + }, + }; + const totalUnwind = { $unwind: '$total' }; + const totalProject = { + $project: { + channels: '$channels', + total: '$total.total', + }, + }; + + const params: Exclude['aggregate']>[0], undefined> = [ + typeAndDateMatch, + roomsGroup, + lookup, + roomsUnwind, + project, + facet, + totalUnwind, + totalProject, + ]; + + return params; + } + + findRoomsByTypesWithNumberOfMessagesBetweenDate(params: { + types: Array; + start: number; + end: number; + startOfLastWeek: number; + endOfLastWeek: number; + options?: any; + }): AggregationCursor<{ channels: IChannelsWithNumberOfMessagesBetweenDate[]; total: number }> { + const aggregationParams = this.getRoomsWithNumberOfMessagesBetweenDateQuery(params); + return this.col.aggregate<{ channels: IChannelsWithNumberOfMessagesBetweenDate[]; total: number }>(aggregationParams, { + allowDiskUse: true, + readPreference: readSecondaryPreferred(), + }); + } } diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 1859f6c18b35..a4cd19a1c30a 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -413,18 +413,25 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { } getChannelsWithNumberOfMessagesBetweenDateQuery({ + types, start, end, startOfLastWeek, endOfLastWeek, options, }: { + types: Array; start: number; end: number; startOfLastWeek: number; endOfLastWeek: number; options?: any; }) { + const typeMatch = { + $match: { + t: { $in: types }, + }, + }; const lookup = { $lookup: { from: 'rocketchat_analytics', @@ -504,30 +511,32 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { diffFromLastWeek: { $subtract: ['$messages', '$lastWeekMessages'] }, }, }; - const firstParams = [ - lookup, - messagesProject, - messagesUnwind, - messagesGroup, - lastWeekMessagesUnwind, - lastWeekMessagesGroup, - presentationProject, - ]; + const firstParams = [typeMatch, lookup, messagesProject, messagesUnwind, messagesGroup]; + const lastParams = [lastWeekMessagesUnwind, lastWeekMessagesGroup, presentationProject]; + const sort = { $sort: options?.sort || { messages: -1 } }; - const params: Exclude['aggregate']>[0], undefined> = [...firstParams, sort]; + const sortAndPaginationParams: Exclude['aggregate']>[0], undefined> = [sort]; if (options?.offset) { - params.push({ $skip: options.offset }); + sortAndPaginationParams.push({ $skip: options.offset }); } if (options?.count) { - params.push({ $limit: options.count }); + sortAndPaginationParams.push({ $limit: options.count }); + } + const params: Exclude['aggregate']>[0], undefined> = [...firstParams]; + + if (options?.sort) { + params.push(...lastParams, ...sortAndPaginationParams); + } else { + params.push(...sortAndPaginationParams, ...lastParams, sort); } return params; } - findChannelsWithNumberOfMessagesBetweenDate(params: { + findChannelsByTypesWithNumberOfMessagesBetweenDate(params: { + types: Array; start: number; end: number; startOfLastWeek: number; @@ -541,22 +550,6 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { }); } - countChannelsWithNumberOfMessagesBetweenDate(params: { - start: number; - end: number; - startOfLastWeek: number; - endOfLastWeek: number; - options?: any; - }): AggregationCursor<{ total: number }> { - const aggregationParams = this.getChannelsWithNumberOfMessagesBetweenDateQuery(params); - aggregationParams.push({ $count: 'total' }); - - return this.col.aggregate<{ total: number }>(aggregationParams, { - allowDiskUse: true, - readPreference: readSecondaryPreferred(), - }); - } - findOneByNameOrFname(name: NonNullable, options: FindOptions = {}): Promise { const query = { $or: [ diff --git a/apps/meteor/tests/end-to-end/api/34-engagement-dashboard.ts b/apps/meteor/tests/end-to-end/api/34-engagement-dashboard.ts new file mode 100644 index 000000000000..c1fc685d11f9 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/34-engagement-dashboard.ts @@ -0,0 +1,347 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { after, before, describe, it } from 'mocha'; +import type { Response } from 'supertest'; + +import { getCredentials, api, request, credentials } from '../../data/api-data'; +import { sendSimpleMessage } from '../../data/chat.helper'; +import { updatePermission } from '../../data/permissions.helper'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; + +describe('[Engagement Dashboard]', function () { + this.retries(0); + + const isEnterprise = Boolean(process.env.IS_EE); + + before((done) => getCredentials(done)); + + before(() => updatePermission('view-engagement-dashboard', ['admin'])); + + after(() => updatePermission('view-engagement-dashboard', ['admin'])); + + (isEnterprise ? describe : describe.skip)('[/engagement-dashboard/channels/list]', () => { + let testRoom: IRoom; + + before(async () => { + testRoom = (await createRoom({ type: 'c', name: `channel.test.engagement.${Date.now()}-${Math.random()}` })).body.channel; + }); + + after(async () => { + await deleteRoom({ type: 'c', roomId: testRoom._id }); + }); + + it('should fail if user does not have the view-engagement-dashboard permission', async () => { + await updatePermission('view-engagement-dashboard', []); + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + offset: 0, + count: 25, + }) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]'); + }); + }); + + it('should fail if start param is not a valid date', async () => { + await updatePermission('view-engagement-dashboard', ['admin']); + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + start: 'invalid-date', + end: new Date().toISOString(), + offset: 0, + count: 25, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('Match error: Failed Match.Where validation in field start'); + }); + }); + + it('should fail if end param is not a valid date', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + end: 'invalid-date', + offset: 0, + count: 25, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('Match error: Failed Match.Where validation in field end'); + }); + }); + + it('should fail if start param is not provided', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date(), + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal("Match error: Missing key 'start'"); + }); + }); + + it('should fail if end param is not provided', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal("Match error: Missing key 'end'"); + }); + }); + + it('should succesfuly return results', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + expect(res.body.channels[0]).to.be.an('object').that.is.not.empty; + expect(res.body.channels[0]).to.have.property('messages').that.is.a('number'); + expect(res.body.channels[0]).to.have.property('lastWeekMessages').that.is.a('number'); + expect(res.body.channels[0]).to.have.property('diffFromLastWeek').that.is.a('number'); + expect(res.body.channels[0].room).to.be.an('object').that.is.not.empty; + + expect(res.body.channels[0].room).to.have.property('_id').that.is.a('string'); + expect(res.body.channels[0].room).to.have.property('name').that.is.a('string'); + expect(res.body.channels[0].room).to.have.property('ts').that.is.a('string'); + expect(res.body.channels[0].room).to.have.property('t').that.is.a('string'); + expect(res.body.channels[0].room).to.have.property('_updatedAt').that.is.a('string'); + }); + }); + + it('should not return empty rooms when the hideRoomsWithNoActivity param is provided', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + hideRoomsWithNoActivity: true, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).to.be.undefined; + }); + }); + + it('should correctly count messages in an empty room', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 0); + expect(channelRecord).to.have.property('lastWeekMessages', 0); + expect(channelRecord).to.have.property('diffFromLastWeek', 0); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + expect(channelRecord.room).to.have.property('_updatedAt', testRoom._updatedAt); + }); + }); + + it('should correctly count messages diff compared to last week when the hideRoomsWithNoActivity param is provided and there are messages in a room', async () => { + await sendSimpleMessage({ roomId: testRoom._id }); + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + hideRoomsWithNoActivity: true, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 1); + expect(channelRecord).to.have.property('lastWeekMessages', 0); + expect(channelRecord).to.have.property('diffFromLastWeek', 1); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + }); + }); + + it('should correctly count messages diff compared to last week when there are messages in a room', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 1); + expect(channelRecord).to.have.property('lastWeekMessages', 0); + expect(channelRecord).to.have.property('diffFromLastWeek', 1); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + }); + }); + + it('should correctly count messages from last week and diff when moving to the next week and providing the hideRoomsWithNoActivity param', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date(Date.now() + 8 * 24 * 60 * 60 * 1000).toISOString(), + start: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + hideRoomsWithNoActivity: true, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 0); + expect(channelRecord).to.have.property('lastWeekMessages', 1); + expect(channelRecord).to.have.property('diffFromLastWeek', -1); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + }); + }); + + it('should correctly count messages from last week and diff when moving to the next week', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date(Date.now() + 8 * 24 * 60 * 60 * 1000).toISOString(), + start: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 0); + expect(channelRecord).to.have.property('lastWeekMessages', 1); + expect(channelRecord).to.have.property('diffFromLastWeek', -1); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + }); + }); + }); +}); diff --git a/packages/model-typings/src/models/IAnalyticsModel.ts b/packages/model-typings/src/models/IAnalyticsModel.ts index 4d31f66a94d8..1ca4d7abf118 100644 --- a/packages/model-typings/src/models/IAnalyticsModel.ts +++ b/packages/model-typings/src/models/IAnalyticsModel.ts @@ -2,6 +2,7 @@ import type { IAnalytic, IRoom } from '@rocket.chat/core-typings'; import type { AggregationCursor, FindCursor, FindOptions, UpdateResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { IChannelsWithNumberOfMessagesBetweenDate } from './IRoomsModel'; export interface IAnalyticsModel extends IBaseModel { saveMessageSent({ room, date }: { room: IRoom; date: IAnalytic['date'] }): Promise; @@ -38,4 +39,12 @@ export interface IAnalyticsModel extends IBaseModel { users: number; }>; findByTypeBeforeDate({ type, date }: { type: IAnalytic['type']; date: IAnalytic['date'] }): FindCursor; + findRoomsByTypesWithNumberOfMessagesBetweenDate(params: { + types: Array; + start: number; + end: number; + startOfLastWeek: number; + endOfLastWeek: number; + options?: any; + }): AggregationCursor<{ channels: IChannelsWithNumberOfMessagesBetweenDate[]; total: number }>; } diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 887f51987ae6..f9daef91dece 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -86,7 +86,8 @@ export interface IRoomsModel extends IBaseModel { setTeamDefaultById(rid: IRoom['_id'], teamDefault: NonNullable, options?: UpdateOptions): Promise; - findChannelsWithNumberOfMessagesBetweenDate(params: { + findChannelsByTypesWithNumberOfMessagesBetweenDate(params: { + types: Array; start: number; end: number; startOfLastWeek: number; @@ -94,14 +95,6 @@ export interface IRoomsModel extends IBaseModel { options?: any; }): AggregationCursor; - countChannelsWithNumberOfMessagesBetweenDate(params: { - start: number; - end: number; - startOfLastWeek: number; - endOfLastWeek: number; - options?: any; - }): AggregationCursor<{ total: number }>; - findOneByName(name: NonNullable, options?: FindOptions): Promise; findDefaultRoomsForTeam(teamId: any): FindCursor;