UNPKG

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
// 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; } } } }