chore: Deprecate analytics & transcript methods in favor of API endpoints (#29427)

pull/30201/head^2
Kevin Aleman 2 years ago committed by GitHub
parent 064c48c238
commit 9496f1eb97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .changeset/tricky-years-swim.md
  2. 1
      apps/meteor/app/livechat/server/api/rest.ts
  3. 65
      apps/meteor/app/livechat/server/api/v1/statistics.ts
  4. 3
      apps/meteor/app/livechat/server/methods/discardTranscript.ts
  5. 5
      apps/meteor/app/livechat/server/methods/getAgentData.ts
  6. 5
      apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts
  7. 13
      apps/meteor/client/views/omnichannel/analytics/AgentOverview.tsx
  8. 20
      apps/meteor/client/views/omnichannel/analytics/Overview.tsx
  9. 2
      apps/meteor/client/views/omnichannel/realTimeMonitoring/counter/CounterItem.tsx
  10. 54
      apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts
  11. 93
      apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts
  12. 74
      packages/rest-typings/src/v1/omnichannel.ts

@ -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

@ -12,3 +12,4 @@ import './v1/transfer';
import './v1/contact';
import './v1/webhooks';
import './v1/integration';
import './v1/statistics';

@ -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,
}),
);
},
},
);

@ -17,6 +17,9 @@ Meteor.methods<ServerMethods>({
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();

@ -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<ServerMethods>({
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);

@ -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<ServerMethods>({
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', {

@ -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 = ({
<TableHead>
<TableRow>
{displayData.head?.map(({ name }, i) => (
<TableCell key={i}>{t(name)}</TableCell>
<TableCell key={i}>{t(name as TranslationKey)}</TableCell>
))}
</TableRow>
</TableHead>

@ -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) => (
<CounterRow key={i} border='0' pb='none'>
{items.map(({ title, value }, i) => (
<CounterItem flexShrink={1} pb={8} flexBasis='100%' key={i} title={title ? t(title) : <Skeleton width='x60' />} count={value} />
<CounterItem
flexShrink={1}
pb={8}
flexBasis='100%'
key={i}
title={title ? t(title as TranslationKey) : <Skeleton width='x60' />}
count={value}
/>
))}
</CounterRow>
))}

@ -7,7 +7,7 @@ const CounterItem = ({
...props
}: {
title: string | JSX.Element;
count: string;
count: string | number;
flexShrink?: number;
pb?: number;
flexBasis?: string;

@ -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);
});
});
});

@ -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);
});
});
});

@ -3121,6 +3121,68 @@ const POSTLivechatAppearanceParamsSchema = {
export const isPOSTLivechatAppearanceParams = ajv.compile<POSTLivechatAppearanceParams>(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<LivechatAnalyticsAgentOverviewProps>(
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<LivechatAnalyticsOverviewProps>(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': {

Loading…
Cancel
Save