Skip to content

Commit

Permalink
Added the ability to export transcript messages from trace activities…
Browse files Browse the repository at this point in the history
… when in debug mode (#1452)

* Added the ability to export transcript messages from trace activities when in debug mode
  • Loading branch information
justinwilaby authored Apr 24, 2019
1 parent 014fa59 commit 0d4f107
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 83 deletions.
13 changes: 9 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions packages/app/client/src/commands/uiCommands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ import {
SecretPromptDialogContainer,
} from '../ui/dialogs';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { BotActionType } from '../data/action/botActions';
import { ExplorerActions } from '../data/action/explorerActions';
import { SWITCH_DEBUG_MODE } from '../data/action/debugModeAction';
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';

import { registerCommands } from './uiCommands';

Expand Down Expand Up @@ -156,9 +156,11 @@ describe('the uiCommands', () => {
dispatchedActions.push(action);
return action;
};
registry.getCommand(Commands.SwitchDebugMode).handler(DebugMode.Sidecar);
expect(dispatchedActions.length).toBe(3);
[BotActionType.close, ExplorerActions.Show, SWITCH_DEBUG_MODE].forEach((type, index) =>
const closeActiveBotSpy = jest.spyOn(ActiveBotHelper, 'closeActiveBot').mockResolvedValueOnce(true);
await registry.getCommand(Commands.SwitchDebugMode).handler(DebugMode.Sidecar);
expect(dispatchedActions.length).toBe(2);
expect(closeActiveBotSpy).toHaveBeenCalled();
[ExplorerActions.Show, SWITCH_DEBUG_MODE].forEach((type, index) =>
expect(type).toEqual(dispatchedActions[index].type)
);
});
Expand Down
6 changes: 3 additions & 3 deletions packages/app/client/src/commands/uiCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import { ServiceTypes } from 'botframework-config/lib/schema';

import * as Constants from '../constants';
import { azureArmTokenDataChanged, beginAzureAuthWorkflow, invalidateArmToken } from '../data/action/azureAuthActions';
import { closeBot } from '../data/action/botActions';
import { switchDebugMode } from '../data/action/debugModeAction';
import * as EditorActions from '../data/action/editorActions';
import * as NavBarActions from '../data/action/navBarActions';
Expand All @@ -63,6 +62,7 @@ import {
import * as ExplorerActions from '../data/action/explorerActions';
import { closeConversation } from '../data/action/chatActions';
import { close } from '../data/action/editorActions';
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';

/** Register UI commands (toggling UI) */
export function registerCommands(commandRegistry: CommandRegistry) {
Expand Down Expand Up @@ -135,12 +135,12 @@ export function registerCommands(commandRegistry: CommandRegistry) {

// ---------------------------------------------------------------------------
// Debug mode from main
commandRegistry.registerCommand(UI.SwitchDebugMode, (debugMode: DebugMode) => {
commandRegistry.registerCommand(UI.SwitchDebugMode, async (debugMode: DebugMode) => {
const {
editor: { editors, activeEditor },
} = store.getState();
const { documents } = editors[activeEditor];
store.dispatch(closeBot());
await ActiveBotHelper.closeActiveBot();
store.dispatch(ExplorerActions.showExplorer(debugMode !== DebugMode.Sidecar));
store.dispatch(switchDebugMode(debugMode));
// Close all active conversations - this is a clean wipe of all active conversations
Expand Down
2 changes: 1 addition & 1 deletion packages/app/client/src/ui/editor/emulator/emulator.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
.restart-icon {
&::before { -webkit-mask: url(../../media/ic_refresh.svg); }
}
.save-transcript-icon {
.save-icon {
margin-left: 20px;

&::before { -webkit-mask: url(../../media/ic_save.svg); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const vertical: string;
export const header: string;
export const toolbarIcon: string;
export const restartIcon: string;
export const saveTranscriptIcon: string;
export const saveIcon: string;
export const content: string;
export const presentation: string;
export const chatPanel: string;
Expand Down
31 changes: 15 additions & 16 deletions packages/app/client/src/ui/editor/emulator/emulator.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ import * as React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { mount, shallow } from 'enzyme';
import { DebugMode, SharedConstants } from '@bfemulator/app-shared';
import { DebugMode, newNotification, SharedConstants } from '@bfemulator/app-shared';
import base64Url from 'base64url';

import { disable, enable } from '../../../data/action/presentationActions';
import { clearLog, newConversation, setInspectorObjects } from '../../../data/action/chatActions';
import { updateDocument } from '../../../data/action/editorActions';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { beginAdd } from '../../../data/action/notificationActions';

import { Emulator, RestartConversationOptions } from './emulator';
import { EmulatorContainer } from './emulatorContainer';
Expand Down Expand Up @@ -135,7 +137,7 @@ describe('<EmulatorContainer/>', () => {
mockDispatch = jest.spyOn(mockStore, 'dispatch');
wrapper = mount(
<Provider store={mockStore}>
<EmulatorContainer documentId={'doc1'} url={'someUrl'} mode={'livechat'} />
<EmulatorContainer documentId={'doc1'} url={'someUrl'} mode={'livechat'} conversationId={'convo1'} />
</Provider>
);
node = wrapper.find(Emulator);
Expand Down Expand Up @@ -284,23 +286,20 @@ describe('<EmulatorContainer/>', () => {
});

it('should export a transcript', () => {
mockStoreState.chat.chats.doc1.directLine = {
conversationId: 'convo1',
};
wrapper = shallow(
<Emulator
createErrorNotification={jest.fn(() => null)}
newConversation={jest.fn(() => null)}
mode={'transcript'}
document={mockStoreState.chat.chats.doc1}
/>
);
instance = wrapper.instance();
instance.onExportClick();
instance.onExportTranscriptClick();

expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.SaveTranscriptToFile);
expect(mockRemoteCallsMade[0].args).toEqual(['convo1']);
expect(mockRemoteCallsMade[0].args).toEqual([16, 'convo1']);
});

it('should report a notification when exporting a transcript fails', async () => {
jest.spyOn(CommandServiceImpl, 'remoteCall').mockRejectedValueOnce({ message: 'oh noes!' });
await instance.onExportTranscriptClick();
const notification = newNotification('oh noes!');
notification.timestamp = jasmine.any(Number) as any;

expect(mockDispatch).toHaveBeenCalledWith(beginAdd(notification));
});

it('should start a new conversation', async () => {
Expand Down
57 changes: 38 additions & 19 deletions packages/app/client/src/ui/editor/emulator/emulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ import { SplitButton, Splitter } from '@bfemulator/ui-react';
import base64Url from 'base64url';
import { IEndpointService } from 'botframework-config/lib/schema';
import * as React from 'react';
import { newNotification, Notification, SharedConstants } from '@bfemulator/app-shared';
import { DebugMode } from '@bfemulator/app-shared';
import { DebugMode, newNotification, Notification, SharedConstants, ValueTypesMask } from '@bfemulator/app-shared';

import { Document } from '../../../data/reducer/editor';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
Expand Down Expand Up @@ -72,6 +71,7 @@ export interface EmulatorProps {
enablePresentationMode?: (enabled: boolean) => void;
endpointId?: string;
endpointService?: IEndpointService;
exportItems?: (types: ValueTypesMask, conversationId: string) => Promise<void>;
mode?: EmulatorMode;
newConversation?: (documentId: string, options: any) => void;
presentationModeEnabled?: boolean;
Expand Down Expand Up @@ -262,24 +262,34 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
renderDefaultView(): JSX.Element {
const { NewUserId, SameUserId } = RestartConversationOptions;

const { mode, debugMode } = this.props;
return (
<div className={styles.emulator} key={this.getConversationId()}>
{this.props.mode === 'livechat' && (
{mode === 'livechat' && (
<div className={styles.header}>
<ToolBar>
<SplitButton
defaultLabel="Restart conversation"
buttonClass={styles.restartIcon}
options={[NewUserId, SameUserId]}
disabled={this.props.debugMode === DebugMode.Sidecar}
onClick={this.onStartOverClick}
/>
{debugMode === DebugMode.Normal && (
<SplitButton
defaultLabel="Restart conversation"
buttonClass={styles.restartIcon}
options={[NewUserId, SameUserId]}
onClick={this.onStartOverClick}
/>
)}
<button
className={`${styles.saveTranscriptIcon} ${styles.toolbarIcon || ''}`}
onClick={this.onExportClick}
className={`${styles.saveIcon} ${styles.toolbarIcon || ''}`}
onClick={this.onExportTranscriptClick}
>
Save transcript
</button>
{/*{debugMode === DebugMode.Sidecar && (*/}
{/* <button*/}
{/* className={`${styles.saveIcon} ${styles.toolbarIcon || ''}`}*/}
{/* onClick={this.onExportBotStateClick}*/}
{/* >*/}
{/* Save bot state*/}
{/* </button>*/}
{/*)}*/}
</ToolBar>
</div>
)}
Expand Down Expand Up @@ -370,13 +380,22 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
break;
}
};

private onExportClick = (): void => {
if (this.props.document.directLine) {
CommandServiceImpl.remoteCall(
SharedConstants.Commands.Emulator.SaveTranscriptToFile,
this.props.document.directLine.conversationId
);
// Uncomment when ready to export bot state
// private onExportBotStateClick = async (): Promise<void> => {
// try {
// await this.props.exportItems(ValueTypesMask.BotState, this.props.conversationId);
// } catch (e) {
// const notification = newNotification(e.message);
// this.props.createErrorNotification(notification);
// }
// };

private onExportTranscriptClick = async (): Promise<void> => {
try {
await this.props.exportItems(ValueTypesMask.Activity, this.props.conversationId);
} catch (e) {
const notification = newNotification(e.message);
this.props.createErrorNotification(notification);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
//
import { connect } from 'react-redux';
import { Notification, SharedConstants } from '@bfemulator/app-shared';
import { ValueTypesMask } from '@bfemulator/app-shared/src';

import { RootState } from '../../../data/store';
import * as PresentationActions from '../../../data/action/presentationActions';
Expand Down Expand Up @@ -65,6 +66,8 @@ const mapDispatchToProps = (dispatch): EmulatorProps => ({
createErrorNotification: (notification: Notification) => dispatch(beginAdd(notification)),
trackEvent: (name: string, properties?: { [key: string]: any }) =>
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, name, properties).catch(),
exportItems: (valueTypes: ValueTypesMask, conversationId: string) =>
CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SaveTranscriptToFile, valueTypes, conversationId),
});

export const EmulatorContainer = connect(
Expand Down
28 changes: 13 additions & 15 deletions packages/app/client/src/ui/helpers/activeBotHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const ActiveBotHelper = new (class {
/** Sets a bot as active
* @param bot Bot to set as active
*/
async setActiveBot(bot: BotConfigWithPath): Promise<any> {
async setActiveBot(bot: BotConfigWithPath): Promise<void> {
try {
// set the bot as active on the server side
const botDirectory = await CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.SetActive, bot);
Expand All @@ -104,21 +104,19 @@ export const ActiveBotHelper = new (class {
}

/** tell the server-side the active bot is now closed */
closeActiveBot(): Promise<any> {
return CommandServiceImpl.remoteCall(Bot.Close)
.then(() => {
store.dispatch(BotActions.closeBot());
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, '');
})
.catch(err => {
const errMsg = `Error while closing active bot: ${err}`;
const notification = newNotification(errMsg);
store.dispatch(beginAdd(notification));
throw new Error(errMsg);
});
async closeActiveBot(): Promise<void> {
try {
await CommandServiceImpl.remoteCall(Bot.Close);
store.dispatch(BotActions.closeBot());
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, '');
} catch (err) {
const errMsg = `Error while closing active bot: ${err}`;
const notification = newNotification(errMsg);
store.dispatch(beginAdd(notification));
}
}

async botAlreadyOpen(): Promise<any> {
async botAlreadyOpen(): Promise<void> {
// TODO - localization
return await CommandServiceImpl.remoteCall(Electron.ShowMessageBox, true, {
buttons: ['OK'],
Expand All @@ -131,7 +129,7 @@ export const ActiveBotHelper = new (class {
});
}

async confirmAndCreateBot(botToCreate: BotConfigWithPath, secret: string): Promise<any> {
async confirmAndCreateBot(botToCreate: BotConfigWithPath, secret: string): Promise<void> {
// prompt the user to confirm the switch
const result = await this.confirmSwitchBot();

Expand Down
11 changes: 3 additions & 8 deletions packages/app/main/src/botHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,9 @@ export async function removeBotFromList(botPath: string): Promise<void> {
}

export function getTranscriptsPath(activeBot: BotConfigWithPath, conversation: Conversation): string {
if (conversation.mode === 'livechat-url') {
if (!activeBot || conversation.mode === 'livechat-url') {
return path.join(electron.app.getPath('downloads'), './transcripts');
}

if (activeBot) {
const dirName = path.dirname(activeBot.path);
return path.join(dirName, './transcripts');
}

return '/';
const dirName = path.dirname(activeBot.path);
return path.join(dirName, './transcripts');
}
3 changes: 2 additions & 1 deletion packages/app/main/src/commands/emulatorCommands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { BotConfigWithPathImpl, CommandRegistryImpl } from '@bfemulator/sdk-shar
import { BotConfiguration } from 'botframework-config';
import { newBot, newEndpoint, SharedConstants } from '@bfemulator/app-shared';
import { Conversation } from '@bfemulator/emulator-core';
import { ValueTypesMask } from '@bfemulator/app-shared';

import * as store from '../botData/store';
import { getStore as getSettingsStore } from '../settingsData/store';
Expand Down Expand Up @@ -422,7 +423,7 @@ describe('The emulatorCommands', () => {
const patchBotJsonSpy = jest.spyOn((botHelpers as any).default, 'patchBotsJson').mockResolvedValue(true);

const command = mockCommandRegistry.getCommand(SharedConstants.Commands.Emulator.SaveTranscriptToFile);
await command.handler('1234');
await command.handler(ValueTypesMask.Activity, '1234');

expect(getActiveBotSpy).toHaveBeenCalled();
expect(conversationByIdSpy).toHaveBeenCalledWith('1234');
Expand Down
4 changes: 2 additions & 2 deletions packages/app/main/src/commands/emulatorCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
// Saves the conversation to a transcript file, with user interaction to set filename.
commandRegistry.registerCommand(
Commands.SaveTranscriptToFile,
async (conversationId: string): Promise<void> => {
async (valueTypes: number, conversationId: string): Promise<void> => {
const activeBot: BotConfigWithPath = getActiveBot();
const conversation = Emulator.getInstance().framework.server.botEmulator.facilities.conversations.conversationById(
conversationId
Expand All @@ -85,7 +85,7 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {

if (filename && filename.length) {
mkdirpSync(path.dirname(filename));
const transcripts = await conversation.getTranscript();
const transcripts = await conversation.getTranscript(valueTypes);
writeFile(filename, transcripts);
TelemetryService.trackEvent('transcript_save');
}
Expand Down
Loading

0 comments on commit 0d4f107

Please sign in to comment.