From 9496f1eb978bcc76c847a8b87f308f5cb97fbf10 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 28 Aug 2023 16:06:24 -0600 Subject: [PATCH] chore: Deprecate analytics & transcript methods in favor of API endpoints (#29427) --- .changeset/tricky-years-swim.md | 6 ++ apps/meteor/app/livechat/server/api/rest.ts | 1 + .../app/livechat/server/api/v1/statistics.ts | 65 +++++++++++++ .../server/methods/discardTranscript.ts | 3 + .../livechat/server/methods/getAgentData.ts | 5 + .../server/methods/getAgentOverviewData.ts | 5 + .../omnichannel/analytics/AgentOverview.tsx | 13 +-- .../views/omnichannel/analytics/Overview.tsx | 20 ++-- .../counter/CounterItem.tsx | 2 +- .../tests/end-to-end/api/livechat/00-rooms.ts | 54 +++++++++++ .../end-to-end/api/livechat/04-dashboards.ts | 93 +++++++++++++++++++ packages/rest-typings/src/v1/omnichannel.ts | 74 +++++++++++++++ 12 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 .changeset/tricky-years-swim.md create mode 100644 apps/meteor/app/livechat/server/api/v1/statistics.ts diff --git a/.changeset/tricky-years-swim.md b/.changeset/tricky-years-swim.md new file mode 100644 index 00000000000..2ab1254525b --- /dev/null +++ b/.changeset/tricky-years-swim.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Deprecate `livechat:getOverviewData` and `livechat:getAgentOverviewData` methods and create API endpoints `livechat/analytics/overview` and `livechat/analytics/agent-overview` to fetch analytics data diff --git a/apps/meteor/app/livechat/server/api/rest.ts b/apps/meteor/app/livechat/server/api/rest.ts index d1eb008ab4d..f9da6690185 100644 --- a/apps/meteor/app/livechat/server/api/rest.ts +++ b/apps/meteor/app/livechat/server/api/rest.ts @@ -12,3 +12,4 @@ import './v1/transfer'; import './v1/contact'; import './v1/webhooks'; import './v1/integration'; +import './v1/statistics'; diff --git a/apps/meteor/app/livechat/server/api/v1/statistics.ts b/apps/meteor/app/livechat/server/api/v1/statistics.ts new file mode 100644 index 00000000000..078f366bb48 --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/statistics.ts @@ -0,0 +1,65 @@ +import { Users } from '@rocket.chat/models'; +import { isLivechatAnalyticsAgentOverviewProps, isLivechatAnalyticsOverviewProps } from '@rocket.chat/rest-typings'; + +import { API } from '../../../../api/server'; +import { settings } from '../../../../settings/server'; +import { Livechat } from '../../lib/Livechat'; + +API.v1.addRoute( + 'livechat/analytics/agent-overview', + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsAgentOverviewProps, + }, + { + async get() { + const { name, departmentId, from, to } = this.queryParams; + + if (!name) { + throw new Error('invalid-chart-name'); + } + + const user = await Users.findOneById(this.userId, { projection: { _id: 1, utcOffset: 1 } }); + return API.v1.success( + await Livechat.Analytics.getAgentOverviewData({ + departmentId, + utcOffset: user?.utcOffset || 0, + daterange: { from, to }, + chartOptions: { name }, + }), + ); + }, + }, +); + +API.v1.addRoute( + 'livechat/analytics/overview', + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsOverviewProps, + }, + { + async get() { + const { name, departmentId, from, to } = this.queryParams; + + if (!name) { + throw new Error('invalid-chart-name'); + } + + const user = await Users.findOneById(this.userId, { projection: { _id: 1, utcOffset: 1 } }); + const language = user?.language || settings.get('Language') || 'en'; + + return API.v1.success( + await Livechat.Analytics.getAnalyticsOverviewData({ + departmentId, + utcOffset: user?.utcOffset || 0, + daterange: { from, to }, + analyticsOptions: { name }, + language, + }), + ); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/methods/discardTranscript.ts b/apps/meteor/app/livechat/server/methods/discardTranscript.ts index 2e03b6ebd92..d46c8ffea35 100644 --- a/apps/meteor/app/livechat/server/methods/discardTranscript.ts +++ b/apps/meteor/app/livechat/server/methods/discardTranscript.ts @@ -17,6 +17,9 @@ Meteor.methods({ async 'livechat:discardTranscript'(rid: string) { methodDeprecationLogger.method('livechat:discardTranscript', '7.0.0'); check(rid, String); + methodDeprecationLogger.warn( + 'The method "livechat:discardTranscript" is deprecated and will be removed after version v7.0.0. Use "livechat/transcript/:rid" (DELETE) instead.', + ); const user = Meteor.userId(); diff --git a/apps/meteor/app/livechat/server/methods/getAgentData.ts b/apps/meteor/app/livechat/server/methods/getAgentData.ts index f4ba67ebe10..d32d24d7f7c 100644 --- a/apps/meteor/app/livechat/server/methods/getAgentData.ts +++ b/apps/meteor/app/livechat/server/methods/getAgentData.ts @@ -4,6 +4,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { settings } from '../../../settings/server'; declare module '@rocket.chat/ui-contexts' { @@ -21,6 +22,10 @@ Meteor.methods({ check(roomId, String); check(token, String); + methodDeprecationLogger.warn( + 'The method "livechat:getAgentData" is deprecated and will be removed after version v7.0.0. Use "livechat/agent.info/:rid/:token" instead.', + ); + const room = await LivechatRooms.findOneById(roomId); const visitor = await LivechatVisitors.getVisitorByToken(token); diff --git a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts index afbc2e8bb7f..94fae239b74 100644 --- a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts +++ b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts @@ -3,6 +3,7 @@ import type { ServerMethods, TranslationKey } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { Livechat } from '../lib/Livechat'; declare module '@rocket.chat/ui-contexts' { @@ -17,6 +18,10 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:getAgentOverviewData'(options) { + methodDeprecationLogger.warn( + 'The method "livechat:getAgentOverviewData" is deprecated and will be removed after version v7.0.0. Use "livechat/analytics/agent-overview" instead.', + ); + const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { diff --git a/apps/meteor/client/views/omnichannel/analytics/AgentOverview.tsx b/apps/meteor/client/views/omnichannel/analytics/AgentOverview.tsx index a34b9af8bb7..b9f0abd6b14 100644 --- a/apps/meteor/client/views/omnichannel/analytics/AgentOverview.tsx +++ b/apps/meteor/client/views/omnichannel/analytics/AgentOverview.tsx @@ -1,6 +1,6 @@ import { Table, TableBody, TableCell, TableHead, TableRow } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo, useEffect, useState } from 'react'; const style = { width: '100%' }; @@ -19,19 +19,20 @@ const AgentOverview = ({ const params = useMemo( () => ({ - chartOptions: { name: type }, - daterange: { from: start, to: end }, + name: type, + from: start, + to: end, ...(departmentId && { departmentId }), }), [departmentId, end, start, type], ); - const [displayData, setDisplayData] = useState<{ head: { name: TranslationKey }[]; data: { name: string; value: number | string }[] }>({ + const [displayData, setDisplayData] = useState<{ head: { name: string }[]; data: { name: string; value: number | string }[] }>({ head: [], data: [], }); - const loadData = useMethod('livechat:getAgentOverviewData'); + const loadData = useEndpoint('GET', '/v1/livechat/analytics/agent-overview'); useEffect(() => { async function fetchData() { @@ -49,7 +50,7 @@ const AgentOverview = ({ {displayData.head?.map(({ name }, i) => ( - {t(name)} + {t(name as TranslationKey)} ))} diff --git a/apps/meteor/client/views/omnichannel/analytics/Overview.tsx b/apps/meteor/client/views/omnichannel/analytics/Overview.tsx index 82049c36fb0..075d67bf7f1 100644 --- a/apps/meteor/client/views/omnichannel/analytics/Overview.tsx +++ b/apps/meteor/client/views/omnichannel/analytics/Overview.tsx @@ -1,12 +1,12 @@ import { Box, Skeleton } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState, useMemo } from 'react'; import CounterItem from '../realTimeMonitoring/counter/CounterItem'; import CounterRow from '../realTimeMonitoring/counter/CounterRow'; -const initialData: { title?: TranslationKey; value: string }[] = Array.from({ length: 3 }).map(() => ({ title: undefined, value: '' })); +const initialData: { title?: string; value: string | number }[] = Array.from({ length: 3 }).map(() => ({ title: undefined, value: '' })); const conversationsInitialData = [initialData, initialData]; const productivityInitialData = [initialData]; @@ -18,14 +18,15 @@ const Overview = ({ type, dateRange, departmentId }: { type: string; dateRange: const params = useMemo( () => ({ - analyticsOptions: { name: type }, - daterange: { from: start, to: end }, + name: type, + from: start, + to: end, ...(departmentId && { departmentId }), }), [departmentId, end, start, type], ); - const loadData = useMethod('livechat:getAnalyticsOverviewData'); + const loadData = useEndpoint('GET', '/v1/livechat/analytics/overview'); const [displayData, setDisplayData] = useState(conversationsInitialData); @@ -58,7 +59,14 @@ const Overview = ({ type, dateRange, departmentId }: { type: string; dateRange: {displayData.map((items = [], i) => ( {items.map(({ title, value }, i) => ( - } count={value} /> + } + count={value} + /> ))} ))} diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/counter/CounterItem.tsx b/apps/meteor/client/views/omnichannel/realTimeMonitoring/counter/CounterItem.tsx index b625cb8aa6b..93cb239df8a 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/counter/CounterItem.tsx +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/counter/CounterItem.tsx @@ -7,7 +7,7 @@ const CounterItem = ({ ...props }: { title: string | JSX.Element; - count: string; + count: string | number; flexShrink?: number; pb?: number; flexBasis?: string; diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 831ad54a33c..8dcbad11821 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -2043,4 +2043,58 @@ describe('LIVECHAT - rooms', function () { expect(unread).to.equal(totalMessagesSent); }); }); + + describe('livechat/transcript/:rid', () => { + it('should fail if user is not logged in', async () => { + await request.delete(api('livechat/transcript/rid')).expect(401); + }); + it('should fail if user doesnt have send-omnichannel-chat-transcript permission', async () => { + await updatePermission('send-omnichannel-chat-transcript', []); + await request.delete(api('livechat/transcript/rid')).set(credentials).expect(403); + }); + it('should fail if :rid is not a valid room id', async () => { + await updatePermission('send-omnichannel-chat-transcript', ['admin']); + await request.delete(api('livechat/transcript/rid')).set(credentials).expect(400); + }); + it('should fail if room is closed', async () => { + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + await closeOmnichannelRoom(_id); + await request + .delete(api(`livechat/transcript/${_id}`)) + .set(credentials) + .expect(400); + }); + it('should fail if room doesnt have a transcript request active', async () => { + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + await request + .delete(api(`livechat/transcript/${_id}`)) + .set(credentials) + .expect(400); + }); + it('should return OK if all conditions are met', async () => { + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + // First, request transcript with livechat:requestTranscript method + await request + .post(methodCall('livechat:requestTranscript')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:requestTranscript', + params: [_id, 'test@test.com', 'Transcript of your omnichannel conversation'], + id: 'id', + msg: 'method', + }), + }) + .expect(200); + + // Then, delete the transcript + await request + .delete(api(`livechat/transcript/${_id}`)) + .set(credentials) + .expect(200); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts index caf8d0bac2d..61a2719d9cb 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts @@ -259,4 +259,97 @@ describe('LIVECHAT - dashboards', function () { }); }); }); + + describe('livechat/analytics/agent-overview', () => { + it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { + await updatePermission('view-livechat-manager', []); + await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: '2020-01-01', to: '2020-01-02', name: 'Total_conversations' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403); + }); + it('should return an "invalid-chart-name error" when the chart name is empty', async () => { + await updatePermission('view-livechat-manager', ['admin']); + await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: '2020-01-01', to: '2020-01-02', name: '' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400); + }); + it('should return empty when chart name is invalid', async () => { + await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: '2020-01-01', to: '2020-01-02', name: 'invalid-chart-name' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(Object.keys(res.body)).to.have.lengthOf(1); + }); + }); + it('should return an array of agent overview data', async () => { + const result = await request + .get(api('livechat/analytics/agent-overview')) + .query({ from: '2020-01-01', to: '2020-01-02', name: 'Total_conversations' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.have.property('success', true); + expect(result.body).to.have.property('head'); + expect(result.body).to.have.property('data'); + expect(result.body.head).to.be.an('array'); + expect(result.body.data).to.be.an('array'); + }); + }); + + describe('livechat/analytics/overview', () => { + it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { + await updatePermission('view-livechat-manager', []); + await request + .get(api('livechat/analytics/overview')) + .query({ from: '2020-01-01', to: '2020-01-02', name: 'Conversations' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403); + }); + it('should return an "invalid-chart-name error" when the chart name is empty', async () => { + await updatePermission('view-livechat-manager', ['admin']); + await request + .get(api('livechat/analytics/overview')) + .query({ from: '2020-01-01', to: '2020-01-02', name: '' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400); + }); + it('should return empty when chart name is invalid', async () => { + await request + .get(api('livechat/analytics/overview')) + .query({ from: '2020-01-01', to: '2020-01-02', name: 'invalid-chart-name' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(Object.keys(res.body)).to.have.lengthOf(1); + }); + }); + it('should return an array of analytics overview data', async () => { + const result = await request + .get(api('livechat/analytics/overview')) + .query({ from: '2020-01-01', to: '2020-01-02', name: 'Conversations' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(result.body).to.be.an('array'); + expect(result.body).to.have.lengthOf(7); + expect(result.body[0]).to.have.property('title', 'Total_conversations'); + expect(result.body[0]).to.have.property('value', 0); + }); + }); }); diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 1ee29ea555c..9f28cbad1a8 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -3121,6 +3121,68 @@ const POSTLivechatAppearanceParamsSchema = { export const isPOSTLivechatAppearanceParams = ajv.compile(POSTLivechatAppearanceParamsSchema); +type LivechatAnalyticsAgentOverviewProps = { + name: string; + from: string; + to: string; + departmentId?: string; +}; + +const LivechatAnalyticsAgentOverviewPropsSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + from: { + type: 'string', + }, + to: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['name', 'from', 'to'], + additionalProperties: false, +}; + +export const isLivechatAnalyticsAgentOverviewProps = ajv.compile( + LivechatAnalyticsAgentOverviewPropsSchema, +); + +type LivechatAnalyticsOverviewProps = { + name: string; + from: string; + to: string; + departmentId?: string; +}; + +const LivechatAnalyticsOverviewPropsSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + from: { + type: 'string', + }, + to: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['name', 'from', 'to'], + additionalProperties: false, +}; + +export const isLivechatAnalyticsOverviewProps = ajv.compile(LivechatAnalyticsOverviewPropsSchema); + export type OmnichannelEndpoints = { '/v1/livechat/appearance': { GET: () => { @@ -3589,6 +3651,18 @@ export type OmnichannelEndpoints = { '/v1/livechat/rooms/filters': { GET: () => { filters: IOmnichannelRoom['source'][] }; }; + '/v1/livechat/analytics/agent-overview': { + GET: (params: LivechatAnalyticsAgentOverviewProps) => { + head: { name: string }[]; + data: { name: string; value: number }[]; + }; + }; + '/v1/livechat/analytics/overview': { + GET: (params: LivechatAnalyticsOverviewProps) => { + title: string; + value: string | number; + }[]; + }; } & { // EE '/v1/livechat/analytics/agents/average-service-time': {