Skip to content

Commit

Permalink
feat: Added network and activity logs to Web Chat log panel (microsof…
Browse files Browse the repository at this point in the history
…t#6638)

* Added fadedWhenReadOnly option to JsonEditor

* Added activity tracking and inspection panel

* Plumbed data through to Web Chat inspection panel.

* More style polish

* Made inspector resizable and did some renames

* Plumbed network response data into log

* Fixed logging network responses to client

* Condensed network and activity traffic into one socket

* Consolidated network errors into traffic channel

* Cleaned up types and recoil code

* Factored out client code into smaller components

* Only show error indicator when unread errors are present

* Fixed client tests

* Fixed server tests.

* Removing old name directory

* Adding back properly named dir

* PR comments

* Fixed timestamps

* More PR comments addressed

* Fixed tests

* Addressed more PR comments

* Fixed types

* Added test for logNetworkTraffic

* Fixed test.

* Updated recoil dispatcher names to be consistent.

* Added web chat state tests

* Adjusted fonts in both log and inspector panes to match

* Merge conflicts resolved

Signed-off-by: Srinaath Ravichandran <[email protected]>

* Smoothing out merge conflict

* Moved network logging middleware to specific routes

* Fixed attachment route tests.

Co-authored-by: Ben Yackley <[email protected]>
Co-authored-by: Srinaath Ravichandran <[email protected]>
Co-authored-by: Dong Lei <[email protected]>
Co-authored-by: Srinaath Ravichandran <[email protected]>
  • Loading branch information
5 people authored Apr 7, 2021
1 parent 6a4b903 commit 0dd130a
Show file tree
Hide file tree
Showing 44 changed files with 1,070 additions and 398 deletions.
94 changes: 63 additions & 31 deletions Composer/packages/client/src/components/WebChat/WebChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@

import React, { useMemo, useEffect, useState, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { DirectLineLog } from '@botframework-composer/types';
import {
ConversationActivityTraffic,
ConversationNetworkTrafficItem,
ConversationNetworkErrorItem,
} from '@botframework-composer/types';
import { AxiosResponse } from 'axios';
import formatMessage from 'format-message';

import TelemetryClient from '../../telemetry/TelemetryClient';
import { BotStatus } from '../../constants';
import { dispatcherState } from '../../recoilModel';

import { ConversationService, ChatData, BotSecrets, getDateTimeFormatted } from './utils/conversationService';
import { ConversationService, ChatData, BotSecrets } from './utils/conversationService';
import { WebChatHeader } from './WebChatHeader';
import { WebChatContainer } from './WebChatContainer';
import { RestartOption } from './type';
Expand Down Expand Up @@ -39,7 +43,7 @@ export const WebChatPanel: React.FC<WebChatPanelProps> = ({
}) => {
const {
openBotInEmulator,
appendLogToWebChatInspector,
appendWebChatTraffic,
clearWebChatLogs,
setDebugPanelExpansion,
setActiveTabInDebugPanel,
Expand All @@ -50,35 +54,62 @@ export const WebChatPanel: React.FC<WebChatPanelProps> = ({
const conversationService = useMemo(() => new ConversationService(directlineHostUrl), [directlineHostUrl]);
const webChatPanelRef = useRef<HTMLDivElement>(null);
const [currentRestartOption, onSetRestartOption] = useState<RestartOption>(RestartOption.NewUserID);
const directLineErrorChannel = useRef<WebSocket>();
const webChatTrafficChannel = useRef<WebSocket>();

useEffect(() => {
const bootstrapChat = async () => {
const conversationServerPort = await conversationService.setUpConversationServer();
try {
directLineErrorChannel.current = new WebSocket(
`ws://localhost:${conversationServerPort}/ws/errors/createErrorChannel`
);
if (directLineErrorChannel.current) {
directLineErrorChannel.current.onmessage = (event) => {
const data: DirectLineLog = JSON.parse(event.data);
appendLogToWebChatInspector(projectId, data);
setTimeout(() => {
setActiveTabInDebugPanel('WebChatInspector');
setDebugPanelExpansion(true);
}, 300);
// set up Web Chat traffic listener
webChatTrafficChannel.current = new WebSocket(`ws://localhost:${conversationServerPort}/ws/traffic`);
if (webChatTrafficChannel.current) {
webChatTrafficChannel.current.onmessage = (event) => {
const data:
| ConversationActivityTraffic
| ConversationNetworkTrafficItem
| ConversationNetworkErrorItem = JSON.parse(event.data);

switch (data.trafficType) {
case 'network': {
appendWebChatTraffic(projectId, data);
break;
}
case 'activity': {
appendWebChatTraffic(
projectId,
data.activities.map((a) => ({
activity: a,
timestamp: new Date(a.timestamp || Date.now()).getTime(),
trafficType: data.trafficType,
}))
);
break;
}
case 'networkError': {
appendWebChatTraffic(projectId, data);
setTimeout(() => {
setActiveTabInDebugPanel('WebChatInspector');
setDebugPanelExpansion(true);
}, 300);
break;
}
default:
break;
}
};
}
} catch (ex) {
const response: AxiosResponse = ex.response;
const err: DirectLineLog = {
timestamp: getDateTimeFormatted(),
route: 'conversations/ws/port',
status: response.status,
logType: 'Error',
message: formatMessage('An error occurred connecting initializing the DirectLine server'),
const err: ConversationNetworkErrorItem = {
error: {
message: formatMessage('An error occurred connecting initializing the DirectLine server'),
},
request: { route: 'conversations/ws/port', method: 'GET', payload: {} },
response: { payload: response.data, statusCode: response.status },
timestamp: Date.now(),
trafficType: 'networkError',
};
appendLogToWebChatInspector(projectId, err);
appendWebChatTraffic(projectId, err);
setActiveTabInDebugPanel('WebChatInspector');
setDebugPanelExpansion(true);
}
Expand All @@ -87,7 +118,7 @@ export const WebChatPanel: React.FC<WebChatPanelProps> = ({
bootstrapChat();

return () => {
directLineErrorChannel.current?.close();
webChatTrafficChannel.current?.close();
};
}, []);

Expand Down Expand Up @@ -176,15 +207,16 @@ export const WebChatPanel: React.FC<WebChatPanelProps> = ({
TelemetryClient.track('SaveTranscriptClicked');
webChatPanelRef.current?.removeChild(downloadLink);
} catch (ex) {
const err: DirectLineLog = {
timestamp: getDateTimeFormatted(),
route: 'saveTranscripts/',
status: 400,
logType: 'Error',
message: formatMessage('An error occurred saving transcripts'),
details: ex.message,
const err: ConversationNetworkErrorItem = {
error: {
message: formatMessage('An error occurred saving transcripts'),
},
request: { route: 'saveTranscripts/', method: '', payload: {} },
response: { payload: ex, statusCode: 400 },
timestamp: Date.now(),
trafficType: 'networkError',
};
appendLogToWebChatInspector(projectId, err);
appendWebChatTraffic(projectId, err);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { createStore as createWebChatStore } from 'botframework-webchat-core';
import { createDirectLine } from 'botframework-webchat';
import moment from 'moment';
import { DirectLineLog } from '@bfc/shared';
import formatMessage from 'format-message';

export type User = {
Expand Down Expand Up @@ -255,7 +254,7 @@ export class ConversationService {
});
} catch (ex) {
const response: AxiosResponse = ex.response;
const err: DirectLineLog = {
const err = {
timestamp: getDateTimeFormatted(),
route: 'conversations/ws/port',
status: response.status,
Expand All @@ -277,7 +276,7 @@ export class ConversationService {
});
} catch (ex) {
const response: AxiosResponse = ex.response;
const err: DirectLineLog = {
const err = {
timestamp: getDateTimeFormatted(),
route: response.request?.path ?? '',
status: response.status,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { css, jsx } from '@emotion/core';
import { NeutralColors, SharedColors } from '@uifabric/fluent-theme';

const timestampStyle = css`
color: ${SharedColors.green20};
padding-right: 6px;
`;

const timestampBracket = css`
color: ${NeutralColors.gray130};
`;

export const renderActivityArrow = (activity) => {
if (activity?.recipient && activity.recipient.role === 'bot') {
return <span>{'->'}</span>;
}
return <span>{'<-'}</span>;
};

export const renderTimeStamp = (timestamp: number, appLocale: string) => {
return (
<span css={timestampStyle}>
<span css={timestampBracket}>[</span>
{new Intl.DateTimeFormat(appLocale, { hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(timestamp)}
<span css={timestampBracket}>]</span>
</span>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { css, jsx } from '@emotion/core';
import { ConversationActivityTrafficItem } from '@botframework-composer/types';
import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';

import { WebChatInspectionData } from '../../../../../recoilModel/types';
import { userSettingsState } from '../../../../../recoilModel';

import { renderTimeStamp } from './LogItemHelpers';
import { clickableSegment, emphasizedText, hoverItem, logItem } from './logItemStyles';

const clickable = css`
cursor: pointer;
`;

const renderActivityArrow = (activity) => {
if (activity?.recipient?.role === 'bot') {
return <span>{'->'}</span>;
}
return <span>{'<-'}</span>;
};

type WebChatActivityLogItemProps = {
index: number;
item: ConversationActivityTrafficItem;
isSelected?: boolean;
onClickTraffic: (data: WebChatInspectionData) => void;
};

export const WebChatActivityLogItem: React.FC<WebChatActivityLogItemProps> = (props) => {
const { index, item, isSelected = false, onClickTraffic } = props;
const { appLocale } = useRecoilValue(userSettingsState);

const onClick = useCallback(() => {
onClickTraffic({ item });
}, [item, onClickTraffic]);

return (
<span key={`webchat-activity-item-${index}`} css={[clickable, hoverItem(isSelected), logItem]} onClick={onClick}>
{renderTimeStamp(item.timestamp, appLocale)}
{renderActivityArrow(item.activity)}
<span css={clickableSegment}>{item.activity.type || 'unknown'}</span>
{item.activity.type === 'message' ? <span css={emphasizedText}>{item.activity.text}</span> : null}
</span>
);
};
Loading

0 comments on commit 0dd130a

Please sign in to comment.