Skip to content

Commit

Permalink
Cherry-pick #2145, #2190 and #2196 to master (#2192)
Browse files Browse the repository at this point in the history
* Add Dialog / PageView telemetry (#2145)

* Update app insights package version

* Add PageView logging

* move TelemetryView helper to botTelemetryClient.ts

* remove telemetry Extensions class, harden helper method, add tests

Co-authored-by: Steven Gum <14935595+stevengum@users.noreply.github.com>

* move botbuilder-lg and adaptive-expressions out of preview (#2190)

* move botbuilder-lg and adaptive-expressions out of preview

* correct set-version script in preview packages

* restore preview packages to using Version instead of PreviewPackageVersion

* Fixed issues with AdaptiveSkillDialog (#2196)

and added unit tests

Co-authored-by: Gary Pretty <gary@garypretty.co.uk>
Co-authored-by: Steven Ickman <stevenic@microsoft.com>
  • Loading branch information
3 people committed May 7, 2020
1 parent c3f7e33 commit 77c49e3
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 27 deletions.
2 changes: 1 addition & 1 deletion libraries/botbuilder-applicationinsights/package.json
Expand Up @@ -21,7 +21,7 @@
"main": "./lib/index.js",
"typings": "./lib/index.d.ts",
"dependencies": {
"applicationinsights": "1.2.0",
"applicationinsights": "1.7.5",
"botbuilder-core": "4.1.6",
"cls-hooked": "^4.2.2"
},
Expand Down
Expand Up @@ -6,7 +6,7 @@
* Licensed under the MIT License.
*/
import * as appInsights from 'applicationinsights';
import { Activity, BotTelemetryClient, TelemetryDependency, TelemetryEvent, TelemetryException, TelemetryTrace } from 'botbuilder-core';
import { Activity, BotTelemetryClient, BotPageViewTelemetryClient, TelemetryDependency, TelemetryEvent, TelemetryException, TelemetryTrace, TelemetryPageView } from 'botbuilder-core';
import * as cls from 'cls-hooked';
import * as crypto from 'crypto';
const ns: any = cls.createNamespace('my.request');
Expand Down Expand Up @@ -61,7 +61,7 @@ export const ApplicationInsightsWebserverMiddleware: any = (req: any, res: any,
* myDialog.telemetryClient = appInsightsClient;
* ```
*/
export class ApplicationInsightsTelemetryClient implements BotTelemetryClient {
export class ApplicationInsightsTelemetryClient implements BotTelemetryClient, BotPageViewTelemetryClient {

private client: appInsights.TelemetryClient;
private config: appInsights.Configuration;
Expand Down Expand Up @@ -117,6 +117,10 @@ export class ApplicationInsightsTelemetryClient implements BotTelemetryClient {
this.defaultClient.trackTrace(telemetry as appInsights.Contracts.TraceTelemetry);
}

public trackPageView(telemetry: TelemetryPageView): void {
this.defaultClient.trackPageView(telemetry as appInsights.Contracts.PageViewTelemetry);
}

public flush(): void {
this.defaultClient.flush();
}
Expand Down
42 changes: 40 additions & 2 deletions libraries/botbuilder-core/src/botTelemetryClient.ts
Expand Up @@ -27,6 +27,10 @@ export interface BotTelemetryClient {
flush();
}

export interface BotPageViewTelemetryClient {
trackPageView(telemetry: TelemetryPageView);
}

export interface TelemetryDependency {
dependencyTypeName: string;
target: string;
Expand Down Expand Up @@ -57,11 +61,21 @@ export interface TelemetryTrace {
severityLevel?: Severity;
}

export class NullTelemetryClient implements BotTelemetryClient {
export interface TelemetryPageView {
name: string;
properties?: {[key: string]: string};
metrics?: {[key: string]: number };
}

export class NullTelemetryClient implements BotTelemetryClient, BotPageViewTelemetryClient {

constructor(settings?: any) {
// noop
}

trackPageView(telemetry: TelemetryPageView) {
// noop
}

trackDependency(telemetry: TelemetryDependency) {
// noop
Expand All @@ -74,12 +88,36 @@ export class NullTelemetryClient implements BotTelemetryClient {
trackException(telemetry: TelemetryException) {
// noop
}

trackTrace(telemetry: TelemetryTrace) {
// noop
}

flush() {
// noop
}
}

export function telemetryTrackDialogView(telemetryClient: BotTelemetryClient, dialogName: string, properties?: {[key: string]: any}, metrics?: {[key: string]: number }): void {
if (!clientSupportsTrackDialogView(telemetryClient)) {
throw new TypeError('"telemetryClient" parameter does not have methods trackPageView() or trackTrace()');
}
if (instanceOfBotPageViewTelemetryClient(telemetryClient)) {
telemetryClient.trackPageView({ name: dialogName, properties: properties, metrics: metrics });
}
else {
telemetryClient.trackTrace({ message: 'Dialog View: ' + dialogName, severityLevel: Severity.Information } );
}
}

function instanceOfBotPageViewTelemetryClient(object: any): object is BotPageViewTelemetryClient {
return 'trackPageView' in object;
}

}
function clientSupportsTrackDialogView(client: any): boolean {
if (!client) { return false; }
if (typeof client.trackPageView !== 'function' && typeof client.trackTrace !== 'function') {
return false;
}
return true;
}
46 changes: 46 additions & 0 deletions libraries/botbuilder-core/tests/botTelemetryClient.test.js
@@ -0,0 +1,46 @@
const { ok, strictEqual } = require('assert');
const { Severity, telemetryTrackDialogView } = require('../');

describe('BotTelemetryClient', function() {
this.timeout(3000);

describe('"telemetryTrackDialogView" helper', () => {
it('should call client.trackPageView if it exists', () => {
const testClient = {
trackPageView({ name, properties, metrics }) {
ok(name);
ok(properties);
ok(metrics);
}
};
const testProps = { description: 'value' };
const testMetrics = { duration: 1 };
telemetryTrackDialogView(testClient, 'dialogName', testProps, testMetrics);
});

it('should call client.trackTrace if trackPageView is not supported', () => {
const testClient = {
trackTrace({ message, severityLevel }) {
ok(message);
strictEqual(severityLevel, Severity.Information);
}
};
telemetryTrackDialogView(testClient, 'dialogName');
});

it('should throw TypeError if trackTrace and trackPageView do not exist', () => {
try {
telemetryTrackDialogView(undefined, 'dialogName');
} catch (err) {
strictEqual(err.message, '"telemetryClient" parameter does not have methods trackPageView() or trackTrace()');
}

try {
telemetryTrackDialogView({}, 'dialogName');
} catch (err) {
strictEqual(err.message, '"telemetryClient" parameter does not have methods trackPageView() or trackTrace()');
}

});
});
});
Expand Up @@ -8,9 +8,10 @@
import { SkillDialog, SkillDialogOptions, DialogContext, DialogTurnResult, DialogManager, BeginSkillDialogOptions } from 'botbuilder-dialogs';
import { BoolExpression, StringExpression } from 'adaptive-expressions';
import { TemplateInterface } from '../template';
import { Activity, ActivityTypes } from 'botbuilder-core';
import { Activity, ActivityTypes, BotFrameworkClient, SkillConversationIdFactoryBase } from 'botbuilder-core';

const GLOBAL_SKILL_OPTIONS = Symbol('globalSkillOptions');
const SKILL_CLIENT = Symbol('skillClient');
const CONVERSATION_ID_FACTORY = Symbol('conversationIdFactory');

export class AdaptiveSkillDialog extends SkillDialog {

Expand Down Expand Up @@ -83,13 +84,6 @@ export class AdaptiveSkillDialog extends SkillDialog {
return await dc.endDialog();
}

// Update options with global options
const globalOptions = dc.context.turnState.get(GLOBAL_SKILL_OPTIONS);
if (globalOptions) {
this.dialogOptions = Object.assign(globalOptions, this.dialogOptions);
}
if (!this.dialogOptions.conversationState) { dc.dialogManager.conversationState }

// Setup the skill to call
const botId = this.botId.getValue(dcState);
const skillHostEndpoint = this.skillHostEndpoint.getValue(dcState);
Expand All @@ -98,6 +92,9 @@ export class AdaptiveSkillDialog extends SkillDialog {
if (this.skillAppId) { this.dialogOptions.skill.id = this.dialogOptions.skill.appId = this.skillAppId.getValue(dcState) }
if (this.skillEndpoint) { this.dialogOptions.skill.skillEndpoint = this.skillEndpoint.getValue(dcState) }
if (this.connectionName) { this.dialogOptions.connectionName = this.connectionName.getValue(dcState) }
if (!this.dialogOptions.conversationState) { this.dialogOptions.conversationState = dc.dialogManager.conversationState }
if (!this.dialogOptions.skillClient) { this.dialogOptions.skillClient = dc.context.turnState.get(SKILL_CLIENT) }
if (!this.dialogOptions.conversationIdFactory) { this.dialogOptions.conversationIdFactory = dc.context.turnState.get(CONVERSATION_ID_FACTORY) }

// Get the activity to send to the skill.
options = {} as BeginSkillDialogOptions;
Expand Down Expand Up @@ -126,12 +123,20 @@ export class AdaptiveSkillDialog extends SkillDialog {
return await super.continueDialog(dc);
}

protected onComputeId(): string {
const appId = this.skillAppId ? this.skillAppId.toString() : '';
const activity = this.activity ? this.activity.toString() : '<activity>';
return `Skill[${appId}:${activity}]`;
}

/**
* Configures the initial skill dialog options to use.
* Configures the skill client and conversation ID factory to use.
* @param dm DialogManager to configure.
* @param options Skill dialog options to use.
* @param skillClient Skill client instance to use.
* @param conversationIdFactory Conversation ID factory to use.
*/
static setGlobalSkillOptions(dm: DialogManager, options: SkillDialogOptions): void {
dm.initialTurnState.set(GLOBAL_SKILL_OPTIONS, options);
static setSkillHostOptions(dm: DialogManager, skillClient: BotFrameworkClient, conversationIdFactory: SkillConversationIdFactoryBase): void {
dm.initialTurnState.set(SKILL_CLIENT, skillClient);
dm.initialTurnState.set(CONVERSATION_ID_FACTORY, conversationIdFactory);
}
}
@@ -0,0 +1,152 @@
const { ok, strictEqual } = require('assert');
const { createHash } = require('crypto');
const { stub } = require('sinon');
const {
ActivityTypes,
ConversationState,
MemoryStorage,
TestAdapter,
SkillConversationIdFactoryBase,
StatusCodes,
TurnContext
} = require('botbuilder-core');
const { BoolExpression, StringExpression } = require('adaptive-expressions');
const { DialogManager, DialogTurnStatus } = require('botbuilder-dialogs');
const { AdaptiveSkillDialog } = require('../')


class SimpleConversationIdFactory extends SkillConversationIdFactoryBase {
constructor(config = { disableCreateWithOpts: false, disableGetSkillRef: false }) {
super();
this._conversationRefs = new Map();
this.disableCreateWithOpts = config.disableCreateWithOpts;
this.disableGetSkillRef = config.disableGetSkillRef;
}

async createSkillConversationIdWithOptions(opts) {
if (this.disableCreateWithOpts) {
return super.createSkillConversationIdWithOptions();
}
}

async createSkillConversationId(options) {
const key = createHash('md5').update(options.activity.conversation.id + options.activity.serviceUrl).digest('hex');

const ref = this._conversationRefs.has(key);
if (!ref) {
this._conversationRefs.set(key, {
conversationReference: TurnContext.getConversationReference(options.activity),
oAuthScope: options.fromBotOAuthScope
});
}
return key;
}

async getConversationReference() {

}

async getSkillConversationReference(skillConversationId) {
return this._conversationRefs.get(skillConversationId);
}

deleteConversationReference() {

}
}

describe('SkillDialog', function() {
this.timeout(3000);

let activitySent; // Activity
let fromBotIdSent; // string
let toBotIdSent; // string
let toUriSent; // string (URI)

// Callback to capture the parameters sent to the skill
const captureAction = (fromBotId, toBotId, toUri, serviceUrl, conversationId, activity) => {
// Capture values sent to the skill so we can assert the right parameters were used.
fromBotIdSent = fromBotId;
toBotIdSent = toBotId;
toUriSent = toUri;
activitySent = activity;
}

// Create BotFrameworkHttpClient and postActivityStub
const [skillClient, postActivityStub] = createSkillClientAndStub(captureAction);

// Setup dialog manager
const conversationState = new ConversationState(new MemoryStorage());
const dm = new DialogManager();
dm.conversationState = conversationState;
AdaptiveSkillDialog.setSkillHostOptions(dm, skillClient, new SimpleConversationIdFactory());

// Setup skill dialog
const dialog = new AdaptiveSkillDialog();
setSkillDialogOptions(dialog);
dm.rootDialog = dialog;

it('should call skill via beginDialog()', async () => {
// Send initial activity
const adapter = new TestAdapter(async (context) => {
const { turnResult } = await dm.onTurn(context);

// Assert results and data sent to the SkillClient for first turn
strictEqual(fromBotIdSent, 'SkillCallerId');
strictEqual(toBotIdSent, 'SkillId');
strictEqual(toUriSent, 'http://testskill.contoso.com/api/messages');
strictEqual(activitySent.text, 'test');
strictEqual(turnResult.status, DialogTurnStatus.waiting);
ok(postActivityStub.calledOnce);
});

await adapter.send('test');
});
});

function setSkillDialogOptions(dialog) {
dialog.disabled = new BoolExpression(false);
dialog.botId = new StringExpression('SkillCallerId');
dialog.skillHostEndpoint = new StringExpression('http://test.contoso.com/skill/messages');
dialog.skillAppId = new StringExpression('SkillId');
dialog.skillEndpoint = new StringExpression('http://testskill.contoso.com/api/messages');
}

/**
* @remarks
* captureAction should match the below signature:
* `(fromBotId: string, toBotId: string, toUrl: string, serviceUrl: string, conversationId: string, activity: Activity) => void`
* @param {Function} captureAction
* @param {StatusCodes} returnStatusCode Defaults to StatusCodes.OK
* @returns [BotFrameworkHttpClient, postActivityStub]
*/
function createSkillClientAndStub(captureAction, returnStatusCode = StatusCodes.OK) {
// This require should not fail as this method should only be called after verifying that botbuilder is resolvable.
const { BotFrameworkHttpClient } = require('../../botbuilder');

if (captureAction && typeof captureAction !== 'function') {
throw new TypeError(`Failed test arrangement - createSkillClientAndStub() received ${typeof captureAction} instead of undefined or a function.`);
}

// Create ExpectedReplies object for response body
const dummyActivity = { type: ActivityTypes.Message, attachments: [], entities: [] };
dummyActivity.text = 'dummy activity';
const activityList = { activities: [dummyActivity] };

// Create wrapper for captureAction
function wrapAction(...args) {
captureAction(...args);
return { status: returnStatusCode, body: activityList };
}
// Create client and stub
const skillClient = new BotFrameworkHttpClient({});
const postActivityStub = stub(skillClient, 'postActivity');

if (captureAction) {
postActivityStub.callsFake(wrapAction);
} else {
postActivityStub.returns({ status: returnStatusCode, body: activityList });
}

return [ skillClient, postActivityStub ];
}
4 changes: 3 additions & 1 deletion libraries/botbuilder-dialogs/src/componentDialog.ts
Expand Up @@ -5,7 +5,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TurnContext, BotTelemetryClient, NullTelemetryClient } from 'botbuilder-core';
import { BotTelemetryClient, NullTelemetryClient, telemetryTrackDialogView, TurnContext } from 'botbuilder-core';
import { Dialog, DialogInstance, DialogReason, DialogTurnResult, DialogTurnStatus } from './dialog';
import { DialogContext } from './dialogContext';
import { DialogContainer } from './dialogContainer';
Expand Down Expand Up @@ -81,6 +81,8 @@ export class ComponentDialog<O extends object = {}> extends DialogContainer<O> {
public async beginDialog(outerDC: DialogContext, options?: O): Promise<DialogTurnResult> {
await this.checkForVersionChange(outerDC);

telemetryTrackDialogView(this.telemetryClient, this.id);

// Start the inner dialog.
const innerDC: DialogContext = this.createChildContext(outerDC)
const turnResult: DialogTurnResult<any> = await this.onBeginDialog(innerDC, options);
Expand Down

0 comments on commit 77c49e3

Please sign in to comment.