botbuilder-core
Version:
Core components for Microsoft Bot Builder. Components in this library can run either in a browser or on the server.
413 lines (357 loc) • 17 kB
text/typescript
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License
import { BotTelemetryClient, NullTelemetryClient } from './botTelemetryClient';
import { Middleware } from './middlewareSet';
import { TelemetryConstants } from './telemetryConstants';
import { TurnContext } from './turnContext';
import {
Activity,
ActivityTypes,
ConversationReference,
ResourceResponse,
TeamsChannelData,
} from 'botframework-schema';
// Internal helper duplicated from
// https://github.com/microsoft/botbuilder-js/commit/9277f901701ef270cf5af089e37e7aa7ab2579e1
function isTeamsChannelData(channelData: unknown): channelData is TeamsChannelData {
return typeof channelData === 'object';
}
/**
* Middleware for logging incoming, outgoing, updated or deleted Activity messages.
* Uses the botTelemetryClient interface.
*/
export class TelemetryLoggerMiddleware implements Middleware {
/**
* The name of the event when when new message is received from the user.
*/
static readonly botMsgReceiveEvent: string = 'BotMessageReceived';
/**
* The name of the event when a message is updated by the bot.
*/
static readonly botMsgSendEvent: string = 'BotMessageSend';
/**
* The name of the event when a message is updated by the bot.
*/
static readonly botMsgUpdateEvent: string = 'BotMessageUpdate';
/**
* The name of the event when a message is deleted by the bot.
*/
static readonly botMsgDeleteEvent: string = 'BotMessageDelete';
private readonly _telemetryClient: BotTelemetryClient;
private readonly _logPersonalInformation: boolean;
/**
* Initializes a new instance of the TelemetryLoggerMiddleware class.
*
* @param telemetryClient The BotTelemetryClient used for logging.
* @param logPersonalInformation (Optional) Enable/Disable logging original message name within Application Insights.
*/
constructor(telemetryClient: BotTelemetryClient, logPersonalInformation = false) {
this._telemetryClient = telemetryClient || new NullTelemetryClient();
this._logPersonalInformation = logPersonalInformation;
}
/**
* Gets a value indicating whether to log personal information that came from the user.
*
* @returns A value indicating whether to log personal information or not.
*/
get logPersonalInformation(): boolean {
return this._logPersonalInformation;
}
/**
* Gets the currently configured botTelemetryClient that logs the events.
*
* @returns The currently configured [BotTelemetryClient](xref:botbuilder-core.BotTelemetryClient) that logs the events.
*/
get telemetryClient(): BotTelemetryClient {
return this._telemetryClient;
}
/**
* Logs events based on incoming and outgoing activities using the botTelemetryClient class.
*
* @param context The context object for this turn.
* @param next The delegate to call to continue the bot middleware pipeline
*/
async onTurn(context: TurnContext, next: () => Promise<void>): Promise<void> {
if (context === null) {
throw new Error('context is null');
}
context.turnState.set('telemetryClient', this.telemetryClient);
// log incoming activity at beginning of turn
if (context.activity !== null) {
const activity: Activity = context.activity;
// Log Bot Message Received
await this.onReceiveActivity(activity);
}
// hook up onSend pipeline
context.onSendActivities(
async (
ctx: TurnContext,
activities: Partial<Activity>[],
nextSend: () => Promise<ResourceResponse[]>,
): Promise<ResourceResponse[]> => {
// run full pipeline
const responses: ResourceResponse[] = await nextSend();
activities.forEach(async (act: Partial<Activity>) => {
await this.onSendActivity(<Activity>act);
});
return responses;
},
);
// hook up update activity pipeline
context.onUpdateActivity(
async (ctx: TurnContext, activity: Partial<Activity>, nextUpdate: () => Promise<void>) => {
// run full pipeline
const response: void = await nextUpdate();
await this.onUpdateActivity(<Activity>activity);
return response;
},
);
// hook up delete activity pipeline
context.onDeleteActivity(
async (ctx: TurnContext, reference: Partial<ConversationReference>, nextDelete: () => Promise<void>) => {
// run full pipeline
await nextDelete();
const deletedActivity: Partial<Activity> = TurnContext.applyConversationReference(
{
type: ActivityTypes.MessageDelete,
id: reference.activityId,
},
reference,
false,
);
await this.onDeleteActivity(<Activity>deletedActivity);
},
);
if (next !== null) {
await next();
}
}
/**
* Invoked when a message is received from the user.
* Performs logging of telemetry data using the IBotTelemetryClient.TrackEvent() method.
* The event name logged is "BotMessageReceived".
*
* @param activity Current activity sent from user.
*/
protected async onReceiveActivity(activity: Activity): Promise<void> {
this.telemetryClient.trackEvent({
name: TelemetryLoggerMiddleware.botMsgReceiveEvent,
properties: await this.fillReceiveEventProperties(activity),
});
}
/**
* Invoked when the bot sends a message to the user.
* Performs logging of telemetry data using the botTelemetryClient.trackEvent() method.
* The event name logged is "BotMessageSend".
*
* @param activity Last activity sent from user.
*/
protected async onSendActivity(activity: Activity): Promise<void> {
this.telemetryClient.trackEvent({
name: TelemetryLoggerMiddleware.botMsgSendEvent,
properties: await this.fillSendEventProperties(<Activity>activity),
});
}
/**
* Invoked when the bot updates a message.
* Performs logging of telemetry data using the botTelemetryClient.trackEvent() method.
* The event name used is "BotMessageUpdate".
*
* @param activity Current activity sent from user.
*/
protected async onUpdateActivity(activity: Activity): Promise<void> {
this.telemetryClient.trackEvent({
name: TelemetryLoggerMiddleware.botMsgUpdateEvent,
properties: await this.fillUpdateEventProperties(<Activity>activity),
});
}
/**
* Invoked when the bot deletes a message.
* Performs logging of telemetry data using the botTelemetryClient.trackEvent() method.
* The event name used is "BotMessageDelete".
*
* @param activity Current activity sent from user.
*/
protected async onDeleteActivity(activity: Activity): Promise<void> {
this.telemetryClient.trackEvent({
name: TelemetryLoggerMiddleware.botMsgDeleteEvent,
properties: await this.fillDeleteEventProperties(<Activity>activity),
});
}
/**
* Fills the Application Insights Custom Event properties for BotMessageReceived.
* These properties are logged in the custom event when a new message is received from the user.
*
* @param activity Last activity sent from user.
* @param telemetryProperties Additional properties to add to the event.
* @returns A dictionary that is sent as "Properties" to botTelemetryClient.trackEvent method.
*/
protected async fillReceiveEventProperties(
activity: Activity,
telemetryProperties?: Record<string, string>,
): Promise<Record<string, string>> {
const properties: Record<string, string> = {};
if (activity) {
properties[TelemetryConstants.fromIdProperty] = activity.from?.id ?? '';
properties[TelemetryConstants.conversationIdProperty] = activity.conversation?.id ?? '';
properties[TelemetryConstants.conversationNameProperty] = activity.conversation?.name ?? '';
properties[TelemetryConstants.localeProperty] = activity.locale ?? '';
properties[TelemetryConstants.recipientIdProperty] = activity.recipient?.id ?? '';
properties[TelemetryConstants.recipientNameProperty] = activity.recipient?.name ?? '';
properties[TelemetryConstants.activityTypeProperty] = activity.type ?? '';
properties[TelemetryConstants.activityIdProperty] = activity.id ?? '';
// Use the LogPersonalInformation flag to toggle logging PII data, text and user name are common examples
if (this.logPersonalInformation) {
const fromName = activity.from?.name?.trim();
if (fromName) {
properties[TelemetryConstants.fromNameProperty] = fromName;
}
const activityText = activity.text?.trim();
if (activityText) {
properties[TelemetryConstants.textProperty] = activityText;
}
const activitySpeak = activity.speak?.trim();
if (activitySpeak) {
properties[TelemetryConstants.speakProperty] = activitySpeak;
}
}
// Additional Properties can override "stock" properties.
if (telemetryProperties) {
return Object.assign({}, properties, telemetryProperties);
}
}
this.populateAdditionalChannelProperties(activity, properties);
// Additional Properties can override "stock" properties.
if (telemetryProperties) {
return Object.assign({}, properties, telemetryProperties);
}
return properties;
}
/**
* Fills the Application Insights Custom Event properties for BotMessageSend.
* These properties are logged in the custom event when a response message is sent by the Bot to the user.
*
* @param activity - Last activity sent from user.
* @param telemetryProperties Additional properties to add to the event.
* @returns A dictionary that is sent as "Properties" to botTelemetryClient.trackEvent method.
*/
protected async fillSendEventProperties(
activity: Activity,
telemetryProperties?: Record<string, string>,
): Promise<Record<string, string>> {
const properties: Record<string, string> = {};
if (activity) {
properties[TelemetryConstants.replyActivityIdProperty] = activity.replyToId ?? '';
properties[TelemetryConstants.recipientIdProperty] = activity.recipient?.id ?? '';
properties[TelemetryConstants.conversationIdProperty] = activity.conversation?.id ?? '';
properties[TelemetryConstants.conversationNameProperty] = activity.conversation?.name ?? '';
properties[TelemetryConstants.localeProperty] = activity.locale ?? '';
properties[TelemetryConstants.activityTypeProperty] = activity.type ?? '';
properties[TelemetryConstants.activityIdProperty] = activity.id ?? '';
// Use the LogPersonalInformation flag to toggle logging PII data, text and user name are common examples
if (this.logPersonalInformation) {
const recipientName = activity.recipient?.name?.trim();
if (recipientName) {
properties[TelemetryConstants.recipientNameProperty] = recipientName;
}
const activityText = activity.text?.trim();
if (activityText) {
properties[TelemetryConstants.textProperty] = activityText;
}
const activitySpeak = activity.speak?.trim();
if (activitySpeak) {
properties[TelemetryConstants.speakProperty] = activitySpeak;
}
if (activity.attachments?.length) {
properties[TelemetryConstants.attachmentsProperty] = JSON.stringify(activity.attachments);
}
}
// Additional Properties can override "stock" properties.
if (telemetryProperties) {
return Object.assign({}, properties, telemetryProperties);
}
}
return properties;
}
/**
* Fills the event properties for BotMessageUpdate.
* These properties are logged when an activity message is updated by the Bot.
* For example, if a card is interacted with by the use, and the card needs to be updated to reflect
* some interaction.
*
* @param activity - Last activity sent from user.
* @param telemetryProperties Additional properties to add to the event.
* @returns A dictionary that is sent as "Properties" to botTelemetryClient.trackEvent method.
*/
protected async fillUpdateEventProperties(
activity: Activity,
telemetryProperties?: Record<string, string>,
): Promise<Record<string, string>> {
const properties: Record<string, string> = {};
if (activity) {
properties[TelemetryConstants.recipientIdProperty] = activity.recipient?.id ?? '';
properties[TelemetryConstants.conversationIdProperty] = activity.conversation?.id ?? '';
properties[TelemetryConstants.conversationNameProperty] = activity.conversation?.name ?? '';
properties[TelemetryConstants.localeProperty] = activity.locale ?? '';
properties[TelemetryConstants.activityTypeProperty] = activity.type ?? '';
properties[TelemetryConstants.activityIdProperty] = activity.id ?? '';
// Use the LogPersonalInformation flag to toggle logging PII data, text is a common example
if (this.logPersonalInformation) {
const activityText = activity.text?.trim();
if (activityText) {
properties[TelemetryConstants.textProperty] = activityText;
}
}
// Additional Properties can override "stock" properties.
if (telemetryProperties) {
return Object.assign({}, properties, telemetryProperties);
}
}
return properties;
}
/**
* Fills the Application Insights Custom Event properties for BotMessageDelete.
* These properties are logged in the custom event when an activity message is deleted by the Bot. This is a relatively rare case.
*
* @param activity - Last activity sent from user.
* @param telemetryProperties Additional properties to add to the event.
* @returns A dictionary that is sent as "Properties" to botTelemetryClient.trackEvent method.
*/
protected async fillDeleteEventProperties(
activity: Activity,
telemetryProperties?: Record<string, string>,
): Promise<Record<string, string>> {
const properties: Record<string, string> = {};
if (activity) {
properties[TelemetryConstants.channelIdProperty] = activity.channelId ?? '';
properties[TelemetryConstants.recipientIdProperty] = activity.recipient?.id ?? '';
properties[TelemetryConstants.conversationIdProperty] = activity.conversation?.id ?? '';
properties[TelemetryConstants.conversationNameProperty] = activity.conversation?.name ?? '';
properties[TelemetryConstants.activityTypeProperty] = activity.type ?? '';
properties[TelemetryConstants.activityIdProperty] = activity.id ?? '';
// Additional Properties can override "stock" properties.
if (telemetryProperties) {
return Object.assign({}, properties, telemetryProperties);
}
}
return properties;
}
private populateAdditionalChannelProperties(activity: Activity, properties?: Record<string, string>): void {
if (activity) {
const channelData = activity.channelData;
switch (activity.channelId) {
case 'msteams':
properties.TeamsUserAadObjectId = activity.from?.aadObjectId ?? '';
if (isTeamsChannelData(channelData)) {
properties.TeamsTenantId = channelData.tenant?.id ?? '';
if (channelData.team) {
properties.TeamsTeamInfo = JSON.stringify(channelData.team);
}
}
break;
default:
break;
}
}
}
}