diff --git a/Composer/packages/client/src/components/WebChat/WebChatPanel.tsx b/Composer/packages/client/src/components/WebChat/WebChatPanel.tsx index 022ef29975..a96fe143c0 100644 --- a/Composer/packages/client/src/components/WebChat/WebChatPanel.tsx +++ b/Composer/packages/client/src/components/WebChat/WebChatPanel.tsx @@ -10,6 +10,7 @@ import { } from '@botframework-composer/types'; import { AxiosResponse } from 'axios'; import formatMessage from 'format-message'; +import { v4 as uuid } from 'uuid'; import TelemetryClient from '../../telemetry/TelemetryClient'; import { BotStatus } from '../../constants'; @@ -79,6 +80,7 @@ export const WebChatPanel: React.FC = ({ projectId, data.activities.map((a) => ({ activity: a, + id: uuid(), timestamp: new Date(a.timestamp || Date.now()).getTime(), trafficType: data.trafficType, })) @@ -104,6 +106,7 @@ export const WebChatPanel: React.FC = ({ error: { message: formatMessage('An error occurred connecting initializing the DirectLine server'), }, + id: uuid(), request: { route: 'conversations/ws/port', method: 'GET', payload: {} }, response: { payload: response.data, statusCode: response.status }, timestamp: Date.now(), @@ -211,6 +214,7 @@ export const WebChatPanel: React.FC = ({ error: { message: formatMessage('An error occurred saving transcripts'), }, + id: uuid(), request: { route: 'saveTranscripts/', method: '', payload: {} }, response: { payload: ex, statusCode: 400 }, timestamp: Date.now(), diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogContent.tsx index ac1dd52c3b..b4d7e27aec 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogContent.tsx @@ -7,6 +7,7 @@ import React, { useMemo, useEffect, useState, useRef, useCallback } from 'react' import { useRecoilValue } from 'recoil'; import { ConversationTrafficItem } from '@botframework-composer/types/src'; import formatMessage from 'format-message'; +import debounce from 'lodash/debounce'; import { dispatcherState, @@ -42,6 +43,10 @@ const logPane = css` box-sizing: border-box; `; +const itemIsSelected = (item: ConversationTrafficItem, currentInspectionData?: WebChatInspectionData) => { + return item.id === currentInspectionData?.item?.id; +}; + // R12: We are showing Errors from the root bot only. export const WebChatLogContent: React.FC = ({ isActive }) => { const currentProjectId = useRecoilValue(rootBotProjectIdSelector); @@ -59,9 +64,33 @@ export const WebChatLogContent: React.FC = ({ isActive } }; + const performInspection = useRef( + debounce((trafficItem: ConversationTrafficItem) => { + if (currentProjectId) { + if (trafficItem?.trafficType === 'network') { + // default to inspecting the request body + setWebChatInspectionData(currentProjectId, { item: trafficItem, mode: 'request' }); + } else { + setWebChatInspectionData(currentProjectId, { item: trafficItem }); + } + } + }, 500) + ).current; + + const inspectLatestLogMessage = () => { + // inspect latest log message if nothing is being inspected + if (!inspectionData && currentProjectId) { + const latestTrafficItem = [...rawWebChatTraffic].pop(); + if (latestTrafficItem) { + performInspection(latestTrafficItem); + } + } + }; + useEffect(() => { if (navigateToLatestEntry && isActive) { navigateToNewestLogEntry(); + inspectLatestLogMessage(); navigateToLatestEntryWhenActive(false); } }, [isActive, navigateToLatestEntry]); @@ -80,16 +109,37 @@ export const WebChatLogContent: React.FC = ({ isActive ); const renderLogItem = useCallback( - (item: ConversationTrafficItem, index: number) => { + (item: ConversationTrafficItem, index: number, inspectionData?: WebChatInspectionData) => { switch (item.trafficType) { case 'activity': - return ; + return ( + + ); case 'network': - return ; + return ( + + ); case 'networkError': - return ; + return ( + + ); default: return null; @@ -99,12 +149,10 @@ export const WebChatLogContent: React.FC = ({ isActive ); const displayedTraffic = useMemo(() => { - const sortedTraffic = [...rawWebChatTraffic] - .sort((t1, t2) => t1.timestamp - t2.timestamp) - .map((t, i) => renderLogItem(t, i)); - setLogItemCount(sortedTraffic.length); - return sortedTraffic; - }, [rawWebChatTraffic, renderLogItem]); + const renderedTraffic = [...rawWebChatTraffic].map((t, i) => renderLogItem(t, i, inspectionData)); + setLogItemCount(renderedTraffic.length); + return renderedTraffic; + }, [inspectionData, rawWebChatTraffic, renderLogItem]); const setInspectionData = (data: WebChatInspectionData) => { if (currentProjectId) { diff --git a/Composer/packages/client/src/pages/design/__tests__/WebChatLogContent.test.tsx b/Composer/packages/client/src/pages/design/__tests__/WebChatLogContent.test.tsx index 67ed5f93c9..5558e43809 100644 --- a/Composer/packages/client/src/pages/design/__tests__/WebChatLogContent.test.tsx +++ b/Composer/packages/client/src/pages/design/__tests__/WebChatLogContent.test.tsx @@ -31,8 +31,9 @@ describe('', () => { }); set(webChatTrafficState(rootBotId), [ { - trafficType: 'activity', activity: {} as any, + id: '', + trafficType: 'activity', timestamp: Date.now(), }, ]); @@ -49,8 +50,9 @@ describe('', () => { }); set(webChatTrafficState(rootBotId), [ { - trafficType: 'activity', activity: {} as any, + id: '', + trafficType: 'activity', timestamp: Date.now(), }, ]); diff --git a/Composer/packages/client/src/pages/design/__tests__/WebChatLogItemHeader.test.tsx b/Composer/packages/client/src/pages/design/__tests__/WebChatLogItemHeader.test.tsx index 7ceede00bf..69240c6892 100644 --- a/Composer/packages/client/src/pages/design/__tests__/WebChatLogItemHeader.test.tsx +++ b/Composer/packages/client/src/pages/design/__tests__/WebChatLogItemHeader.test.tsx @@ -35,6 +35,7 @@ describe('', () => { error: { message: 'Error validating Microsoft App ID and Password', }, + id: '', timestamp: Date.now(), trafficType: 'networkError', request: { @@ -61,6 +62,7 @@ describe('', () => { error: { message: 'Error validating Microsoft App ID and Password', }, + id: '', timestamp: Date.now(), trafficType: 'networkError', request: { @@ -88,6 +90,7 @@ describe('', () => { error: { message: 'Error validating Microsoft App ID and Password', }, + id: '', timestamp: Date.now(), trafficType: 'networkError', request: { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/webchat.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/webchat.test.tsx index 6a433e6de6..db6b95706a 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/webchat.test.tsx +++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/webchat.test.tsx @@ -51,6 +51,7 @@ describe('web chat dispatcher', () => { it('should append a single web chat traffic item to the log', async () => { const trafficItem = { activity: {} as any, + id: '', timestamp: Date.now(), trafficType: 'activity' as 'activity', }; @@ -64,11 +65,13 @@ describe('web chat dispatcher', () => { it('should append multiple web chat traffic items to the log', async () => { const trafficItem1 = { activity: {} as any, + id: '', timestamp: Date.now(), trafficType: 'activity' as 'activity', }; const trafficItem2 = { activity: {} as any, + id: '', timestamp: Date.now() + 5, trafficType: 'activity' as 'activity', }; @@ -82,6 +85,7 @@ describe('web chat dispatcher', () => { it('should clear traffic from the log', async () => { const trafficItem = { activity: {} as any, + id: '', timestamp: Date.now(), trafficType: 'activity' as 'activity', }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts index 4e1b5e350a..7d9f02d82d 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts @@ -12,6 +12,7 @@ export const webChatLogDispatcher = () => { const clearWebChatLogs = useRecoilCallback((callbackHelpers: CallbackInterface) => (projectId: string) => { const { set } = callbackHelpers; set(webChatTrafficState(projectId), []); + set(webChatInspectionDataState(projectId), undefined); // clear the inspection panel }); const setWebChatPanelVisibility = useRecoilCallback((callbackHelpers: CallbackInterface) => (value: boolean) => { @@ -27,9 +28,9 @@ export const webChatLogDispatcher = () => { const { set } = callbackHelpers; set(webChatTrafficState(projectId), (currentTraffic) => { if (Array.isArray(traffic)) { - return [...currentTraffic, ...traffic]; + return [...currentTraffic, ...traffic].sort((t1, t2) => t1.timestamp - t2.timestamp); } else { - return [...currentTraffic, traffic]; + return [...currentTraffic, traffic].sort((t1, t2) => t1.timestamp - t2.timestamp); } }); } diff --git a/Composer/packages/server/src/middleware/logNetworkTraffic.ts b/Composer/packages/server/src/middleware/logNetworkTraffic.ts index b9126cb977..175cd5a552 100644 --- a/Composer/packages/server/src/middleware/logNetworkTraffic.ts +++ b/Composer/packages/server/src/middleware/logNetworkTraffic.ts @@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from 'express'; import { ConversationNetworkErrorItem, ConversationNetworkTrafficItem } from '@botframework-composer/types'; +import { v4 as uuid } from 'uuid'; import { WebSocketServer } from '../directline/utils/webSocketServer'; @@ -25,6 +26,7 @@ export function logNetworkTraffic(req: Request, res: Response, next?: NextFuncti details: error.details, message: error.message, }, + id: uuid(), request: { method: req.method, payload: req.body, route: req.originalUrl }, response: { payload: JSON.parse((res as any).sentData || '{}'), @@ -36,6 +38,7 @@ export function logNetworkTraffic(req: Request, res: Response, next?: NextFuncti } else { // a successful response was sent to the client data = { + id: uuid(), request: { method: req.method, payload: req.body, route: req.originalUrl }, response: { payload: JSON.parse((res as any).sentData || '{}'), diff --git a/Composer/packages/types/src/server.ts b/Composer/packages/types/src/server.ts index 7a932e745d..6ee8b1a508 100644 --- a/Composer/packages/types/src/server.ts +++ b/Composer/packages/types/src/server.ts @@ -65,6 +65,7 @@ export type NetworkTrafficResponse = { }; export type ConversationNetworkTrafficItem = { + id: string; request: NetworkTrafficRequest; response: NetworkTrafficResponse; timestamp: number; @@ -78,11 +79,13 @@ export type ConversationActivityTraffic = { export type ConversationActivityTrafficItem = { activity: Activity; + id: string; timestamp: number; trafficType: 'activity'; }; export type ConversationNetworkErrorItem = { + id: string; error: { message: string; details?: string;