412 lines
17 KiB
TypeScript
412 lines
17 KiB
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;
|
|
}
|
|
}
|
|
}
|
|
}
|