├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md └── packages ├── botbuilder-feedback ├── README.md ├── images │ ├── default-feedback-annotated-resized-66.png │ ├── default-feedback-annotated.png │ ├── default-feedback.png │ ├── feedback-sample-66.png │ └── feedback-sample.png ├── package-lock.json ├── package.json ├── src │ └── index.ts ├── test │ ├── feedback-spec.ts │ ├── tsconfig.json │ └── tslint.json └── tsconfig.json ├── botbuilder-http-test-recorder ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── find-root-module-dir.ts │ ├── http-test-playback.ts │ └── index.ts ├── test │ └── tsconfig.json └── tsconfig.json ├── botbuilder-transcript-app-insights ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── app-insights.ts │ ├── index.ts │ └── serializer.ts ├── test │ ├── app-insights-transcript-store-spec.ts │ ├── mocks.ts │ ├── tsconfig.json │ └── tslint.json └── tsconfig.json ├── botbuilder-transcript-cosmosdb ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── cosmosdb.ts │ ├── index.ts │ ├── initializer.ts │ └── queries.ts ├── test │ ├── cosmosdb-transcript-store-spec.ts │ ├── initializer-spec.ts │ ├── mock-documentdb.ts │ ├── tsconfig.json │ └── tslint.json └── tsconfig.json ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Botbuilder Utils for JavaScript 2 | 3 | This repo contains sample code that can be used to enhance a __v4 Microsoft Bot Framework__ bot running __NodeJS__. It has been tested against version 4.0.6. 4 | 5 | _Packages come with no guarantee of support or updates._ 6 | 7 | ## Packages 8 | 9 | The following packages are provided as sample code only, and so are not published to npm. To use them in your bot, follow the instructions in the respective READMEs: 10 | 11 | 1. [botbuilder-transcript-cosmosdb](./packages/botbuilder-transcript-cosmosdb) 12 | 2. [botbuilder-transcript-app-insights](./packages/botbuilder-transcript-app-insights) 13 | 3. [botbuilder-feedback](./packages/botbuilder-feedback) 14 | 4. [botbuilder-http-test-recorder](./packages/botbuilder-http-test-recorder) 15 | 16 | ## Questions and Contact 17 | 18 | If you have a question or would like to contact the development team, please open an issue in this [repo](https://github.com/Microsoft/botbuilder-utils-js). 19 | 20 | ## Contributing 21 | 22 | > The botbuilder-utils team is currently only accepting bug fixes, not feature requests. If you spot a bug, please open an issue, and if possible, open a PR. 23 | 24 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 25 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 26 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 27 | 28 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 29 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 30 | provided by the bot. You will only need to do this once across all repos using our CLA. 31 | 32 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 33 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 34 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 35 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /packages/botbuilder-feedback/README.md: -------------------------------------------------------------------------------- 1 | # Feedback Collection Middleware for Microsoft Bot Framework 2 | 3 | This directory contains sample code that can be used to build a feedback-request mechanism for your bot. For example, a question-and-answer (QnA) bot may choose to prompt users to rate the quality of answers. 4 | 5 | When combined with the [TranscriptLogger](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/transcriptLogger.ts) middleware and an appropriate transcript store (e.g. [Cosmos DB](../botbuilder-transcript-cosmosdb) or [Application Insights](../botbuilder-transcript-app-insights)) this tool can help drive quality-assurance (QA) analytics reporting. 6 | 7 | ## Prerequisites 8 | 9 | - A NodeJS bot using [Bot Framework v4](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0) 10 | - Your bot should be configured for [ConversationState](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0&tabs=js) 11 | - For optimal use, your bot should be configured to store transcript logs to an analytics database like [Cosmos DB](../botbuilder-transcript-cosmosdb) or [Application Insights](../botbuilder-transcript-app-insights) 12 | 13 | ## Install 14 | 15 | Because this package is supplied as sample code, it is not available on npm and it comes with no guarantee of support or updates. To use this software in your own app: 16 | 17 | 1. clone this repo 18 | 2. `cd botbuilder-utils-js/packages/botbuilder-feedback` 19 | 3. `npm install` 20 | 4. `cd {your-app}` 21 | 5. `npm install file:path-to-botbuilder-utils-js/packages/botbuilder-feedback` 22 | 6. _Recommended_: follow install steps for either [botbuilder-transcript-cosmosdb](../botbuilder-transcript-cosmosdb) or [botbuilder-transcript-app-insights](../botbuilder-transcript-app-insights) so that you can query feedback results. 23 | 24 | > To support CI and other automation tasks, you may also choose to publish this package on a private npm repo, or simply copy the code/dependencies into your own app. 25 | 26 | ## Usage 27 | 28 | > JavaScript example is shown below, but this package also works great in TypeScript projects. 29 | 30 | ```JavaScript 31 | const { 32 | ActivityTypes, AutoSaveStateMiddleware, BotFrameworkAdapter, ConsoleTranscriptLogger, 33 | ConversationState, MemoryStorage, TranscriptLoggerMiddleware } = require('botbuilder'); 34 | const { Feedback } = require('botbuilder-feedback'); 35 | 36 | // configure middleware 37 | const logstore = new ConsoleTranscriptLogger(); // upgrade this to a persistent store like Cosmos DB or Appplication Insights 38 | const stateStorage = new MemoryStorage(); // only use MemoryStorage in dev 39 | const conversationState = new ConversationState(stateStorage); 40 | const autoSaveState = new AutoSaveStateMiddleware(conversationState); 41 | const feedback = new Feedback(conversationState); 42 | const logger = new TranscriptLoggerMiddleware(logstore); 43 | 44 | // create the bot 45 | const adapter = new BotFrameworkAdapter({ 46 | appId: process.env.MICROSOFT_APP_ID, 47 | appPassword: process.env.MICROSOFT_APP_PASSWORD, 48 | }).use(logger, autoSaveState, feedback); 49 | 50 | // call for feedback in your bot logic 51 | const logic = async (context) => { 52 | if (context.activity.type === ActivityTypes.Message) { 53 | if (context.activity.text.toLowerCase().startsWith('what is the meaning of life')) { 54 | await Feedback.sendFeedbackActivity(context, '42'); 55 | } else { 56 | await context.sendActivity(`You said '${context.activity.text}'`); 57 | } 58 | } 59 | }; 60 | 61 | /* adapter.processActivity(...) implementation omitted */ 62 | ``` 63 | 64 | When `Feedback.sendFeedbackActivity(...)` is invoked, a message is automaticaly sent to the user showing feedback choices with the answer: 65 | 66 | ![sample feedback buttons](images/feedback-sample-66.png) 67 | 68 | _The user may click or type their response. If anything other than the available options is typed, the feedback is considered ignored_ 69 | 70 | ## API 71 | 72 | ### Feedback (class) 73 | 74 | ```TypeScript 75 | constructor(conversationState: ConversationState, options?: FeedbackOptions) 76 | ``` 77 | 78 | * `conversationState`: The instance of [`ConversationState`](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0&tabs=js) used by your bot 79 | * `options`: Optional configuration to override default prompts and behavior 80 | * `options.feedbackActions` (`FeedbackAction`): Custom feedback choices for the user. Default values are: `['👍 good answer', '👎 bad answer']` 81 | * `options.feedbackResponse` (`Message`): Message to show when a user provides some feedback. Default value is `'Thanks for your feedback!'` 82 | * `options.dismissAction` (`FeedbackAction`): Text to show on button that allows user to hide/ignore the feedback request. Default value is `'dismiss'` 83 | * `options.promptFreeForm` (`boolean | string[]`): Optionally enable prompting for free-form comments for all or select feedback choices (free-form prompt is shown after user selects a preset choice) 84 | * `options.freeFormPrompt` (`Message`): Message to show when `promptFreeForm` is enabled. Default value is `'Please add any additional comments in the chat'` 85 | 86 | ```TypeScript 87 | static createFeedbackMessage(context: TurnContext, textOrActivity: string|Partial, tag?: string): Partial 88 | ``` 89 | 90 | _Create an Activity object with feedback choices that can be sent to the user_ 91 | 92 | * `context`: Current bot TurnContext 93 | * `textOrActivity`: message sent to the user for which feedback is being requested. If the message is an Activity, and already contains a set of suggested actions, the feedback actions will be appened to the existing actions. 94 | * `tag` optional tag so that feedback responses can be grouped for analytics purposes 95 | * _returns_ An `Activity` object containing the desired `message` and `suggestedAction` parameters 96 | 97 | ```TypeScript 98 | static sendFeedbackActivity(context: TurnContext, textOrActivity: string | Partial, tag?: string): Partial 99 | ``` 100 | 101 | _Send an Activity object with feedback choices to the user_ 102 | 103 | * `context`: Current bot TurnContext 104 | * `textOrActivity`: message sent to the user for which feedback is being requested. If the message is an Activity, and already contains a set of suggested actions, the feedback actions will be appened to the existing actions. 105 | * `tag` optional tag so that feedback responses can be grouped for analytics purposes 106 | * _returns_ The promise from a call to `context.sendActivity(...)`. Don't forget to `await` this! 107 | 108 | This class implements the [Middleware](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/middlewareSet.ts#L14-L16) interface. 109 | 110 | ### Other Types 111 | 112 | ```TypeScript 113 | export type FeedbackAction = string | CardAction; 114 | export type Message = string | { text: string, speak?: string }; 115 | ``` 116 | 117 | ## Customize 118 | 119 | ### Customize feedbackActions 120 | 121 | `feedbackOption` is a choice that a user can click or type. Specify one or more of these in your configuration parameters to customize the message. A FeedbackAction may be either a `string` or [CardAction](https://docs.microsoft.com/en-us/javascript/api/botframework-schema/cardaction) 122 | 123 | _Examples:_ 124 | 125 | ```JavaScript 126 | // using simple strings 127 | new Feedback(conversationState, { 128 | feedbackActions: ['✔ Correct', '✖ Incorrect'], 129 | // OR: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; 130 | // OR: ['Not at all helpful', 'Slightly helpful', 'Somewhat helpful', 'Very helpful', 'Extremely helpful']; 131 | }); 132 | 133 | // or use advanced card actions (e.g. to support PostBack) 134 | new Feedback(conversationState, { 135 | feedbackActions: [ 136 | { title: "😩 Poor", type: ActionTypes.PostBack, value: { response: 0 }}, 137 | { title: "😐 Good", type: ActionTypes.PostBack, value: { response: 1 }}, 138 | { title: "😄 Excellent", type: ActionTypes.PostBack, value: { response: 2 }}, 139 | ], 140 | }); 141 | ``` 142 | 143 | > `PostBack` type supports arbitrary `value` payload. 144 | 145 | ### Customize feedbackResponse 146 | 147 | `feedbackResponse` is the message that appears when a user provides some feedback. The value may be either a `string` or an object giving text along with a speach hint. 148 | 149 | _Examples:_ 150 | 151 | ```JavaScript 152 | new Feedback(conversationState, { 153 | feedbackResponse: 'Thanks a million!', 154 | // OR: { text: 'Thanks a million!', speak: 'Thanks a million!' } 155 | }); 156 | ``` 157 | 158 | ### Customize dismissAction 159 | 160 | `dismissAction` is the value on the button that allows users to hide/ignore the feedback request. The default value is `'dismiss'`. This value may be either a `string` or [CardAction](https://docs.microsoft.com/en-us/javascript/api/botframework-schema/cardaction) 161 | 162 | _Examples:_ 163 | 164 | ```JavaScript 165 | new Feedback(conversationState, { 166 | dismissAction: 'no thanks!', 167 | }); 168 | ``` 169 | 170 | ### Customize promptFreeForm 171 | 172 | Set `promptFreeForm` to `true` to allow users to give open-ended text responses after they make a feedback selection. Alternatively, set `promptFreeForm` to a string array in order to specify which feedback choices can trigger a free-form prompt. 173 | 174 | _Examples_ 175 | ```JavaScript 176 | new Feedback(conversationState, { 177 | promptFreeForm: true, 178 | // OR: promptFreeForm = ['Strongly disagree'], 179 | }); 180 | ``` 181 | 182 | ### Custommize freeFormPrompt 183 | 184 | `freeFormPrompt` is the message shown to ask the user for open-ended text when `promptFreeForm` is enabled. The value may be either a `string` or an object giving text along with a speach hint. 185 | 186 | _Example_ 187 | 188 | ```JavaScript 189 | new Feedback(conversationState, { 190 | promptFreeForm: true, 191 | freeFormPrompt: 'What else would you like to mention?', 192 | }); 193 | ``` 194 | 195 | ## Schema 196 | 197 | When feedback is collected, a `trace` activity is sent by the bot. Configure your bot to use the [TranscriptLogger](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/transcriptLogger.ts) middleware and an appropriate transcript store (e.g. [Cosmos DB](../botbuilder-transcript-cosmosdb) or [Application Insights](../botbuilder-transcript-app-insights) to store and query feedback activities. 198 | 199 | The trace `value` contains information about the feedback 200 | 201 | * `request`: activity sent by the user that triggered the feedback request 202 | * `response`: bot text for which feedback is being requested 203 | * `feedback`: user's feedback selection 204 | * `comments`: (if enabled) user's free-form comments 205 | * `tag`: (if enabled) tag or label describing the feedback that was recorded, for analytics queries 206 | 207 | _Example feedback trace:_ 208 | 209 | ```JSON 210 | { 211 | "type": "trace", 212 | "serviceUrl": "http://localhost:61495", 213 | "channelId": "emulator", 214 | "from": { 215 | "id": "default-bot", 216 | "name": "Bot" 217 | }, 218 | "conversation": { 219 | "id": "4b03khhi12i2" 220 | }, 221 | "recipient": { 222 | "id": "default-user", 223 | "name": "User", 224 | "role": "user" 225 | }, 226 | "replyToId": "chf7mhn9ihb", 227 | "label": "User Feedback", 228 | "valueType": "https://www.example.org/schemas/feedback/trace", 229 | "value": { 230 | "request": { 231 | "type": "message", 232 | "text": "what is the meaning of life?", 233 | "from": { 234 | "id": "default-user", 235 | "name": "User", 236 | "role": "user" 237 | }, 238 | "locale": "en-US", 239 | "textFormat": "plain", 240 | "timestamp": "2018-09-14T16:11:50.622Z", 241 | "channelData": { 242 | "clientActivityId": "1536941498773.2831200917907295.0" 243 | }, 244 | "entities": [ 245 | { 246 | "type": "ClientCapabilities", 247 | "requiresBotState": true, 248 | "supportsTts": true, 249 | "supportsListening": true 250 | } 251 | ], 252 | "id": "1eih4fdj8gceb", 253 | "channelId": "emulator", 254 | "localTimestamp": "2018-09-14T12:11:50-04:00", 255 | "recipient": { 256 | "id": "default-bot", 257 | "name": "Bot" 258 | }, 259 | "conversation": { 260 | "id": "4b03khhi12i2" 261 | }, 262 | "serviceUrl": "http://localhost:61495" 263 | }, 264 | "response": "42", 265 | "feedback": "👍 good answer", 266 | "comments": null 267 | }, 268 | "name": "Feedback", 269 | "id": null 270 | } 271 | ``` 272 | 273 | ## Sample Analytics Queries 274 | 275 | The two reccommended [TranscriptLogger](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/transcriptLogger.ts) stores to perform analytics queries on this, or any other bot activitiy log, are [Cosmos DB](../botbuilder-transcript-cosmosdb) and [Application Insights](../botbuilder-transcript-app-insights). Pick one, and configure it per its documentation. Then and add it to your bot via the `TranscriptLogger` middleware. 276 | 277 | ### Querying Cosmos DB 278 | 279 | > Also see the Cosmos DB [SQL API reference](https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-sql-query-reference). 280 | 281 | 282 | _Cosmos DB Query:_ 283 | ```SQL 284 | SELECT VALUE c.activity['value'] 285 | FROM c 286 | WHERE c.activity['value'].feedback = '👎 bad answer' 287 | ``` 288 | 289 | _Cosmos DB Results:_ 290 | ```JavaScript 291 | [ 292 | { 293 | "request": { 294 | "text": "what is the meaning of life" 295 | /** snip **/ 296 | }, 297 | "response": "42", 298 | "feedback": "👎 bad answer", 299 | "comments": null 300 | } 301 | ] 302 | ``` 303 | 304 | ### Querying AppInsights 305 | 306 | > Also see the App Insights [Language Reference](https://docs.loganalytics.io/docs/Language-Reference) 307 | 308 | Filtering on nested Activity properties requires that you configure them _a priori_ in the `AppInsightsTranscriptStore`: 309 | 310 | ```JavaScript 311 | const store = new AppInsightsTranscriptStore(client, { 312 | filterableActivityProperties: [ 'value.feedback' ], 313 | }); 314 | ``` 315 | 316 | Now the field is available as a filterable _customDimension_ in your Analytics query: 317 | 318 | ``` 319 | customEvents 320 | | where customDimensions.$valueFeedback == '👎 bad answer' 321 | | project customDimensions._value 322 | ``` 323 | 324 | _Response_: 325 | 326 | ```JavaScript 327 | { 328 | request: { text: 'what is the meaning of life' /** snip **/ }, 329 | response: '42', 330 | feedback: '👎 bad answer', 331 | comments: null } 332 | ``` 333 | 334 | > Events may not be immediately retrievable, depending on client-side buffering and other conditions. -------------------------------------------------------------------------------- /packages/botbuilder-feedback/images/default-feedback-annotated-resized-66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/botbuilder-utils-js/62eea4e44903faefe0e2cbf5f3580cf82aa2f161/packages/botbuilder-feedback/images/default-feedback-annotated-resized-66.png -------------------------------------------------------------------------------- /packages/botbuilder-feedback/images/default-feedback-annotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/botbuilder-utils-js/62eea4e44903faefe0e2cbf5f3580cf82aa2f161/packages/botbuilder-feedback/images/default-feedback-annotated.png -------------------------------------------------------------------------------- /packages/botbuilder-feedback/images/default-feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/botbuilder-utils-js/62eea4e44903faefe0e2cbf5f3580cf82aa2f161/packages/botbuilder-feedback/images/default-feedback.png -------------------------------------------------------------------------------- /packages/botbuilder-feedback/images/feedback-sample-66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/botbuilder-utils-js/62eea4e44903faefe0e2cbf5f3580cf82aa2f161/packages/botbuilder-feedback/images/feedback-sample-66.png -------------------------------------------------------------------------------- /packages/botbuilder-feedback/images/feedback-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/botbuilder-utils-js/62eea4e44903faefe0e2cbf5f3580cf82aa2f161/packages/botbuilder-feedback/images/feedback-sample.png -------------------------------------------------------------------------------- /packages/botbuilder-feedback/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-feedback", 3 | "version": "4.2.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/chai": { 8 | "version": "4.1.4", 9 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.4.tgz", 10 | "integrity": "sha512-h6+VEw2Vr3ORiFCyyJmcho2zALnUq9cvdB/IO8Xs9itrJVCenC7o26A6+m7D0ihTTr65eS259H5/Ghl/VjYs6g==", 11 | "dev": true 12 | }, 13 | "@types/mocha": { 14 | "version": "5.2.5", 15 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", 16 | "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==", 17 | "dev": true 18 | }, 19 | "@types/node": { 20 | "version": "9.6.32", 21 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/@types/node/-/node-9.6.32.tgz", 22 | "integrity": "sha512-5+L3wQ+FHoQ589EaH6rYICleuj8gnunq+1CJkM9fxklirErIOv+kxm3s/vecYnpJOYnFowE5uUizcb3hgjHUug==", 23 | "dev": true 24 | }, 25 | "ansi-regex": { 26 | "version": "2.1.1", 27 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 28 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 29 | "dev": true 30 | }, 31 | "ansi-styles": { 32 | "version": "2.2.1", 33 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 34 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", 35 | "dev": true 36 | }, 37 | "argparse": { 38 | "version": "1.0.10", 39 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 40 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 41 | "dev": true, 42 | "requires": { 43 | "sprintf-js": "1.0.3" 44 | } 45 | }, 46 | "arrify": { 47 | "version": "1.0.1", 48 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 49 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 50 | "dev": true 51 | }, 52 | "assert": { 53 | "version": "1.4.1", 54 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/assert/-/assert-1.4.1.tgz", 55 | "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", 56 | "dev": true, 57 | "requires": { 58 | "util": "0.10.3" 59 | } 60 | }, 61 | "assertion-error": { 62 | "version": "1.1.0", 63 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 64 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 65 | "dev": true 66 | }, 67 | "babel-code-frame": { 68 | "version": "6.26.0", 69 | "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", 70 | "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", 71 | "dev": true, 72 | "requires": { 73 | "chalk": "1.1.3", 74 | "esutils": "2.0.2", 75 | "js-tokens": "3.0.2" 76 | }, 77 | "dependencies": { 78 | "chalk": { 79 | "version": "1.1.3", 80 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 81 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 82 | "dev": true, 83 | "requires": { 84 | "ansi-styles": "2.2.1", 85 | "escape-string-regexp": "1.0.5", 86 | "has-ansi": "2.0.0", 87 | "strip-ansi": "3.0.1", 88 | "supports-color": "2.0.0" 89 | } 90 | }, 91 | "supports-color": { 92 | "version": "2.0.0", 93 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 94 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", 95 | "dev": true 96 | } 97 | } 98 | }, 99 | "balanced-match": { 100 | "version": "1.0.0", 101 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 102 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 103 | "dev": true 104 | }, 105 | "botbuilder-core": { 106 | "version": "4.0.6", 107 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/botbuilder-core/-/botbuilder-core-4.0.6.tgz", 108 | "integrity": "sha512-23OE1MOQStQDIL2379O9gMgq/t4LcgXzr9+odFTK8WYpl9FSTelwntb8PIeW4fToqV/palQbmCDLNrFtQ/X7ug==", 109 | "dev": true, 110 | "requires": { 111 | "assert": "1.4.1", 112 | "botframework-schema": "4.0.6" 113 | } 114 | }, 115 | "botframework-schema": { 116 | "version": "4.0.6", 117 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/botframework-schema/-/botframework-schema-4.0.6.tgz", 118 | "integrity": "sha512-FoOSWio0ZkpE3nCGsv5Yz5GbmT29uuFBwTsK9t0+MTv7pAmM+MDrqG3A1Ed99N0oQXr59yacEvx5Zdaeb6Xw5Q==", 119 | "dev": true, 120 | "requires": { 121 | "@types/node": "9.6.32" 122 | } 123 | }, 124 | "brace-expansion": { 125 | "version": "1.1.11", 126 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 127 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 128 | "dev": true, 129 | "requires": { 130 | "balanced-match": "1.0.0", 131 | "concat-map": "0.0.1" 132 | } 133 | }, 134 | "browser-stdout": { 135 | "version": "1.3.1", 136 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 137 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 138 | "dev": true 139 | }, 140 | "buffer-from": { 141 | "version": "1.1.1", 142 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/buffer-from/-/buffer-from-1.1.1.tgz", 143 | "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=", 144 | "dev": true 145 | }, 146 | "builtin-modules": { 147 | "version": "1.1.1", 148 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 149 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 150 | "dev": true 151 | }, 152 | "chai": { 153 | "version": "4.1.2", 154 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 155 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 156 | "dev": true, 157 | "requires": { 158 | "assertion-error": "1.1.0", 159 | "check-error": "1.0.2", 160 | "deep-eql": "3.0.1", 161 | "get-func-name": "2.0.0", 162 | "pathval": "1.1.0", 163 | "type-detect": "4.0.8" 164 | } 165 | }, 166 | "chalk": { 167 | "version": "2.4.1", 168 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 169 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 170 | "dev": true, 171 | "requires": { 172 | "ansi-styles": "3.2.1", 173 | "escape-string-regexp": "1.0.5", 174 | "supports-color": "5.4.0" 175 | }, 176 | "dependencies": { 177 | "ansi-styles": { 178 | "version": "3.2.1", 179 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 180 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 181 | "dev": true, 182 | "requires": { 183 | "color-convert": "1.9.2" 184 | } 185 | } 186 | } 187 | }, 188 | "check-error": { 189 | "version": "1.0.2", 190 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 191 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 192 | "dev": true 193 | }, 194 | "color-convert": { 195 | "version": "1.9.2", 196 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", 197 | "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", 198 | "dev": true, 199 | "requires": { 200 | "color-name": "1.1.1" 201 | } 202 | }, 203 | "color-name": { 204 | "version": "1.1.1", 205 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", 206 | "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", 207 | "dev": true 208 | }, 209 | "commander": { 210 | "version": "2.15.1", 211 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 212 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 213 | "dev": true 214 | }, 215 | "concat-map": { 216 | "version": "0.0.1", 217 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 218 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 219 | "dev": true 220 | }, 221 | "debug": { 222 | "version": "3.1.0", 223 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 224 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 225 | "dev": true, 226 | "requires": { 227 | "ms": "2.0.0" 228 | } 229 | }, 230 | "deep-eql": { 231 | "version": "3.0.1", 232 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 233 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 234 | "dev": true, 235 | "requires": { 236 | "type-detect": "4.0.8" 237 | } 238 | }, 239 | "diff": { 240 | "version": "3.5.0", 241 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 242 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 243 | "dev": true 244 | }, 245 | "escape-string-regexp": { 246 | "version": "1.0.5", 247 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 248 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 249 | "dev": true 250 | }, 251 | "esprima": { 252 | "version": "4.0.1", 253 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 254 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 255 | "dev": true 256 | }, 257 | "esutils": { 258 | "version": "2.0.2", 259 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 260 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 261 | "dev": true 262 | }, 263 | "fs.realpath": { 264 | "version": "1.0.0", 265 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 266 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 267 | "dev": true 268 | }, 269 | "get-func-name": { 270 | "version": "2.0.0", 271 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 272 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 273 | "dev": true 274 | }, 275 | "glob": { 276 | "version": "7.1.2", 277 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 278 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 279 | "dev": true, 280 | "requires": { 281 | "fs.realpath": "1.0.0", 282 | "inflight": "1.0.6", 283 | "inherits": "2.0.3", 284 | "minimatch": "3.0.4", 285 | "once": "1.4.0", 286 | "path-is-absolute": "1.0.1" 287 | } 288 | }, 289 | "growl": { 290 | "version": "1.10.5", 291 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 292 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 293 | "dev": true 294 | }, 295 | "has-ansi": { 296 | "version": "2.0.0", 297 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 298 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 299 | "dev": true, 300 | "requires": { 301 | "ansi-regex": "2.1.1" 302 | } 303 | }, 304 | "has-flag": { 305 | "version": "3.0.0", 306 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 307 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 308 | "dev": true 309 | }, 310 | "he": { 311 | "version": "1.1.1", 312 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 313 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 314 | "dev": true 315 | }, 316 | "inflight": { 317 | "version": "1.0.6", 318 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 319 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 320 | "dev": true, 321 | "requires": { 322 | "once": "1.4.0", 323 | "wrappy": "1.0.2" 324 | } 325 | }, 326 | "inherits": { 327 | "version": "2.0.3", 328 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 329 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 330 | "dev": true 331 | }, 332 | "js-tokens": { 333 | "version": "3.0.2", 334 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 335 | "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", 336 | "dev": true 337 | }, 338 | "js-yaml": { 339 | "version": "3.12.0", 340 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", 341 | "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", 342 | "dev": true, 343 | "requires": { 344 | "argparse": "1.0.10", 345 | "esprima": "4.0.1" 346 | } 347 | }, 348 | "make-error": { 349 | "version": "1.3.5", 350 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/make-error/-/make-error-1.3.5.tgz", 351 | "integrity": "sha1-7+ToH22yjK3WBccPKcgxtY73dsg=", 352 | "dev": true 353 | }, 354 | "minimatch": { 355 | "version": "3.0.4", 356 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 357 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 358 | "dev": true, 359 | "requires": { 360 | "brace-expansion": "1.1.11" 361 | } 362 | }, 363 | "minimist": { 364 | "version": "0.0.8", 365 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 366 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 367 | "dev": true 368 | }, 369 | "mkdirp": { 370 | "version": "0.5.1", 371 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 372 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 373 | "dev": true, 374 | "requires": { 375 | "minimist": "0.0.8" 376 | } 377 | }, 378 | "mocha": { 379 | "version": "5.2.0", 380 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 381 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 382 | "dev": true, 383 | "requires": { 384 | "browser-stdout": "1.3.1", 385 | "commander": "2.15.1", 386 | "debug": "3.1.0", 387 | "diff": "3.5.0", 388 | "escape-string-regexp": "1.0.5", 389 | "glob": "7.1.2", 390 | "growl": "1.10.5", 391 | "he": "1.1.1", 392 | "minimatch": "3.0.4", 393 | "mkdirp": "0.5.1", 394 | "supports-color": "5.4.0" 395 | } 396 | }, 397 | "ms": { 398 | "version": "2.0.0", 399 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 400 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 401 | "dev": true 402 | }, 403 | "once": { 404 | "version": "1.4.0", 405 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 406 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 407 | "dev": true, 408 | "requires": { 409 | "wrappy": "1.0.2" 410 | } 411 | }, 412 | "path-is-absolute": { 413 | "version": "1.0.1", 414 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 415 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 416 | "dev": true 417 | }, 418 | "path-parse": { 419 | "version": "1.0.5", 420 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 421 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 422 | "dev": true 423 | }, 424 | "pathval": { 425 | "version": "1.1.0", 426 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 427 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 428 | "dev": true 429 | }, 430 | "resolve": { 431 | "version": "1.8.1", 432 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", 433 | "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", 434 | "dev": true, 435 | "requires": { 436 | "path-parse": "1.0.5" 437 | } 438 | }, 439 | "semver": { 440 | "version": "5.5.0", 441 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", 442 | "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", 443 | "dev": true 444 | }, 445 | "source-map": { 446 | "version": "0.6.1", 447 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/source-map/-/source-map-0.6.1.tgz", 448 | "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", 449 | "dev": true 450 | }, 451 | "source-map-support": { 452 | "version": "0.5.9", 453 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/source-map-support/-/source-map-support-0.5.9.tgz", 454 | "integrity": "sha1-QbyVOyU0Jn6i1gW8z6e/oxEc7V8=", 455 | "dev": true, 456 | "requires": { 457 | "buffer-from": "1.1.1", 458 | "source-map": "0.6.1" 459 | } 460 | }, 461 | "sprintf-js": { 462 | "version": "1.0.3", 463 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 464 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 465 | "dev": true 466 | }, 467 | "strip-ansi": { 468 | "version": "3.0.1", 469 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 470 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 471 | "dev": true, 472 | "requires": { 473 | "ansi-regex": "2.1.1" 474 | } 475 | }, 476 | "supports-color": { 477 | "version": "5.4.0", 478 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 479 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 480 | "dev": true, 481 | "requires": { 482 | "has-flag": "3.0.0" 483 | } 484 | }, 485 | "ts-node": { 486 | "version": "7.0.1", 487 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/ts-node/-/ts-node-7.0.1.tgz", 488 | "integrity": "sha1-lWLcLR5tJI0kvFX3c+P2FDN9m68=", 489 | "dev": true, 490 | "requires": { 491 | "arrify": "1.0.1", 492 | "buffer-from": "1.1.1", 493 | "diff": "3.5.0", 494 | "make-error": "1.3.5", 495 | "minimist": "1.2.0", 496 | "mkdirp": "0.5.1", 497 | "source-map-support": "0.5.9", 498 | "yn": "2.0.0" 499 | }, 500 | "dependencies": { 501 | "minimist": { 502 | "version": "1.2.0", 503 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 504 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 505 | "dev": true 506 | } 507 | } 508 | }, 509 | "tslib": { 510 | "version": "1.9.3", 511 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 512 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", 513 | "dev": true 514 | }, 515 | "tslint": { 516 | "version": "5.11.0", 517 | "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", 518 | "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", 519 | "dev": true, 520 | "requires": { 521 | "babel-code-frame": "6.26.0", 522 | "builtin-modules": "1.1.1", 523 | "chalk": "2.4.1", 524 | "commander": "2.15.1", 525 | "diff": "3.5.0", 526 | "glob": "7.1.2", 527 | "js-yaml": "3.12.0", 528 | "minimatch": "3.0.4", 529 | "resolve": "1.8.1", 530 | "semver": "5.5.0", 531 | "tslib": "1.9.3", 532 | "tsutils": "2.28.0" 533 | } 534 | }, 535 | "tsutils": { 536 | "version": "2.28.0", 537 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", 538 | "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", 539 | "dev": true, 540 | "requires": { 541 | "tslib": "1.9.3" 542 | } 543 | }, 544 | "type-detect": { 545 | "version": "4.0.8", 546 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 547 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 548 | "dev": true 549 | }, 550 | "typescript": { 551 | "version": "2.9.2", 552 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", 553 | "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", 554 | "dev": true 555 | }, 556 | "util": { 557 | "version": "0.10.3", 558 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/util/-/util-0.10.3.tgz", 559 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", 560 | "dev": true, 561 | "requires": { 562 | "inherits": "2.0.1" 563 | }, 564 | "dependencies": { 565 | "inherits": { 566 | "version": "2.0.1", 567 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/inherits/-/inherits-2.0.1.tgz", 568 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 569 | "dev": true 570 | } 571 | } 572 | }, 573 | "wrappy": { 574 | "version": "1.0.2", 575 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 576 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 577 | "dev": true 578 | }, 579 | "yn": { 580 | "version": "2.0.0", 581 | "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", 582 | "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", 583 | "dev": true 584 | } 585 | } 586 | } 587 | -------------------------------------------------------------------------------- /packages/botbuilder-feedback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-feedback", 3 | "version": "4.2.2", 4 | "description": "Middleware for Microsoft Bot Framework to facilitate collection and logging of user feedback analytics", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "mocha --require ts-node/register \"test/**/*.{ts,tsx}\"", 10 | "pretest": "npm run build", 11 | "prepare": "npm run build", 12 | "prepublishOnly": "npm --no-git-tag-version version patch" 13 | }, 14 | "private": true, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Microsoft/botbuilder-utils-js.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/Microsoft/botbuilder-utils-js/issues" 21 | }, 22 | "keywords": [ 23 | "botbuilder", 24 | "botframework", 25 | "bots", 26 | "chatbots", 27 | "logging", 28 | "transcript", 29 | "analytics" 30 | ], 31 | "author": "Microsoft Corp.", 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@types/chai": "^4.1.4", 35 | "@types/mocha": "^5.2.5", 36 | "botbuilder-core": "^4.0.6", 37 | "chai": "^4.1.2", 38 | "mocha": "^5.2.0", 39 | "ts-node": "^7.0.1", 40 | "tslint": "^5.11.0", 41 | "typescript": "^2.9.2" 42 | }, 43 | "peerDependencies": { 44 | "botbuilder-core": "^4.0.6" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/botbuilder-feedback/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { Activity, ActivityTypes, CardAction, ConversationState, MessageFactory, Middleware, StoreItem, TurnContext } from 'botbuilder-core'; 5 | 6 | const DEFAULT_FEEDBACK_ACTIONS = ['👍 good answer', '👎 bad answer']; 7 | const DEFAULT_FEEDBACK_RESPONSE = 'Thanks for your feedback!'; 8 | const DEFAULT_DISMISS_ACTION = 'dismiss'; 9 | const DEFAULT_FREE_FORM_PROMPT = 'Please add any additional comments in the chat'; 10 | const DEFAULT_PROMPT_FREE_FORM = false; 11 | const TRACE_TYPE = 'https://www.example.org/schemas/feedback/trace'; 12 | const TRACE_NAME = 'Feedback'; 13 | const TRACE_LABEL = 'User Feedback'; 14 | 15 | export type Message = string | { text: string, speak?: string }; 16 | 17 | const feedbackAction = (action: FeedbackAction) => typeof action === 'string' ? action : feedbackValue(action); 18 | const feedbackValue = (activity: { text?: string, value?: any }) => activity.value || activity.text; 19 | 20 | /** StoreItem to track feedback state */ 21 | export interface FeedbackState extends StoreItem { 22 | feedback: FeedbackRecord; 23 | } 24 | 25 | export interface FeedbackContext { 26 | feedback: FeedbackOptions & { conversationState: ConversationState }; 27 | } 28 | 29 | /** Options for Feedback Middleware */ 30 | export interface FeedbackOptions { 31 | 32 | /** Custom feedback choices for the user. Default values are: `['👍 good answer', '👎 bad answer']` */ 33 | feedbackActions?: FeedbackAction[]; 34 | 35 | /** Message to show when a user provides some feedback. Default value is `'Thanks for your feedback!'` */ 36 | feedbackResponse?: Message; 37 | 38 | /** Text to show on button that allows user to hide/ignore the feedback request. Default value is `'dismiss'` */ 39 | dismissAction?: FeedbackAction; 40 | 41 | /** Optionally enable prompting for free-form comments for all or select feedback choices (free-form prompt is shown after user selects a preset choice) */ 42 | promptFreeForm?: boolean | string[]; 43 | 44 | /** Message to show when `promptFreeForm` is enabled. Default value is `'Please add any additional comments in the chat'` */ 45 | freeFormPrompt?: Message; 46 | } 47 | 48 | /** Record of feedback received */ 49 | export interface FeedbackRecord { 50 | 51 | /** arbitrary feedback tagging, for analytics purposes */ 52 | tag: string; 53 | 54 | /** activity sent by the user that triggered the feedback request */ 55 | request: Partial; 56 | 57 | /** bot text or value for which feedback is being requested */ 58 | response: string | any; 59 | 60 | /** user's feedback selection */ 61 | feedback: string | any; 62 | 63 | /** user's free-form comments, if enabled */ 64 | comments: string; 65 | } 66 | 67 | export type FeedbackAction = string | CardAction; 68 | export type ContextWithFeedback = TurnContext & FeedbackContext; 69 | 70 | /** Middleware that managegs user feedback prompts, and stores responses in the transcript log */ 71 | export class Feedback implements Middleware { 72 | 73 | /** 74 | * Returns a message that includes feedback prompts in the form of Suggested Actions 75 | * @param context current bot context 76 | * @param textOrActivity message sent to the user for which feedback is being requested. 77 | * If the message is an Activity, and already contains a set of suggested actions, the feedback actions will be appened to the existing actions. 78 | * @param tag optional tag so that feedback responses can be grouped for analytics purposes 79 | */ 80 | static createFeedbackMessage(context: TurnContext, textOrActivity: string|Partial, tag?: string): Partial { 81 | const feedbackContext = context as ContextWithFeedback; 82 | const state = feedbackContext.feedback.conversationState.get(context) as StoreItem & FeedbackState; // TODO SDK Feedback: get() should be generic 83 | const actions = feedbackContext.feedback.feedbackActions.concat(feedbackContext.feedback.dismissAction) 84 | .filter((x) => !!x); 85 | 86 | state.feedback = { // this should be put on the context object. onTurn should move it from context to state after await next() 87 | tag, 88 | request: context.activity, 89 | response: typeof textOrActivity === 'string' ? textOrActivity : feedbackValue(textOrActivity), 90 | feedback: null, 91 | comments: null, 92 | }; 93 | 94 | if (typeof textOrActivity === 'string') { 95 | const text = textOrActivity; 96 | return MessageFactory.suggestedActions(actions, text); 97 | } else { 98 | const activity = textOrActivity; 99 | const suggestedActions = MessageFactory.suggestedActions(actions).suggestedActions; 100 | if (activity.suggestedActions) { 101 | activity.suggestedActions.actions = activity.suggestedActions.actions.concat(suggestedActions.actions); 102 | } else { 103 | activity.suggestedActions = suggestedActions; 104 | } 105 | return activity; 106 | } 107 | } 108 | 109 | /** 110 | * Sends a message that includes feedback prompts in the form of Suggested Actions 111 | * @param context current bot context 112 | * @param textOrActivity message sent to the user for which feedback is being requested. 113 | * If the message is an Activity, and already contains a set of suggested actions, the feedback actions will be appened to the existing actions. 114 | * @param tag optional tag so that feedback responses can be grouped for analytics purposes 115 | */ 116 | static sendFeedbackActivity(context: TurnContext, textOrActivity: string | Partial, tag?: string) { 117 | const message = Feedback.createFeedbackMessage(context, textOrActivity, tag); 118 | return context.sendActivity(message); 119 | } 120 | 121 | /** 122 | * Create a new Feedback middleware instance 123 | * @param conversationState The instance of `ConversationState` used by your bot 124 | * @param options Optional configuration parameters for the feedback middleware 125 | */ 126 | constructor(private conversationState: ConversationState, private options?: FeedbackOptions) { 127 | this.options = options || {}; 128 | if (!this.options.feedbackActions) { 129 | this.options.feedbackActions = DEFAULT_FEEDBACK_ACTIONS; 130 | } 131 | if (!this.options.feedbackResponse) { 132 | this.options.feedbackResponse = DEFAULT_FEEDBACK_RESPONSE; 133 | } 134 | if (!this.options.dismissAction) { 135 | this.options.dismissAction = DEFAULT_DISMISS_ACTION; 136 | } 137 | if (!this.options.freeFormPrompt) { 138 | this.options.freeFormPrompt = DEFAULT_FREE_FORM_PROMPT; 139 | } 140 | if (!this.options.promptFreeForm) { 141 | this.options.promptFreeForm = DEFAULT_PROMPT_FREE_FORM; 142 | } 143 | } 144 | 145 | async onTurn(context: ContextWithFeedback, next: () => Promise): Promise { 146 | 147 | // store feedback options on the context object so that they can be used downstream by user-invoked `requestFeedback()` 148 | context.feedback = Object.assign({}, this.options, { conversationState: this.conversationState }); 149 | 150 | const record = await this.getFeedbackState(context); 151 | 152 | // feedback is pending 153 | if (record) { 154 | const canGiveComments = this.userCanGiveComments(context); 155 | 156 | // user is giving free-form comments 157 | if (record.feedback && this.options.promptFreeForm) { 158 | record.comments = context.activity.text; 159 | await this.storeFeedback(context); 160 | return; 161 | 162 | // user is giving feedback selection 163 | } else if (this.userGaveFeedback(context)) { 164 | record.feedback = feedbackValue(context.activity); 165 | 166 | if (canGiveComments) { 167 | await this.sendMessage(context, this.options.freeFormPrompt); 168 | } else { 169 | await this.storeFeedback(context); 170 | } 171 | 172 | return; 173 | 174 | // user did not provide feedback: clear record and continue 175 | } else { 176 | this.clearFeedbackState(context); 177 | if (!this.userDismissed(context)) { 178 | return next(); 179 | } 180 | } 181 | 182 | // no pending feedback, or user did not provide feedback 183 | } else { 184 | return next(); 185 | } 186 | } 187 | 188 | private async getFeedbackState(context: ContextWithFeedback): Promise { 189 | const state = await this.conversationState.load(context); 190 | if (!state) { 191 | throw new Error('Feedback middleware cannot find a BotState instance. Make sure that your ConversationState middleware is added to your bot before Feedback'); 192 | } 193 | 194 | return state.feedback; 195 | } 196 | 197 | private clearFeedbackState(context: ContextWithFeedback): void { 198 | const state = this.conversationState.get(context); 199 | delete state.feedback; 200 | } 201 | 202 | private sendMessage(context: TurnContext, message: Message) { 203 | if (typeof message === 'string') { 204 | return context.sendActivity(message); 205 | } else { 206 | return context.sendActivity(message.text, message.speak); 207 | } 208 | } 209 | 210 | private async storeFeedback(context: ContextWithFeedback) { 211 | const record = await this.getFeedbackState(context); 212 | this.clearFeedbackState(context); 213 | await context.sendActivity({ 214 | type: ActivityTypes.Trace, 215 | valueType: TRACE_TYPE, 216 | name: TRACE_NAME, 217 | label: TRACE_LABEL, 218 | value: record, 219 | }); 220 | 221 | // send optional acknowledgement back to user 222 | if (this.options.feedbackResponse) { 223 | await this.sendMessage(context, this.options.feedbackResponse); 224 | } 225 | } 226 | 227 | private userCanGiveComments(context: ContextWithFeedback) { 228 | return this.options.promptFreeForm === true || (Array.isArray(this.options.promptFreeForm) && this.options.promptFreeForm 229 | .some((x) => x === feedbackValue(context.activity))); 230 | } 231 | 232 | private userGaveFeedback(context: ContextWithFeedback) { 233 | return !this.userDismissed(context) && this.options.feedbackActions 234 | .some((x) => feedbackAction(x) === feedbackValue(context.activity)); 235 | } 236 | 237 | private userDismissed(context: ContextWithFeedback) { 238 | return feedbackValue(context.activity) === feedbackAction(this.options.dismissAction); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /packages/botbuilder-feedback/test/feedback-spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { ActivityTypes, ConversationState, MemoryStorage, TestAdapter } from 'botbuilder-core'; 5 | import { expect } from 'chai'; 6 | 7 | import { Feedback, FeedbackOptions, FeedbackRecord } from '../src'; 8 | 9 | const DEFAULT_OPTIONS: Partial = { 10 | feedbackActions: ['👍 good answer', '👎 bad answer'], 11 | feedbackResponse: 'Thanks for your feedback!', 12 | dismissAction: 'dismiss', 13 | promptFreeForm: false, 14 | freeFormPrompt: 'Please add any additional comments in the chat', 15 | }; 16 | 17 | describe('Feedback Middleware', () => { 18 | let convState: ConversationState; 19 | let options: FeedbackOptions; 20 | 21 | beforeEach(() => { 22 | convState = new ConversationState(new MemoryStorage()); 23 | options = Object.assign({}, DEFAULT_OPTIONS, { conversationState: null }); 24 | }); 25 | 26 | it('should set default options', () => { 27 | new Feedback(options); 28 | [ 29 | options.feedbackActions, 30 | options.feedbackResponse, 31 | options.dismissAction, 32 | options.promptFreeForm, 33 | options.freeFormPrompt, 34 | ].forEach((x) => expect(x).to.not.be.undefined); 35 | }); 36 | 37 | it('should pass through non-feedback', async () => { 38 | const adapter = new TestAdapter(async (context) => { 39 | await context.sendActivity('bot response'); 40 | }) 41 | .use(convState, new Feedback(options)); 42 | 43 | await adapter 44 | .send('hello world') 45 | .assertReply((resp) => { 46 | expect(resp.text).to.equal('bot response'); 47 | }); 48 | }); 49 | 50 | it('should write feedback choices onto response activity', async () => { 51 | const adapter = new TestAdapter(async (context) => { 52 | const resp = Feedback.requestFeedback(context, 'the answer is 123'); 53 | const state = convState.get(context); 54 | expect(resp.suggestedActions.actions.length).to.equal(2); 55 | expect(state.feedback).to.not.be.undefined; 56 | await context.sendActivity(resp); 57 | }) 58 | .use(convState, new Feedback(options)); 59 | 60 | await adapter 61 | .send('what is 100 + 20 + 3?') 62 | .assertReply((resp) => { 63 | expect(resp.text).to.equal('the answer is 123'); 64 | expect(resp.suggestedActions.actions.length).to.equal(options.feedbackActions.length); 65 | }); 66 | }); 67 | 68 | it('should send feedback trace activity', async () => { 69 | const adapter = new TestAdapter(async () => { 70 | expect.fail(); 71 | }) 72 | .use(convState) 73 | .use((context, next) => { 74 | const feedback: FeedbackRecord = { 75 | type: 'test', 76 | request: { type: 'message', text: 'foo' }, 77 | response: 'bar', 78 | feedback: null, 79 | comments: null, 80 | }; 81 | convState.get(context).feedback = feedback; 82 | return next(); 83 | }) 84 | .use(new Feedback(options)); 85 | 86 | await adapter 87 | .send(options.feedbackActions[0]) 88 | .assertReply((resp) => { 89 | expect(resp.text).to.equal(options.feedbackResponse); 90 | }) 91 | .assertReply((resp) => { 92 | expect(resp.type).to.equal(ActivityTypes.Trace); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/botbuilder-feedback/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | } -------------------------------------------------------------------------------- /packages/botbuilder-feedback/test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "no-unused-expression": false 5 | } 6 | } -------------------------------------------------------------------------------- /packages/botbuilder-feedback/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "dist", 9 | "test" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/botbuilder-http-test-recorder/README.md: -------------------------------------------------------------------------------- 1 | # Http Test Recorder for Microsoft Bot Framework 2 | 3 | This directory contains sample code that can be used to build an HTTP recording mechanism, so that you can write effective unit tests for your bot. For example, if your bot logic relies on an external HTTP service like QnA Maker, botbuilder-http-test-recorder will help you record real responses during development so that they can be played back during unit testing without needing to make real network calls to external services. 4 | 5 | > It is advisable to regularly re-run captures against external services to ensure that your tests stay up to date. If you don't, your tests may not detect service changes that adversely affect your bot. Likewise, you should re-run captures whenever you upgrade any packages used by your bot. 6 | 7 | ## Prerequisites 8 | 9 | - A NodeJS bot using [Bot Framework v4](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0) 10 | - A bot that relies on external HTTP services (support for QnAMaker, LUIS, and Azure Search is provided, but extensions are available to support any service) 11 | 12 | ## Install 13 | 14 | Because this package is supplied as sample code, it is not available on npm and it comes with no guarantee of support or updates. To use this software in your own app: 15 | 16 | 1. clone this repo 17 | 2. `cd botbuilder-utils-js/packages/botbuilder-http-test-recorder` 18 | 3. `npm install` 19 | 4. `cd {your-app}` 20 | 5. `npm install file:path-to-botbuilder-utils-js/packages/botbuilder-http-test-recorder` 21 | 22 | > To support CI and other automation tasks, you may also choose to publish this package on a private npm repo, or simply copy the code/dependencies into your own app. 23 | 24 | ## Usage 25 | 26 | > JavaScript examples are shown below, but this package also works great in TypeScript projects. 27 | 28 | ### Recording HTTP traffic in the bot (Luis, Azure Search, and QnAMaker) 29 | 30 | This sample shows how to capture traffic from Luis, Azure Search, and QnAMaker. 31 | 32 | ```JavaScript 33 | const { BotFrameworkAdapter } = require('botbuilder'); 34 | const { HttpTestRecorder } = require('botbuilder-http-test-recorder'); 35 | 36 | const testRecorder = new HttpTestRecorder() 37 | .captureLuis() 38 | .captureAzureSearch(); 39 | .captureQnAMaker(); // some default capturing configurations are provided. for complete control use the optional constructor parameters 40 | const adapter = new BotFrameworkAdapter({ 41 | appId: process.env.MICROSOFT_APP_ID, 42 | appPassword: process.env.MICROSOFT_APP_PASSWORD, 43 | }).use(testRecorder); 44 | ``` 45 | 46 | When the HttpTestRecorder middleware is attached to your bot it will respond to several chat commands: 47 | 48 | * `rec:start`: Begin recording HTTP requests and responses. Only HTTP requests made to a matching host will be recorded. 49 | * `rec:stop[:name]` Stop recording, and give an optional recording name that describes the session. If no name is provided, a timestamp will be used. HTTP sessions are stored to disk (default location is at `./test/data`, relative to the root module). 50 | * `rec:cancel` Stop the recording without storing any requests. 51 | 52 | ### Recording HTTP traffic in the bot (generic endpoint) 53 | 54 | This sample shows how to capture traffic from a generic endpoint. It demonstrates how to log traffic when the URL contains "example.com", and how to obscure the key. 55 | 56 | ```JavaScript 57 | const { BotFrameworkAdapter } = require('botbuilder'); 58 | const { HttpTestRecorder } = require('botbuilder-http-test-recorder'); 59 | 60 | const testRecorder = new HttpTestRecorder({ 61 | requestFilter: [(req) => req.scope.indexOf("example.com") != -1], 62 | transformRequest: [(def) => { 63 | const EXAMPLE_KEY = /key=[^&]+/; 64 | const testKey = "******"; 65 | def.path = def.path.replace(EXAMPLE_KEY, `key=${testKey}`) 66 | return def; 67 | }] 68 | }); 69 | const adapter = new BotFrameworkAdapter({ 70 | appId: process.env.MICROSOFT_APP_ID, 71 | appPassword: process.env.MICROSOFT_APP_PASSWORD, 72 | }).use(testRecorder); 73 | ``` 74 | 75 | ### Playback HTTP responses during unit tests 76 | 77 | _This sample uses Mocha and Chai, but any test framework or assertion library will work_ 78 | 79 | ```JavaScript 80 | const { TestAdapter } = require('botbuilder'); 81 | const { LuisRecognizer } = require('botbuilder-ai'); 82 | const { HttpTestPlayback } = require('botbuilder-http-test-recorder'); 83 | 84 | describe('My bot', () => { 85 | it('should ask a question', () => { 86 | // folder name of your stored http session. see naming above at rec:stop[:name] 87 | const playback = new HttpTestPlayback({testDataDirectory: 'YOUR-TEST-DIRECTORY'}); 88 | 89 | // parameters should match the settings used in `textRecorder.captureLuis()` 90 | const luisRecognizer = new LuisRecognizer({ 91 | applicationId: 'testAppId', 92 | endpointKey: 'testKey', 93 | }); 94 | 95 | const adapter = new TestAdapter(async (context) => { 96 | // logic under test goes here 97 | const results = luisRecognizer.recognize(context); 98 | const intent = LuisRecognizer.topIntent(results); 99 | if (intent === 'None') { 100 | await context.sendActivity('I do not understand'); 101 | } else { 102 | await context.sendActivity('OK!'); 103 | } 104 | }); 105 | 106 | // file name of your stored http session 107 | playback.load('YOUR-TEST-FILE'); 108 | 109 | // execute the test logic 110 | await adapter 111 | .send('hello world') 112 | .assertReply((resp) => expect(resp.text).to.equal('OK!')); 113 | }); 114 | }); 115 | ``` 116 | Results from `luisRecognizer.recognize(...)` will reflect the stored LUIS response. 117 | At no point will an actual LUIS endpoint be hit by the TestAdapter 118 | 119 | ## API 120 | 121 | ## HttpTestRecorder (class) 122 | 123 | _Middleware to support automatic storage and cleansing of HTTP requests/responses for external services like LUIS._ 124 | 125 | _This class implements the [Middleware](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/middlewareSet.ts#L14-L16) interface._ 126 | 127 | 128 | ```TypeScript 129 | constructor(options?: HttpTestRecorderOptions) 130 | ``` 131 | 132 | * `options`: optional configuration parameters 133 | * `options.transformRequest` (`RequestTransformer[]`): stored requests/responses will be passed through these functions. use to remove secrets or change parts of the url or path 134 | * `options.requestFilter` (`RequestFilter[]`): only requests matching one or more of these filters will be stored 135 | 136 | > Learn more about [scope filtering](https://www.npmjs.com/package/nock#scope-filtering) 137 | > 138 | > If you are configuring a new external service, and you're not sure what to use for `requestFilter` or `transformRequest`, reference the implementation of [`captureLuis`](./src/index.ts#L88-L103) 139 | 140 | ```TypeScript 141 | captureLuis(testRegion = 'westus', testAppId = 'testAppId', testKey = 'testKey'): this 142 | ``` 143 | 144 | _Configure the test recorder to capture LUIS requests_ 145 | 146 | * `testRegion`: The live HTTP request will be stored as if it hit this region. Your unit tests should be configured to target this region. 147 | * `testKey`: The live HTTP request will be be stored as if this key was used. It should not be a real key, and your unit tests should be configured to use the same value. 148 | * _returns_ the `HttpTestRecorder` instance 149 | 150 | ```TypeScript 151 | captureQnAMaker(testRegion = 'westus', testKBId = 'testKBId', testKey = 'testKey'): this 152 | ``` 153 | 154 | _Configure the test recorder to capture QnA Maker requests_ 155 | 156 | * `testRegion`: The live HTTP request will be stored as if it hit this region. Your unit tests should be configured to target this region. 157 | * `testKBId`: The live HTTP request will be stored as if it hit this QnAMaker Knowledgebase. It should not be a real knowledgebase identifier and your unit tests should be configured to target this. 158 | * `testKey`: The live HTTP request will be be stored as if this key was used. It should not be a real key, and your unit tests should be configured to use the same value. 159 | * _returns_ the `HttpTestRecorder` instance 160 | 161 | ```TypeScript 162 | captureAzureSearch(testService = 'testsearch'): this 163 | ``` 164 | 165 | _Configure the test recorder to capture Azure Search requests_ 166 | 167 | * `testService`: The live HTTP request will be stored as if it hit this search service. It should not be a real service name, and your unit tests should be configured to use the same value. 168 | * _returns_ the `HttpTestRecorder` instance 169 | 170 | ```TypeScript 171 | createPlayback(): HttpTestPlayback 172 | ``` 173 | 174 | _Create a new playback instance to use in unit tests_ 175 | 176 | * _returns_ a configured `HttpTestPlayback` intance that you can use in your unit tests. 177 | 178 | ## HttpTestPlayback (class) 179 | 180 | _Test utility that will load stored HTTP sessions from disk and intercept subsequent requests._ 181 | 182 | ```TypeScript 183 | constructor(options?: HttpTestFileOptions) 184 | ``` 185 | 186 | * `options`: optional configuration parameters 187 | * `options.testDataDirectory`: path to store captured JSON request/response data (default = `./test/data`, relative to root module). this directory will be created if it does not exist 188 | 189 | ```TypeScript 190 | load(name: string, disableOutboundHttp = true): Scope[] 191 | ``` 192 | 193 | * `name`: Name of the saved session (see `rec:stop` above) 194 | * `disableOutboundHttp`: prevent any HTTP request that is not defined in the recorded session from completing. An error will be thrown if an unexpected HTTP request is made by your bot. 195 | * _returns_ array of Nock Scope objects 196 | 197 | ```TypeScript 198 | isDone() 199 | ``` 200 | 201 | _Returns true if all loaded HTTP sessions have been served. You can use this in your `afterEach()` callback to ensure that all expected requests were made by the bot_ 202 | 203 | ## Other Types 204 | 205 | ```TypeScript 206 | type RequestTransformer = (request: NockDefinition) => NockDefinition; 207 | ``` 208 | _Transform a request to protect secrets, etc._ 209 | 210 | ```TypeScript 211 | type RequestFilter = (request: NockDefinition) => boolean; 212 | ``` 213 | _Return true if the given request should stored in the captured HTTP session_ 214 | 215 | > See [Nock package documentation](https://www.npmjs.com/package/nock) for more information about `Scopes` and `NockDefinitions` 216 | -------------------------------------------------------------------------------- /packages/botbuilder-http-test-recorder/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-http-test-recorder", 3 | "version": "4.2.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/chai": { 8 | "version": "4.1.4", 9 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.4.tgz", 10 | "integrity": "sha512-h6+VEw2Vr3ORiFCyyJmcho2zALnUq9cvdB/IO8Xs9itrJVCenC7o26A6+m7D0ihTTr65eS259H5/Ghl/VjYs6g==", 11 | "dev": true 12 | }, 13 | "@types/fs-extra": { 14 | "version": "5.0.4", 15 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/@types/fs-extra/-/fs-extra-5.0.4.tgz", 16 | "integrity": "sha512-DsknoBvD8s+RFfSGjmERJ7ZOP1HI0UZRA3FSI+Zakhrc/Gy26YQsLI+m5V5DHxroHRJqCDLKJp7Hixn8zyaF7g==", 17 | "requires": { 18 | "@types/node": "10.9.4" 19 | } 20 | }, 21 | "@types/mocha": { 22 | "version": "5.2.5", 23 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", 24 | "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==", 25 | "dev": true 26 | }, 27 | "@types/nock": { 28 | "version": "9.3.0", 29 | "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.3.0.tgz", 30 | "integrity": "sha512-ZHf/X8rTQ5Tb1rHjxIJYqm55uO265agE3G7NoSXVa2ep+EcJXgB2fsme+zBvK7MhrxTwkC/xkB6THyv50u0MGw==", 31 | "requires": { 32 | "@types/node": "10.9.4" 33 | } 34 | }, 35 | "@types/node": { 36 | "version": "10.9.4", 37 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", 38 | "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==" 39 | }, 40 | "ansi-regex": { 41 | "version": "2.1.1", 42 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 43 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 44 | "dev": true 45 | }, 46 | "ansi-styles": { 47 | "version": "2.2.1", 48 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 49 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", 50 | "dev": true 51 | }, 52 | "argparse": { 53 | "version": "1.0.10", 54 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 55 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 56 | "dev": true, 57 | "requires": { 58 | "sprintf-js": "1.0.3" 59 | } 60 | }, 61 | "arrify": { 62 | "version": "1.0.1", 63 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 64 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 65 | "dev": true 66 | }, 67 | "assert": { 68 | "version": "1.4.1", 69 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/assert/-/assert-1.4.1.tgz", 70 | "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", 71 | "dev": true, 72 | "requires": { 73 | "util": "0.10.3" 74 | } 75 | }, 76 | "assertion-error": { 77 | "version": "1.1.0", 78 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 79 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" 80 | }, 81 | "babel-code-frame": { 82 | "version": "6.26.0", 83 | "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", 84 | "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", 85 | "dev": true, 86 | "requires": { 87 | "chalk": "1.1.3", 88 | "esutils": "2.0.2", 89 | "js-tokens": "3.0.2" 90 | }, 91 | "dependencies": { 92 | "chalk": { 93 | "version": "1.1.3", 94 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 95 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 96 | "dev": true, 97 | "requires": { 98 | "ansi-styles": "2.2.1", 99 | "escape-string-regexp": "1.0.5", 100 | "has-ansi": "2.0.0", 101 | "strip-ansi": "3.0.1", 102 | "supports-color": "2.0.0" 103 | } 104 | }, 105 | "supports-color": { 106 | "version": "2.0.0", 107 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 108 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", 109 | "dev": true 110 | } 111 | } 112 | }, 113 | "balanced-match": { 114 | "version": "1.0.0", 115 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 116 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 117 | "dev": true 118 | }, 119 | "botbuilder-core": { 120 | "version": "4.0.6", 121 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/botbuilder-core/-/botbuilder-core-4.0.6.tgz", 122 | "integrity": "sha1-sDiiKJ8E/+8pu6YVoHd1e1Q8WZY=", 123 | "dev": true, 124 | "requires": { 125 | "assert": "1.4.1", 126 | "botframework-schema": "4.0.6" 127 | } 128 | }, 129 | "botframework-schema": { 130 | "version": "4.0.6", 131 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/botframework-schema/-/botframework-schema-4.0.6.tgz", 132 | "integrity": "sha1-SD8JoSqnNjheHmdG9cBl2yrhTEI=", 133 | "dev": true, 134 | "requires": { 135 | "@types/node": "9.6.32" 136 | }, 137 | "dependencies": { 138 | "@types/node": { 139 | "version": "9.6.32", 140 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/@types/node/-/node-9.6.32.tgz", 141 | "integrity": "sha1-G2QTT2MLMMnNpIEKpKlPwtQUHb0=", 142 | "dev": true 143 | } 144 | } 145 | }, 146 | "brace-expansion": { 147 | "version": "1.1.11", 148 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 149 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 150 | "dev": true, 151 | "requires": { 152 | "balanced-match": "1.0.0", 153 | "concat-map": "0.0.1" 154 | } 155 | }, 156 | "browser-stdout": { 157 | "version": "1.3.1", 158 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 159 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 160 | "dev": true 161 | }, 162 | "buffer-from": { 163 | "version": "1.1.1", 164 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/buffer-from/-/buffer-from-1.1.1.tgz", 165 | "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=", 166 | "dev": true 167 | }, 168 | "builtin-modules": { 169 | "version": "1.1.1", 170 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 171 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 172 | "dev": true 173 | }, 174 | "chai": { 175 | "version": "4.1.2", 176 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 177 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 178 | "requires": { 179 | "assertion-error": "1.1.0", 180 | "check-error": "1.0.2", 181 | "deep-eql": "3.0.1", 182 | "get-func-name": "2.0.0", 183 | "pathval": "1.1.0", 184 | "type-detect": "4.0.8" 185 | } 186 | }, 187 | "chalk": { 188 | "version": "2.4.1", 189 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 190 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 191 | "dev": true, 192 | "requires": { 193 | "ansi-styles": "3.2.1", 194 | "escape-string-regexp": "1.0.5", 195 | "supports-color": "5.4.0" 196 | }, 197 | "dependencies": { 198 | "ansi-styles": { 199 | "version": "3.2.1", 200 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 201 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 202 | "dev": true, 203 | "requires": { 204 | "color-convert": "1.9.2" 205 | } 206 | } 207 | } 208 | }, 209 | "check-error": { 210 | "version": "1.0.2", 211 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 212 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" 213 | }, 214 | "color-convert": { 215 | "version": "1.9.2", 216 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", 217 | "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", 218 | "dev": true, 219 | "requires": { 220 | "color-name": "1.1.1" 221 | } 222 | }, 223 | "color-name": { 224 | "version": "1.1.1", 225 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", 226 | "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", 227 | "dev": true 228 | }, 229 | "commander": { 230 | "version": "2.15.1", 231 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 232 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 233 | "dev": true 234 | }, 235 | "concat-map": { 236 | "version": "0.0.1", 237 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 238 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 239 | "dev": true 240 | }, 241 | "debug": { 242 | "version": "3.1.0", 243 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 244 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 245 | "requires": { 246 | "ms": "2.0.0" 247 | } 248 | }, 249 | "deep-eql": { 250 | "version": "3.0.1", 251 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 252 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 253 | "requires": { 254 | "type-detect": "4.0.8" 255 | } 256 | }, 257 | "deep-equal": { 258 | "version": "1.0.1", 259 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 260 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" 261 | }, 262 | "diff": { 263 | "version": "3.5.0", 264 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 265 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 266 | "dev": true 267 | }, 268 | "escape-string-regexp": { 269 | "version": "1.0.5", 270 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 271 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 272 | "dev": true 273 | }, 274 | "esprima": { 275 | "version": "4.0.1", 276 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 277 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 278 | "dev": true 279 | }, 280 | "esutils": { 281 | "version": "2.0.2", 282 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 283 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 284 | "dev": true 285 | }, 286 | "fs-extra": { 287 | "version": "7.0.0", 288 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/fs-extra/-/fs-extra-7.0.0.tgz", 289 | "integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==", 290 | "requires": { 291 | "graceful-fs": "4.1.11", 292 | "jsonfile": "4.0.0", 293 | "universalify": "0.1.2" 294 | } 295 | }, 296 | "fs.realpath": { 297 | "version": "1.0.0", 298 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 299 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 300 | "dev": true 301 | }, 302 | "get-func-name": { 303 | "version": "2.0.0", 304 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 305 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" 306 | }, 307 | "glob": { 308 | "version": "7.1.2", 309 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 310 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 311 | "dev": true, 312 | "requires": { 313 | "fs.realpath": "1.0.0", 314 | "inflight": "1.0.6", 315 | "inherits": "2.0.3", 316 | "minimatch": "3.0.4", 317 | "once": "1.4.0", 318 | "path-is-absolute": "1.0.1" 319 | } 320 | }, 321 | "graceful-fs": { 322 | "version": "4.1.11", 323 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/graceful-fs/-/graceful-fs-4.1.11.tgz", 324 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" 325 | }, 326 | "growl": { 327 | "version": "1.10.5", 328 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 329 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 330 | "dev": true 331 | }, 332 | "has-ansi": { 333 | "version": "2.0.0", 334 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 335 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 336 | "dev": true, 337 | "requires": { 338 | "ansi-regex": "2.1.1" 339 | } 340 | }, 341 | "has-flag": { 342 | "version": "3.0.0", 343 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 344 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 345 | "dev": true 346 | }, 347 | "he": { 348 | "version": "1.1.1", 349 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 350 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 351 | "dev": true 352 | }, 353 | "inflight": { 354 | "version": "1.0.6", 355 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 356 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 357 | "dev": true, 358 | "requires": { 359 | "once": "1.4.0", 360 | "wrappy": "1.0.2" 361 | } 362 | }, 363 | "inherits": { 364 | "version": "2.0.3", 365 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 366 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 367 | "dev": true 368 | }, 369 | "js-tokens": { 370 | "version": "3.0.2", 371 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 372 | "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", 373 | "dev": true 374 | }, 375 | "js-yaml": { 376 | "version": "3.12.0", 377 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", 378 | "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", 379 | "dev": true, 380 | "requires": { 381 | "argparse": "1.0.10", 382 | "esprima": "4.0.1" 383 | } 384 | }, 385 | "json-stringify-safe": { 386 | "version": "5.0.1", 387 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 388 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 389 | }, 390 | "jsonfile": { 391 | "version": "4.0.0", 392 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/jsonfile/-/jsonfile-4.0.0.tgz", 393 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 394 | "requires": { 395 | "graceful-fs": "4.1.11" 396 | } 397 | }, 398 | "lodash": { 399 | "version": "4.17.10", 400 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", 401 | "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" 402 | }, 403 | "make-error": { 404 | "version": "1.3.5", 405 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/make-error/-/make-error-1.3.5.tgz", 406 | "integrity": "sha1-7+ToH22yjK3WBccPKcgxtY73dsg=", 407 | "dev": true 408 | }, 409 | "minimatch": { 410 | "version": "3.0.4", 411 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 412 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 413 | "dev": true, 414 | "requires": { 415 | "brace-expansion": "1.1.11" 416 | } 417 | }, 418 | "minimist": { 419 | "version": "0.0.8", 420 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 421 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 422 | }, 423 | "mkdirp": { 424 | "version": "0.5.1", 425 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 426 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 427 | "requires": { 428 | "minimist": "0.0.8" 429 | } 430 | }, 431 | "mocha": { 432 | "version": "5.2.0", 433 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 434 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 435 | "dev": true, 436 | "requires": { 437 | "browser-stdout": "1.3.1", 438 | "commander": "2.15.1", 439 | "debug": "3.1.0", 440 | "diff": "3.5.0", 441 | "escape-string-regexp": "1.0.5", 442 | "glob": "7.1.2", 443 | "growl": "1.10.5", 444 | "he": "1.1.1", 445 | "minimatch": "3.0.4", 446 | "mkdirp": "0.5.1", 447 | "supports-color": "5.4.0" 448 | } 449 | }, 450 | "ms": { 451 | "version": "2.0.0", 452 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 453 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 454 | }, 455 | "nock": { 456 | "version": "9.6.1", 457 | "resolved": "https://registry.npmjs.org/nock/-/nock-9.6.1.tgz", 458 | "integrity": "sha512-EDgl/WgNQ0C1BZZlASOQkQdE6tAWXJi8QQlugqzN64JJkvZ7ILijZuG24r4vCC7yOfnm6HKpne5AGExLGCeBWg==", 459 | "requires": { 460 | "chai": "4.1.2", 461 | "debug": "3.1.0", 462 | "deep-equal": "1.0.1", 463 | "json-stringify-safe": "5.0.1", 464 | "lodash": "4.17.10", 465 | "mkdirp": "0.5.1", 466 | "propagate": "1.0.0", 467 | "qs": "6.5.2", 468 | "semver": "5.5.0" 469 | } 470 | }, 471 | "once": { 472 | "version": "1.4.0", 473 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 474 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 475 | "dev": true, 476 | "requires": { 477 | "wrappy": "1.0.2" 478 | } 479 | }, 480 | "path-is-absolute": { 481 | "version": "1.0.1", 482 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 483 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 484 | "dev": true 485 | }, 486 | "path-parse": { 487 | "version": "1.0.5", 488 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 489 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 490 | "dev": true 491 | }, 492 | "pathval": { 493 | "version": "1.1.0", 494 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 495 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" 496 | }, 497 | "propagate": { 498 | "version": "1.0.0", 499 | "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", 500 | "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=" 501 | }, 502 | "qs": { 503 | "version": "6.5.2", 504 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 505 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 506 | }, 507 | "resolve": { 508 | "version": "1.8.1", 509 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", 510 | "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", 511 | "dev": true, 512 | "requires": { 513 | "path-parse": "1.0.5" 514 | } 515 | }, 516 | "semver": { 517 | "version": "5.5.0", 518 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", 519 | "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" 520 | }, 521 | "source-map": { 522 | "version": "0.6.1", 523 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/source-map/-/source-map-0.6.1.tgz", 524 | "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", 525 | "dev": true 526 | }, 527 | "source-map-support": { 528 | "version": "0.5.9", 529 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/source-map-support/-/source-map-support-0.5.9.tgz", 530 | "integrity": "sha1-QbyVOyU0Jn6i1gW8z6e/oxEc7V8=", 531 | "dev": true, 532 | "requires": { 533 | "buffer-from": "1.1.1", 534 | "source-map": "0.6.1" 535 | } 536 | }, 537 | "sprintf-js": { 538 | "version": "1.0.3", 539 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 540 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 541 | "dev": true 542 | }, 543 | "strip-ansi": { 544 | "version": "3.0.1", 545 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 546 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 547 | "dev": true, 548 | "requires": { 549 | "ansi-regex": "2.1.1" 550 | } 551 | }, 552 | "supports-color": { 553 | "version": "5.4.0", 554 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 555 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 556 | "dev": true, 557 | "requires": { 558 | "has-flag": "3.0.0" 559 | } 560 | }, 561 | "ts-node": { 562 | "version": "7.0.1", 563 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/ts-node/-/ts-node-7.0.1.tgz", 564 | "integrity": "sha1-lWLcLR5tJI0kvFX3c+P2FDN9m68=", 565 | "dev": true, 566 | "requires": { 567 | "arrify": "1.0.1", 568 | "buffer-from": "1.1.1", 569 | "diff": "3.5.0", 570 | "make-error": "1.3.5", 571 | "minimist": "1.2.0", 572 | "mkdirp": "0.5.1", 573 | "source-map-support": "0.5.9", 574 | "yn": "2.0.0" 575 | }, 576 | "dependencies": { 577 | "minimist": { 578 | "version": "1.2.0", 579 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 580 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 581 | "dev": true 582 | } 583 | } 584 | }, 585 | "tslib": { 586 | "version": "1.9.3", 587 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 588 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", 589 | "dev": true 590 | }, 591 | "tslint": { 592 | "version": "5.11.0", 593 | "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", 594 | "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", 595 | "dev": true, 596 | "requires": { 597 | "babel-code-frame": "6.26.0", 598 | "builtin-modules": "1.1.1", 599 | "chalk": "2.4.1", 600 | "commander": "2.15.1", 601 | "diff": "3.5.0", 602 | "glob": "7.1.2", 603 | "js-yaml": "3.12.0", 604 | "minimatch": "3.0.4", 605 | "resolve": "1.8.1", 606 | "semver": "5.5.0", 607 | "tslib": "1.9.3", 608 | "tsutils": "2.28.0" 609 | } 610 | }, 611 | "tsutils": { 612 | "version": "2.28.0", 613 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", 614 | "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", 615 | "dev": true, 616 | "requires": { 617 | "tslib": "1.9.3" 618 | } 619 | }, 620 | "type-detect": { 621 | "version": "4.0.8", 622 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 623 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" 624 | }, 625 | "typescript": { 626 | "version": "2.9.2", 627 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", 628 | "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", 629 | "dev": true 630 | }, 631 | "universalify": { 632 | "version": "0.1.2", 633 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/universalify/-/universalify-0.1.2.tgz", 634 | "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=" 635 | }, 636 | "util": { 637 | "version": "0.10.3", 638 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/util/-/util-0.10.3.tgz", 639 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", 640 | "dev": true, 641 | "requires": { 642 | "inherits": "2.0.1" 643 | }, 644 | "dependencies": { 645 | "inherits": { 646 | "version": "2.0.1", 647 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/inherits/-/inherits-2.0.1.tgz", 648 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 649 | "dev": true 650 | } 651 | } 652 | }, 653 | "wrappy": { 654 | "version": "1.0.2", 655 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 656 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 657 | "dev": true 658 | }, 659 | "yn": { 660 | "version": "2.0.0", 661 | "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", 662 | "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", 663 | "dev": true 664 | } 665 | } 666 | } 667 | -------------------------------------------------------------------------------- /packages/botbuilder-http-test-recorder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-http-test-recorder", 3 | "version": "4.2.2", 4 | "description": "Middleware for Microsoft Bot Framework to facilitate mocking of HTTP response when testing logic that relies on external services like LUIS", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "mocha --require ts-node/register \"test/**/*.{ts,tsx}\"", 10 | "pretest": "npm run build", 11 | "prepare": "npm run build", 12 | "prepublishOnly": "npm --no-git-tag-version version patch" 13 | }, 14 | "private": true, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Microsoft/botbuilder-utils-js.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/Microsoft/botbuilder-utils-js/issues" 21 | }, 22 | "keywords": [ 23 | "botbuilder", 24 | "botframework", 25 | "bots", 26 | "chatbots", 27 | "testing", 28 | "mocks" 29 | ], 30 | "author": "Microsoft Corp.", 31 | "license": "MIT", 32 | "devDependencies": { 33 | "@types/chai": "^4.1.4", 34 | "@types/mocha": "^5.2.5", 35 | "botbuilder-core": "^4.0.6", 36 | "chai": "^4.1.2", 37 | "mocha": "^5.2.0", 38 | "ts-node": "^7.0.1", 39 | "tslint": "^5.11.0", 40 | "typescript": "^2.9.2" 41 | }, 42 | "dependencies": { 43 | "@types/fs-extra": "^5.0.4", 44 | "@types/nock": "^9.3.0", 45 | "fs-extra": "^7.0.0", 46 | "nock": "^9.6.1" 47 | }, 48 | "peerDependencies": { 49 | "botbuilder-core": "^4.0.6" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/botbuilder-http-test-recorder/src/find-root-module-dir.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { dirname } from 'path'; 5 | 6 | export function findRootModuleDir() { 7 | let m = module; 8 | let p: NodeModule; 9 | while (m) { 10 | if (m.parent) { 11 | p = m.parent; 12 | } 13 | m = m.parent; 14 | } 15 | 16 | return dirname(p.filename); 17 | } 18 | -------------------------------------------------------------------------------- /packages/botbuilder-http-test-recorder/src/http-test-playback.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { cleanAll, disableNetConnect, isDone, load, Scope } from 'nock'; 5 | import * as path from 'path'; 6 | 7 | import { HttpTestFileOptions } from "."; 8 | import { findRootModuleDir } from './find-root-module-dir'; 9 | 10 | /** 11 | * Test utility that will load stored HTTP sessions from disk and intercept subsequent requests. 12 | */ 13 | export class HttpTestPlayback { 14 | /** 15 | * Create a new instance of the Playback utility 16 | * @param options optional playback options 17 | */ 18 | constructor(private options?: HttpTestFileOptions) { 19 | this.options = options || {}; 20 | 21 | if (!this.options.testDataDirectory) { 22 | this.options.testDataDirectory = path.join(findRootModuleDir(), 'test', 'data'); 23 | } 24 | } 25 | 26 | /** 27 | * Load a saved HTTP session from disk, serving requests from the saved session instead of making real HTTP requests 28 | * @param name Name of the saved session 29 | * @param disableOutboundHttp Disable all outbound HTTP traffic (i.e. serve only the content loaded in the saved session) 30 | */ 31 | load(name: string, disableOutboundHttp = true): Scope[] { 32 | if (!isDone()) { 33 | throw new Error('HttpTestPlayback still has outstanding responses that were never requested!'); 34 | } 35 | if (disableOutboundHttp) { 36 | disableNetConnect(); 37 | } 38 | cleanAll(); 39 | if (!name.endsWith('.json')) { 40 | name += '.json'; 41 | } 42 | return load(path.join(this.options.testDataDirectory, name)); 43 | } 44 | 45 | /** 46 | * Returns true if all loaded HTTP sessions have been served 47 | */ 48 | isDone() { 49 | return isDone(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/botbuilder-http-test-recorder/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { Middleware, TurnContext } from 'botbuilder-core'; 5 | import * as fs from 'fs-extra'; 6 | import { NockDefinition, recorder, restore } from 'nock'; 7 | import * as path from 'path'; 8 | 9 | import { findRootModuleDir } from './find-root-module-dir'; 10 | import { HttpTestPlayback } from './http-test-playback'; 11 | 12 | const LUIS_HOST = /^https:\/\/[^.]+\.api\.cognitive\.microsoft\.com:443/; 13 | const LUIS_PATH = /\/luis\/v2.0\/apps\/[^?]+/; 14 | const LUIS_QS_KEY = /subscription-key=[^&]+/; 15 | const AZURE_SEARCH_HOST = /^https:\/\/[^.]+\.search\.windows\.net:443/; 16 | const QNA_MAKER_HOST = /^https:\/\/[^.]+\.azurewebsites\.net:443/; 17 | const QNA_MAKER_PATH = /\/qnamaker\/knowledgebases\/[^\/]+\/generateanswer/; 18 | const SESSION_NAME = /rec:(?:end|stop):(.+)/i; 19 | 20 | export type RequestTransformer = (request: NockDefinition) => NockDefinition; 21 | export type RequestFilter = (request: NockDefinition) => boolean; 22 | 23 | export { HttpTestPlayback } from './http-test-playback'; 24 | 25 | export interface HttpTestFileOptions { 26 | /** path to store captured JSON request/response data (default = `./test/data`, relative to root module). this directory will be created if it does not exist */ 27 | testDataDirectory?: string; 28 | } 29 | 30 | export interface HttpTestRecorderOptions extends HttpTestFileOptions { 31 | /** stored requests/responses will be passed through these functions. use to remove secrets or change parts of the url or path */ 32 | transformRequest?: RequestTransformer[]; 33 | 34 | /** only requests matching all of these filters will be stored */ 35 | requestFilter?: RequestFilter[]; 36 | } 37 | 38 | /** 39 | * Middleware to support automatic storage and cleansing of HTTP requests/responses for external services like LUIS. 40 | * Stored HTTP response can be loaded into your unit tests to validate your bot locig without requiring actual network calls to supporting services. 41 | */ 42 | export class HttpTestRecorder implements Middleware { 43 | /** 44 | * Create a new recorder instance 45 | * @param options optional middleware configuration 46 | */ 47 | constructor(private options?: HttpTestRecorderOptions) { 48 | this.options = options || {}; 49 | 50 | if (!this.options.requestFilter) { 51 | this.options.requestFilter = []; 52 | } else if (!Array.isArray(this.options.requestFilter)) { 53 | this.options.requestFilter = [this.options.requestFilter]; 54 | } 55 | 56 | if (!this.options.transformRequest) { 57 | this.options.transformRequest = []; 58 | } else if (!Array.isArray(this.options.transformRequest)) { 59 | this.options.transformRequest = [this.options.transformRequest]; 60 | } 61 | 62 | if (!this.options.testDataDirectory) { 63 | this.options.testDataDirectory = path.join(findRootModuleDir(), 'test', 'data'); 64 | } 65 | } 66 | 67 | async onTurn(context: TurnContext, next: () => Promise): Promise { 68 | 69 | // extract optional session name from the end directive 70 | const sessionName = this.extractSessionName(context); 71 | 72 | const text = context.activity.text ? context.activity.text.toLowerCase() : context.activity.text; 73 | 74 | // look for recording directives 75 | switch (text) { 76 | case 'rec:start': 77 | return this.startRecording(context); 78 | case 'rec:clear': 79 | case 'rec:reset': 80 | case 'rec:cancel': 81 | return this.clearRecording(context); 82 | case 'rec:stop': 83 | case 'rec:end': 84 | return this.stopRecording(context, sessionName); 85 | 86 | default: 87 | return next(); 88 | } 89 | } 90 | 91 | /** 92 | * Configure the test recorder to capture LUIS requests 93 | * @param testRegion replace region in captured request with this value 94 | * @param testKey replace key in captured request query params with this value 95 | */ 96 | captureLuis(testRegion = 'westus', testAppId = 'testAppId', testKey = 'testKey') { 97 | if (Array.isArray(this.options.requestFilter)) { 98 | this.options.requestFilter.push((req) => LUIS_HOST.test(req.scope)); 99 | } 100 | if (Array.isArray(this.options.transformRequest)) { 101 | this.options.transformRequest.push((req) => { 102 | req.path = req.path 103 | .replace(LUIS_QS_KEY, `subscription-key=${testKey}`) 104 | .replace(LUIS_PATH, `/luis/v2.0/apps/${testAppId}`); 105 | req.scope = req.scope 106 | .replace(LUIS_HOST, `https://${testRegion}.api.cognitive.microsoft.com:443`); 107 | 108 | // see nock/nock#1229 109 | // nock is incorrectly identifying JSON requests with extra Content-Type directives as non-JSON 110 | // e.g. Content-Type: application/json; charset=utf8 111 | try { 112 | JSON.parse(req.body); 113 | } catch (err) { 114 | // body was not valid JSON, stringify it 115 | req.body = JSON.stringify(req.body); 116 | } 117 | return req; 118 | }); 119 | } 120 | return this; 121 | } 122 | 123 | /** 124 | * Configure the test recorder to capture QnAMaker requests 125 | * @param testRegion replace region in captured request with this value 126 | * @param testKey replace key in captured request query params with this value 127 | * @param testKBId replace knowledgebase id in captured request with this value 128 | */ 129 | captureQnAMaker(testRegion = 'westus', testKBId = 'testKBId', testKey = 'testKey') { 130 | if (Array.isArray(this.options.requestFilter)) { 131 | this.options.requestFilter.push((req) => QNA_MAKER_HOST.test(req.scope)); 132 | } 133 | if (Array.isArray(this.options.transformRequest)) { 134 | this.options.transformRequest.push((req: NockDefinition & { rawHeaders: string[] }) => { 135 | req.rawHeaders = req.rawHeaders 136 | .filter((e) => !e.includes('Set-Cookie') && !e.includes('ARRAffinity')); 137 | req.path = req.path 138 | .replace(QNA_MAKER_PATH, `/qnamaker/knowledgebases/${testKBId}/generateanswer`); 139 | req.scope = req.scope 140 | .replace(QNA_MAKER_HOST, `https://${testRegion}.azurewebsites.net/qnamaker`); 141 | return req; 142 | }); 143 | } 144 | return this; 145 | } 146 | 147 | /** 148 | * Configure the test recorder to capture Azure Search requests 149 | * @param testService replace search service name in captured request with this value 150 | */ 151 | captureAzureSearch(testService = 'testsearch') { 152 | if (Array.isArray(this.options.requestFilter)) { 153 | this.options.requestFilter.push((req) => AZURE_SEARCH_HOST.test(req.scope)); 154 | } 155 | if (Array.isArray(this.options.transformRequest)) { 156 | this.options.transformRequest.push((req) => { 157 | req.scope = req.scope 158 | .replace(AZURE_SEARCH_HOST, `https://${testService}.search.windows.net:443`); 159 | return req; 160 | }); 161 | } 162 | 163 | return this; 164 | } 165 | 166 | /** 167 | * Create a new playback instance to use in unit tests 168 | */ 169 | createPlayback() { 170 | return new HttpTestPlayback(this.options); 171 | } 172 | 173 | private extractSessionName(context: TurnContext) { 174 | const matchedName = SESSION_NAME.exec(context.activity.text); 175 | const sessionName = matchedName 176 | ? matchedName[1].replace(/[^\w-.]/g, '_') 177 | : null; 178 | context.activity.text = sessionName ? 'rec:end' : context.activity.text; 179 | return sessionName; 180 | } 181 | 182 | private async startRecording(context: TurnContext): Promise { 183 | try { 184 | recorder.rec({ 185 | dont_print: true, 186 | output_objects: true, 187 | }); 188 | await context.sendActivity(`⏺️ HTTP recording has started`); 189 | await context.sendActivity(`⏺️ ${this.options.requestFilter.length} filter(s) are in use.`); 190 | await context.sendActivity('⏺️ Say `rec:stop` or `rec:stop:mySessionName` to stop recording and write requests and responses. Say `rec:clear` to clear the recorder.'); 191 | } catch (err) { 192 | await context.sendActivity(`**ERROR**: ${err.message}`); 193 | console.error(err); 194 | } 195 | } 196 | 197 | private async clearRecording(context: TurnContext): Promise { 198 | try { 199 | recorder.clear(); 200 | restore(); 201 | await context.sendActivity('Recording has stopped'); 202 | } catch (err) { 203 | await context.sendActivity(`**ERROR**: ${err.message}`); 204 | console.error(err); 205 | } 206 | } 207 | 208 | private async stopRecording(context: TurnContext, name?: string) { 209 | 210 | try { 211 | name = name || new Date().toISOString().replace(/[:-]/g, '_'); 212 | const filePath = path.join(this.options.testDataDirectory, `${name}.json`); 213 | const requests = (recorder.play() as NockDefinition[]) 214 | .filter((req) => this.options.requestFilter.length && this.options.requestFilter.some((filter) => filter(req))) 215 | .map((req) => this.options.transformRequest.reduce((m, xform) => xform(m), req)); 216 | recorder.clear(); 217 | restore(); 218 | 219 | await fs.ensureDir(this.options.testDataDirectory); 220 | await fs.writeFile(filePath, JSON.stringify(requests, null, 2)); 221 | await context.sendActivity(`⏺️ HTTP recording has stopped.`); 222 | await context.sendActivity(`⏺️ Requests and responses were written to \`${filePath}\`.`); 223 | } catch (err) { 224 | await context.sendActivity(`**ERROR**: ${err.message}`); 225 | console.error(err); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /packages/botbuilder-http-test-recorder/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | } -------------------------------------------------------------------------------- /packages/botbuilder-http-test-recorder/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "dist", 9 | "test" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/README.md: -------------------------------------------------------------------------------- 1 | # Application Insights Transcript Store for Microsoft Bot Framework 2 | 3 | This directory contains sample code that can be used to build a [TranscriptLogger](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/transcriptLogger.ts) that stores and queries bot transcripts backed by Application Insights. 4 | 5 | ## Prerequisites 6 | 7 | - An [App Insights](https://docs.microsoft.com/en-us/azure/application-insights/app-insights-nodejs-quick-start) deployment 8 | - A NodeJS bot using [Bot Framework v4](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0) 9 | 10 | ## Install 11 | 12 | Because this package is provided as sample code, it is not available on npm and it comes with no guarantee of support or updates. To use this software in your own app: 13 | 14 | 1. clone this repo 15 | 2. `cd botbuilder-utils-js/packages/botbuilder-transcript-app-insights` 16 | 3. `npm install` 17 | 4. `cd {your-app}` 18 | 5. `npm install file:path-to-botbuilder-utils-js/packages/botbuilder-transcript-app-insights` 19 | 6. `npm install applicationinsights` (if you don't already have it) 20 | 21 | > To support CI and other automation tasks, you may also choose to publish this package on a private npm repo, or simply copy the code/dependencies into your own app. 22 | 23 | ## Usage 24 | 25 | > JavaScript example is shown below, but this package also works great in TypeScript projects. 26 | 27 | ```JavaScript 28 | const { BotFrameworkAdapter, TranscriptLoggerMiddleware } = require('botbuilder'); 29 | const { AppInsightsTranscriptStore } = require('botbuilder-transcript-app-insights'); 30 | const { TelemetryClient } = require('applicationinsights'); 31 | 32 | // App Insights configuration 33 | const appInsightsIKey = ''; 34 | const client = new TelemetryClient(appInsightsIKey); 35 | 36 | // Attach store to middleware and bot 37 | const store = new AppInsightsTranscriptStore(client); 38 | const logger = new TranscriptLoggerMiddleware(store); 39 | const adapter = new BotFrameworkAdapter({ 40 | appId: process.env.MICROSOFT_APP_ID, 41 | appPassword: process.env.MICROSOFT_APP_PASSWORD, 42 | }).use(logger); 43 | ``` 44 | 45 | Attaching the middleware to your bot adapter logs every incoming and outgoing Activity between the user and the bot to your App Insights instance. 46 | 47 | ## API 48 | 49 | ### AppInsightsTranscriptStore (class) 50 | 51 | ```TypeScript 52 | constructor(client: TelemetryClient, options?: AppInsightsTranscriptOptions) 53 | ``` 54 | 55 | * `client`: Provide your configured App Insights TelemetryClient instance from the `applicationinsights` package. 56 | * `options` Optional configuration parameters 57 | * `options.query` Optional, only needed if you will call the data access functions to retrieve transcripts and activities. 58 | * `options.query.applicationId` (`string`): Application id for API access 59 | * `options.query.readKey` (`string`): API access key with _Read telemetry_ permissions 60 | * `options.filterableActivityProperties` (`string[]`): Optional nested values on each Activity object that should be promoted as a queryable AppInsights property. Use dot notation to access nested property members. See [usage](#usage) for examples. 61 | 62 | > Learn how to [get your API key and Application ID](https://dev.applicationinsights.io/documentation/Authorization/API-key-and-App-ID) 63 | 64 | This class implements the [TranscriptStore](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/transcriptLogger.ts#L154-L183) interface, which includes functions to support retrieval of transcripts and activities. 65 | 66 | This class does _not_ implement `deleteTranscript()` due to the immutable nature of App Insights records. Calling this function will result in a thrown `Error`. 67 | 68 | ## Schema 69 | 70 | > Learn more about [App Insights Analytics](https://docs.microsoft.com/en-us/azure/application-insights/app-insights-analytics). 71 | 72 | Each transcript activity is stored in App Insights as a `customEvent`. Because [customEvent properties](https://docs.microsoft.com/en-us/azure/application-insights/app-insights-api-custom-events-metrics#properties) are always `string` values, activities are stored in a special way: 73 | 74 | * All top-level string values of the activity are stored verbatim as filterable properties 75 | * Any non-string values of the activity (arrays, complex objects, number, boolean, Date) are stored as JSON-encoded strings. These property names are prefixed by a `_` character. 76 | * Select nested activity strings are copied to top-level properties so that they can be used in App Insights analytics filters. These property names are prefixed by a `$` character: 77 | * `$conversationId` <= `activity.conversation.id` 78 | * `$fromId` <= `activity.from.id` 79 | * `$recipientId` <= `activity.recipient.id` 80 | * `$timestamp` <= `activity.timestamp.toISOString()` 81 | * `$start` (if this is the first activity in the conversation) 82 | 83 | > due to concurrency, multiple records belonging to a single conversation may be flagged as `start`, and they should be de-duped in the results by sorting on `timestamp`. 84 | 85 | Here are some example properties from a customEvent record: 86 | 87 | | Property | Value (string) | 88 | | -------- | -------------- | 89 | | id | `g17a2nle29eg` | 90 | | type | `conversationUpdate` | 91 | | timestamp | `2018-08-29T14:29:13.1450000Z` | 92 | | $conversationId | `06c8jb90efga9` | 93 | | _conversation | `{"id":"06c8jb90efga9"}` | 94 | | $recipientId | `default-bot` | 95 | | $timestamp | `2018-08-29T14:29:13.1450000Z` | 96 | | serviceUrl | `http://localhost:60086` | 97 | | _recipient | `{"id":"default-bot","name":"Bot"}` | 98 | | channelId | `emulator` | 99 | | $fromId | `default-user` | 100 | | $start | `true` | 101 | | _from | `{"id":"default-user","name":"User","role":"user"}` | 102 | | localTimestamp | `2018-08-29T14:29:13.0000000Z` | 103 | | _membersAdded | `[{"id":"default-bot","name":"Bot"}]` | 104 | 105 | [Sample queries](./src/index.ts#L38-L50) are available in this package's implementation. 106 | 107 | ## Using Activity Trace Properties in Analytics Queries 108 | 109 | Every custom event written to App Insights may supply supplementary properties in the form of key/value string pairs. These are also known as an event's _customDimensions_. 110 | 111 | `AppInsightsTranscriptStore` automatically promotes select Activity values as filterable Analytics properties (see [schema](#schema)). 112 | 113 | If you need to promote additional Activity properties as filterable Analytics properties, you can pass them along in the [constructor parameters](#appinsightstranscriptstore-class). The primary reason to do this is to capture `trace` activity content from utilities like [QnAMaker](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-ai/src/qnaMaker.ts#L231-L239), [LUIS](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-ai/src/luisRecognizer.ts#L213-L222), and [Feedback](../botbuilder-feedback#schema). 114 | 115 | Promoted property values are accessed using `lodash.get` and property names are serialized using `lodash.camelcase` (with a prefix of `$`). See the following code for an example 116 | 117 | > Trace activity properties are always _stored_ in full, but they are not _filterable_ unless so configured. 118 | 119 | ```JavaScript 120 | // qnamaker trace configuration 121 | // your app may need zero or more of these, depending on your analytics requirements 122 | const store = new AppInsightsTranscriptStore(client, { 123 | filterableActivityProperties: [ 124 | 'value.knowledgeBaseId', 125 | 'value.queryResults[0].questions[0]', 126 | 'value.queryResults[0].answer', 127 | 'value.queryResults[0].score', 128 | 'value.queryResults[0].source', 129 | ], 130 | }); 131 | ``` 132 | 133 | You can now write an Analytics query that targets these values in a filter: 134 | 135 | ``` 136 | customEvents 137 | | where customDimensions.type == 'trace' 138 | and customDimensions.$valueKnowledgeBaseId == 'kb123' 139 | and customDimensions.$valueQueryResults0Questions0 == 'foo' 140 | ``` -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-transcript-app-insights", 3 | "version": "4.2.4", 4 | "description": "Plugin to store Microsoft Bot Framework transcript activities in AppInsights", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "mocha --require ts-node/register \"test/**/*.{ts,tsx}\"", 10 | "pretest": "npm run build", 11 | "prepare": "npm run build", 12 | "prepublishOnly": "npm --no-git-tag-version version patch" 13 | }, 14 | "private": true, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Microsoft/botbuilder-utils-js.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/Microsoft/botbuilder-utils-js/issues" 21 | }, 22 | "keywords": [ 23 | "botbuilder", 24 | "botframework", 25 | "bots", 26 | "chatbots", 27 | "logging", 28 | "transcript", 29 | "appinsights" 30 | ], 31 | "author": "Microsoft Corp.", 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@types/chai": "^4.1.4", 35 | "@types/mocha": "^5.2.5", 36 | "@types/sinon": "^5.0.2", 37 | "@types/superagent": "^3.8.4", 38 | "applicationinsights": "^1.0.4", 39 | "botbuilder-core": "^4.0.6", 40 | "chai": "^4.1.2", 41 | "mocha": "^5.2.0", 42 | "sinon": "^6.3.3", 43 | "ts-node": "^7.0.1", 44 | "tslint": "^5.11.0", 45 | "typescript": "^2.9.2" 46 | }, 47 | "peerDependencies": { 48 | "applicationinsights": "^1.0.4", 49 | "botbuilder-core": "^4.0.6" 50 | }, 51 | "dependencies": { 52 | "@types/lodash.camelcase": "^4.3.4", 53 | "@types/lodash.get": "^4.4.4", 54 | "@types/lodash.has": "^4.5.4", 55 | "lodash.camelcase": "^4.3.0", 56 | "lodash.get": "^4.4.2", 57 | "lodash.has": "^4.5.2", 58 | "superagent": "^3.8.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/src/app-insights.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as request from 'superagent'; 5 | 6 | export interface EventQuery { 7 | $top?: number; 8 | $skip?: number; 9 | $count?: boolean; 10 | $filter?: string; 11 | $search?: string; 12 | $select?: string; 13 | $orderBy?: string; 14 | $apply?: string; 15 | } 16 | 17 | const URI = 'https://api.applicationinsights.io/v1'; 18 | 19 | interface QueryResponse { 20 | tables: AnalyticsTable[]; 21 | } 22 | 23 | interface AnalyticsTable { 24 | name: string; 25 | columns: AnalyticsColumn[]; 26 | rows: string[][]; 27 | } 28 | 29 | interface AnalyticsColumn { 30 | name: string; 31 | type: string; 32 | } 33 | 34 | const handleError = (err: any) => { 35 | if (err.response && err.response.body) { 36 | const body = err.response.body; 37 | const url = err.response.request.url; 38 | const method = err.response.request.method.toUpperCase(); 39 | const status = err.response.status; 40 | if (body.error) { 41 | throw new Error(`Cannot ${method} ${url} (${status}): ${JSON.stringify(body.error)}`); 42 | } 43 | } 44 | throw err; 45 | }; 46 | 47 | export class AppInsightsReadClient { 48 | constructor(private appId: string, private key: string) { } 49 | 50 | async customEvents(query: EventQuery) { 51 | return this.request('get', 'events/customEvents') 52 | .query(query) 53 | .catch(handleError); 54 | } 55 | 56 | async query(query: string): Promise { 57 | const resp = await this.request('post', 'query') 58 | .set('Content-Type', 'application/json; charset=utf-8') 59 | .send({ query }) 60 | .then((resp) => resp.body as QueryResponse) 61 | .catch(handleError); 62 | 63 | const table = resp.tables.find((x) => x.name === 'PrimaryResult'); 64 | if (!table) { 65 | throw new Error('Cannot find primary result in analytics query response'); 66 | } 67 | 68 | return table.rows.map((row) => { 69 | const item: { [key: string]: string } = { }; 70 | table.columns.forEach((col, i) => item[col.name] = row[i]); 71 | if (item.customDimensions) { 72 | item.customDimensions = JSON.parse(item.customDimensions); 73 | } 74 | return item as any as T; 75 | }); 76 | } 77 | 78 | private request(method: string, path: string) { 79 | if (!path.startsWith('/')) { 80 | path = '/' + path; 81 | } 82 | return request(method, `${URI}/apps/${this.appId}${path}`) 83 | .set('X-Api-Key', this.key); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { TelemetryClient } from 'applicationinsights'; 5 | import { Activity, PagedResult, TranscriptInfo, TranscriptStore } from 'botbuilder-core'; 6 | 7 | import { AppInsightsReadClient } from './app-insights'; 8 | import { deserialize, serialize, serializeMetadata, serializeProperties } from './serializer'; 9 | 10 | export interface AppInsightsQueryOptions { 11 | /** 12 | * API Access application id 13 | */ 14 | applicationId: string; 15 | 16 | /** 17 | * API Access key with 'Read telemetry' permissions 18 | */ 19 | readKey: string; 20 | } 21 | 22 | export interface AppInsightsTranscriptOptions { 23 | 24 | /** 25 | * Configure transcript store for reading (only if using `getTranscriptActivities` and `listTranscripts` functions) 26 | */ 27 | query?: AppInsightsQueryOptions; 28 | 29 | /** 30 | * Specify nested activity fields that should be exposed as queryable AppInsights properties 31 | * 32 | * @example 33 | * // Given a trace Activity like: 34 | * { "type": "trace", "value": { "nested": { "property": 123 } } } 35 | * 36 | * // And an `filterableActivityProperties` configuration of: 37 | * [ 'value.nested.property' ] 38 | * 39 | * // You can use the string property in an AppInsights analytics query as: 40 | * customEvents 41 | * | where customDimensions.$valueNestedProperty == '123' 42 | * 43 | * // Such activities are prefixed with `$` to differentiate them from other Activity properties, which are automatically stored 44 | * // Note that select Activity are automatically converted to properties: 45 | * ['conversation.id', 'from.id', 'recipient.id'] 46 | */ 47 | filterableActivityProperties?: string[]; 48 | } 49 | 50 | /** 51 | * Some clients (emulator) might send timestamp as a string 52 | * @param activity bot activity 53 | */ 54 | const getTimestamp = (activity: Activity): Date => { 55 | const ts: Date | string = activity.timestamp; 56 | if (typeof ts === 'string') { 57 | return new Date(ts); 58 | } else if (!ts) { 59 | return new Date(); 60 | } else { 61 | return ts; 62 | } 63 | }; 64 | 65 | const qTranscriptActivities = (channelId: string, conversationId: string, startDate: Date) => ` 66 | customEvents 67 | | where customDimensions.channelId == '${channelId}' 68 | and customDimensions.$conversationId == '${conversationId}' 69 | ${startDate ? `and customDimensions.$timestamp ge '${startDate.toISOString()}'` : ''}`; 70 | 71 | const qListTranscripts = (channelId: string) => ` 72 | customEvents 73 | | where customDimensions.channelId == '${channelId}' 74 | and customDimensions.$start == 'true' 75 | | project channelId=customDimensions.channelId 76 | , id=customDimensions.$conversationId 77 | , created=customDimensions.$timestamp`; 78 | 79 | export class AppInsightsTranscriptStore implements TranscriptStore { 80 | private transcriptIdCache = new Set(); 81 | private readClient: AppInsightsReadClient; 82 | 83 | /** 84 | * Create a new Application Insights transcript store for use in a Bot Framework bot 85 | * @param client Application Insights telemetry client 86 | * @param options Optional configuration parameters 87 | */ 88 | constructor(private client: TelemetryClient, private options?: AppInsightsTranscriptOptions) { 89 | if (this.options) { 90 | if (this.options.query) { 91 | this.readClient = new AppInsightsReadClient(this.options.query.applicationId, options.query.readKey); 92 | } 93 | } 94 | } 95 | 96 | logActivity(activity: Activity): Promise { 97 | const properties = serialize(activity); 98 | const transcriptId = activity.channelId + activity.conversation.id; 99 | const timestamp = getTimestamp(activity); 100 | 101 | // select non-string/deep properties are copied to top level so that they can be queried 102 | // these properties should be deleted from the returned object at query time 103 | serializeMetadata(properties, { 104 | conversationId: activity.conversation.id, 105 | fromId: activity.from.id, 106 | recipientId: activity.recipient.id, 107 | timestamp: timestamp.toISOString(), 108 | start: (!this.transcriptIdCache.has(transcriptId)).toString(), 109 | }); 110 | if (this.options && this.options.filterableActivityProperties) { 111 | serializeProperties(properties, this.options.filterableActivityProperties, activity); 112 | } 113 | 114 | this.transcriptIdCache.add(transcriptId); 115 | this.client.trackEvent({ name: 'activity', properties }); 116 | return Promise.resolve(); 117 | } 118 | 119 | async getTranscriptActivities(channelId: string, conversationId: string, continuationToken?: string, startDate?: Date): Promise> { 120 | this.throwIfNoReader(); 121 | const query = qTranscriptActivities(channelId, conversationId, startDate); 122 | const resp = await this.readClient.query(query); 123 | const activities = resp.map((x) => deserialize(x.customDimensions)); 124 | return { 125 | items: activities, 126 | continuationToken: undefined, 127 | }; 128 | } 129 | 130 | async listTranscripts(channelId: string, continuationToken?: string): Promise> { 131 | this.throwIfNoReader(); 132 | const query = qListTranscripts(channelId); 133 | const resp = await this.readClient.query(query); 134 | const transcripts = resp.map((x) => deserialize(x)); 135 | 136 | // due to concurrency, a transcript may have duplicate records 137 | // use a Map to limit each transcript to a single (earliest by date) output record 138 | const transcriptStarts = new Map(); 139 | for (const transcript of transcripts) { 140 | const key = transcript.channelId + transcript.id; 141 | 142 | // add the transcript to output 143 | if (!transcriptStarts.has(key) || transcript.created < transcriptStarts.get(key).created) { 144 | transcriptStarts.set(key, { 145 | channelId: transcript.channelId, 146 | id: transcript.id, 147 | created: transcript.created, 148 | }); 149 | } 150 | } 151 | 152 | return { 153 | items: Array.from(transcriptStarts.values()), 154 | continuationToken: undefined, 155 | }; 156 | } 157 | 158 | deleteTranscript(channelId: string, conversationId: string): Promise { 159 | // https://stackoverflow.com/questions/38219702/delete-specific-application-insights-events-on-azure 160 | return Promise.reject(new Error('AppInsights event deletion is not supported')); 161 | } 162 | 163 | private throwIfNoReader() { 164 | if (!this.readClient) { 165 | throw new Error('Please configure AppInsightsTranscriptStore readOptions'); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/src/serializer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import camelcase = require('lodash.camelcase'); 5 | import get = require('lodash.get'); 6 | import has = require('lodash.has'); 7 | 8 | const MAX_KEY_SIZE = 150; 9 | const MAX_VALUE_SIZE = 8192; 10 | const KEY_META_PREFIX = '$'; 11 | const KEY_STRING_PREFIX = '_'; 12 | const DATE_RE = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; 13 | 14 | export interface EventProperties { 15 | [key: string]: string; 16 | } 17 | 18 | /** 19 | * Serialize an object into key/value pairs. 20 | * Non-string values will be changed to use a prefix of '_' 21 | * Callers can add new properties to the returned object using keys prefixed with '$' to denote properties that will not be deserialized. 22 | * @param obj Any object 23 | */ 24 | export function serialize(obj: any): EventProperties { 25 | const event: EventProperties = {}; 26 | for (const prop of Object.getOwnPropertyNames(obj)) { 27 | if (prop.length > MAX_KEY_SIZE) { 28 | console.warn('Skipping transcript serialization of large key'); 29 | continue; 30 | } 31 | 32 | const value = obj[prop]; 33 | 34 | // string values should not be JSON stringified 35 | const isstr = typeof value === 'string'; 36 | const key = isstr ? prop : KEY_STRING_PREFIX + prop; 37 | const strval = isstr ? obj[prop] : JSON.stringify(obj[prop]); 38 | if (strval.length > MAX_VALUE_SIZE) { 39 | console.warn('Skipping transcript serialization of large value'); 40 | continue; 41 | } 42 | 43 | event[key] = strval; 44 | } 45 | return event; 46 | } 47 | 48 | export function serializeMetadata(event: EventProperties, properties: EventProperties): EventProperties { 49 | for (const key of Object.getOwnPropertyNames(properties)) { 50 | event[KEY_META_PREFIX + key] = properties[key]; 51 | } 52 | return properties; 53 | } 54 | 55 | export function serializeProperties(event: EventProperties, keys: string[], source: any) { 56 | for (const key of keys) { 57 | if (has(source, key)) { 58 | event[KEY_META_PREFIX + camelcase(key)] = get(source, key); 59 | } 60 | } 61 | } 62 | 63 | export function deserialize(event: EventProperties): T { 64 | const obj: any = {}; 65 | 66 | for (const prop of Object.getOwnPropertyNames(event)) { 67 | // keys with $ prefix should not be deserialized 68 | if (!prop.startsWith(KEY_META_PREFIX)) { 69 | 70 | // keys with _ prefix are already strings and should not be JSON-parsed 71 | const isstr = !prop.startsWith(KEY_STRING_PREFIX); 72 | const strval = event[prop]; 73 | const key = isstr ? prop : prop.substr(KEY_STRING_PREFIX.length); 74 | const value = isstr ? strval : JSON.parse(strval); 75 | obj[key] = DATE_RE.test(value) ? new Date(value) : value; 76 | } 77 | } 78 | 79 | return obj as T; 80 | } 81 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/test/app-insights-transcript-store-spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { expect } from 'chai'; 5 | import { stub } from 'sinon'; 6 | 7 | import { AppInsightsTranscriptStore } from '../src'; 8 | import { EventQuery } from '../src/app-insights'; 9 | import { 10 | CHANNEL_ID, CONVERSATION_ID, createActivity, 11 | createMockAppInsightsEventClient, createMockTelemetryClient, createStoredActivity, 12 | customEvents, MockEventClient, MockTelemetryClient, 13 | TIMESTAMP } from './mocks'; 14 | 15 | describe('AppInsights Transcript Store', () => { 16 | let client: MockTelemetryClient; 17 | // let readClient: MockEventClient; 18 | let store: AppInsightsTranscriptStore; 19 | 20 | beforeEach(() => { 21 | client = createMockTelemetryClient(); 22 | // readClient = createMockAppInsightsEventClient(); 23 | store = new AppInsightsTranscriptStore( 24 | client as any, 25 | { readKey: 'test', applicationId: 'test' }); 26 | }); 27 | 28 | describe('Logging', () => { 29 | it('should annotate only first activity', async () => { 30 | await store.logActivity(createActivity()); 31 | await store.logActivity(createActivity()); 32 | const [[telemetry1], [telemetry2]] = client.trackEvent.args; 33 | expect(telemetry1.properties.$start).to.equal('true'); 34 | expect(telemetry2.properties.$start).to.equal('false'); 35 | }); 36 | }); 37 | 38 | // describe('Transcript Retrieval', () => { 39 | 40 | // beforeEach(() => { 41 | // readClient.customEvents = stub().resolves(customEvents([createStoredActivity(true)])); 42 | // }); 43 | 44 | // it('should return transcript activities', async () => { 45 | // const retrieved = await store.getTranscriptActivities(CHANNEL_ID, CONVERSATION_ID); 46 | // expect(retrieved.items).to.deep.equal([createActivity()]); 47 | // expect(retrieved.continuationToken).to.be.undefined; 48 | // }); 49 | 50 | // describe('With StartDate in Request', () => { 51 | // const startDate = new Date(); 52 | // it('should return transcript activities', async () => { 53 | // const retrieved = await store.getTranscriptActivities(CHANNEL_ID, CONVERSATION_ID, null, startDate); 54 | // const [[query]] = readClient.customEvents.args as [[EventQuery]]; 55 | // expect(retrieved.items).to.deep.equal([createActivity()]); 56 | // expect(retrieved.continuationToken).to.be.undefined; 57 | // expect(query.$filter).to.include(`$timestamp ge '${startDate.toISOString()}'`); 58 | // }); 59 | // }); 60 | // }); 61 | 62 | // describe('Transcript Listing', () => { 63 | // const storedActivities = [ 64 | // createStoredActivity(true, CHANNEL_ID, '1'), 65 | // createStoredActivity(true, CHANNEL_ID, '2'), 66 | // ]; 67 | 68 | // beforeEach(() => { 69 | // readClient.customEvents = stub().resolves(customEvents(storedActivities)); 70 | // }); 71 | 72 | // it('should list transcripts', async () => { 73 | // const retrieved = await store.listTranscripts(CHANNEL_ID); 74 | // expect(retrieved.items).to.deep.equal([ 75 | // { channelId: CHANNEL_ID, id: '1', created: TIMESTAMP }, 76 | // { channelId: CHANNEL_ID, id: '2', created: TIMESTAMP }, 77 | // ]); 78 | // expect(retrieved.continuationToken).to.be.undefined; 79 | // }); 80 | 81 | // describe('With Extra Conversation Activity Record', () => { 82 | // beforeEach(() => { 83 | // const extra = createStoredActivity(true, CHANNEL_ID, '2'); 84 | // readClient.customEvents = stub().resolves(customEvents(storedActivities.concat(extra))); 85 | // }); 86 | 87 | // it('should list transcripts', async () => { 88 | // const retrieved = await store.listTranscripts(CHANNEL_ID); 89 | // expect(retrieved.items).to.deep.equal([ 90 | // { channelId: CHANNEL_ID, id: '1', created: TIMESTAMP }, 91 | // { channelId: CHANNEL_ID, id: '2', created: TIMESTAMP }, 92 | // ]); 93 | // expect(retrieved.continuationToken).to.be.undefined; 94 | // }); 95 | // }); 96 | // }); 97 | 98 | describe('Delete Transcript', () => { 99 | it('should throw error', async () => { 100 | try { 101 | await store.deleteTranscript(CHANNEL_ID, CONVERSATION_ID); 102 | expect.fail(); 103 | } catch (err) { 104 | expect(err).to.be.an('error'); 105 | } 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/test/mocks.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { Activity } from 'botframework-schema'; 5 | import { SinonStub, stub } from 'sinon'; 6 | 7 | export const CHANNEL_ID = 'foo'; 8 | export const CONVERSATION_ID = 'bar'; 9 | export const FROM_ID = 'from.id'; 10 | export const RECIPIENT_ID = 'recipient.id'; 11 | export const TIMESTAMP = new Date(); 12 | 13 | export interface MockTelemetryClient { 14 | trackEvent: SinonStub; 15 | } 16 | 17 | export interface MockEventClient { 18 | customEvents: SinonStub; 19 | } 20 | 21 | export function createMockTelemetryClient(): MockTelemetryClient { 22 | return { 23 | trackEvent: stub(), 24 | }; 25 | } 26 | 27 | export function createMockAppInsightsEventClient(): MockEventClient { 28 | return { 29 | customEvents: stub().resolves(customEvents([])), 30 | }; 31 | } 32 | 33 | export function customEvents(events: any[]) { 34 | return { 35 | values: events, 36 | count: events.length, 37 | }; 38 | } 39 | 40 | export const createActivity = (channelId = CHANNEL_ID, conversationId = CONVERSATION_ID) => ({ 41 | channelId, 42 | timestamp: TIMESTAMP, 43 | conversation: { id: conversationId }, 44 | from: { id: FROM_ID }, 45 | recipient: { id: RECIPIENT_ID }, 46 | }) as any as Activity; 47 | 48 | export const createStoredActivity = (start = false, channelId = CHANNEL_ID, conversationId = CONVERSATION_ID) => ({ 49 | $conversationId: conversationId, 50 | $fromId: FROM_ID, 51 | $recipientId: RECIPIENT_ID, 52 | $timestamp: TIMESTAMP.toISOString(), 53 | $start: start.toString(), 54 | channelId, 55 | _conversation: JSON.stringify({ id: conversationId }), 56 | _from: JSON.stringify({ id: FROM_ID }), 57 | _recipient: JSON.stringify({ id: RECIPIENT_ID }), 58 | _timestamp: JSON.stringify(TIMESTAMP), 59 | }); 60 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | } -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "no-unused-expression": false 5 | } 6 | } -------------------------------------------------------------------------------- /packages/botbuilder-transcript-app-insights/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "dist", 9 | "test" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/README.md: -------------------------------------------------------------------------------- 1 | # Cosmos DB Transcript Store for Microsoft Bot Framework 2 | 3 | This directory contains sample code that can be used to build a [TranscriptLogger](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/transcriptLogger.ts) that stores and queries bot transcripts backed by Cosmos DB SQL. 4 | 5 | ## Prerequisites 6 | 7 | - A [Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/introduction) service using the SQL API 8 | - A NodeJS bot using [Bot Framework v4](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0) 9 | 10 | ## Install 11 | 12 | Because this package is provided as sample code, it is not available on npm and it comes with no guarantee of support or updates. To use this software in your own app: 13 | 14 | 1. clone this repo 15 | 2. `cd botbuilder-utils-js/packages/botbuilder-transcript-cosmosdb` 16 | 3. `npm install` 17 | 4. `cd {your-app}` 18 | 5. `npm install file:path-to-botbuilder-utils-js/packages/botbuilder-transcript-cosmosdb` 19 | 6. `npm install documentdb` (if you don't already have it) 20 | 21 | > To support CI and other automation tasks, you may also choose to publish this package on a private npm repo, or simply copy the code/dependencies into your own app. 22 | 23 | ## Usage 24 | 25 | > JavaScript example is shown below, but this package also works great in TypeScript projects. 26 | 27 | ```JavaScript 28 | const { CosmosDbTranscriptStore } = require('botbuilder-transcript-cosmosdb'); 29 | const { BotFrameworkAdapter, TranscriptLoggerMiddleware } = require('botbuilder'); 30 | const { DocumentClient } = require('documentdb'); 31 | 32 | // Cosmos DB configuration 33 | const serviceEndpoint = ''; 34 | const masterKey = ''; 35 | const client = new DocumentClient(serviceEndpoint, { masterKey }); 36 | 37 | // Attach store to middleware and bot 38 | const store = new CosmosDbTranscriptStore(client); 39 | const logger = new TranscriptLoggerMiddleware(store); 40 | const adapter = new BotFrameworkAdapter({ 41 | appId: process.env.MICROSOFT_APP_ID, 42 | appPassword: process.env.MICROSOFT_APP_PASSWORD, 43 | }).use(logger); 44 | ``` 45 | 46 | Attaching the middleware to your bot adapter logs every incoming and outgoing Activity between the user and the bot to your Cosmos DB instance. The default database and collection names are `botframework` and `transcripts`, respectively. 47 | 48 | ## API 49 | 50 | ### CosmosDbTranscriptStore (class) 51 | 52 | ```TypeScript 53 | constructor(client: DocumentClient, options?: CosmosDbTranscriptStoreOptions) 54 | ``` 55 | 56 | * `client`: Provide your configured `DocumentClient` instance from the `documentdb` package. 57 | * `options.databaseName` (string): Database name (default: 'botframework'; created if it does not exist) 58 | * `options.collectionName` (string): Collection name (default: 'transcripts'; created if it does not exist) 59 | * `options.onCreateCollection.throughput` (number): Throughput for created collections (default: 1000) 60 | * `options.onCreateCollection.ttl` (number): Time-to-live for created collections (default: none) 61 | 62 | This class implements the [TranscriptStore](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/transcriptLogger.ts#L154-L183) interface, which includes functions to support retrieval of transcripts and activities. 63 | 64 | ## Schema 65 | 66 | > This section describes techniques for querying the data directly, instead of using the supported [TranscriptStore](https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder-core/src/transcriptLogger.ts#L154-L183) APIs. 67 | 68 | Before analyzing the stored transcript logs, it is important to understand the schema of the stored documents. Each document contains an `activity`, property, and an optional `start` property. Other properties like `id` and anything starting with `_` are [managed by Cosmos DB]((https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-resources#system-vs-user-defined-resources)). 69 | 70 | The `activity` property stores the full payload processed by the bot, and aheres to the [Bot Framework Activity Schema](https://github.com/Microsoft/BotBuilder/blob/hub/specs/transcript/transcript.md). 71 | 72 | The `start` property is boolean flag indicating whether the corresponding Activty was the first activity in its conversation (due to concurrency, multiple documents in a conversation may be flagged as the start, and they should be de-duped in the results by sorting on `activity.timestamp`). 73 | 74 | Here is an example document: 75 | 76 | ```JSON 77 | { 78 | "activity": { 79 | "type": "conversationUpdate", 80 | "membersAdded": [ 81 | { 82 | "id": "default-bot", 83 | "name": "Bot" 84 | } 85 | ], 86 | "id": "9b7jb8lf4258", 87 | "channelId": "emulator", 88 | "timestamp": "2018-09-05T15:35:14.627Z", 89 | "localTimestamp": "2018-09-05T11:35:14-04:00", 90 | "recipient": { 91 | "id": "default-bot", 92 | "name": "Bot" 93 | }, 94 | "conversation": { 95 | "id": "a2gigibf515i" 96 | }, 97 | "from": { 98 | "id": "default-user", 99 | "name": "User", 100 | "role": "user" 101 | }, 102 | "serviceUrl": "http://localhost:59426" 103 | }, 104 | "start": true, 105 | "id": "beca96ae-8101-6bd6-8d27-349a2844e581", 106 | "_rid": "VOgVAP9F8HMDAAAAAAAAAA==", 107 | "_self": "dbs/VOgVAA==/colls/VOgVAP9F8HM=/docs/VOgVAP9F8HMDAAAAAAAAAA==/", 108 | "_etag": "\"0000d868-0000-0000-0000-5b8ff7b30000\"", 109 | "_attachments": "attachments/", 110 | "_ts": 1536161715 111 | } 112 | ``` 113 | 114 | [Sample queries](./src/queries.ts) are available in this package's implementation. 115 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-transcript-cosmosdb", 3 | "version": "4.2.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@sinonjs/commons": { 8 | "version": "1.0.2", 9 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/@sinonjs/commons/-/commons-1.0.2.tgz", 10 | "integrity": "sha512-WR3dlgqJP4QNrLC4iXN/5/2WaLQQ0VijOOkmflqFGVJ6wLEpbSjo7c0ZeGIdtY8Crk7xBBp87sM6+Mkerz7alw==", 11 | "dev": true, 12 | "requires": { 13 | "type-detect": "4.0.8" 14 | } 15 | }, 16 | "@sinonjs/formatio": { 17 | "version": "3.0.0", 18 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/@sinonjs/formatio/-/formatio-3.0.0.tgz", 19 | "integrity": "sha512-vdjoYLDptCgvtJs57ULshak3iJe4NW3sJ3g36xVDGff5AE8P30S6A093EIEPjdi2noGhfuNOEkbxt3J3awFW1w==", 20 | "dev": true, 21 | "requires": { 22 | "@sinonjs/samsam": "2.1.0" 23 | }, 24 | "dependencies": { 25 | "@sinonjs/samsam": { 26 | "version": "2.1.0", 27 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/@sinonjs/samsam/-/samsam-2.1.0.tgz", 28 | "integrity": "sha512-5x2kFgJYupaF1ns/RmharQ90lQkd2ELS8A9X0ymkAAdemYHGtI2KiUHG8nX2WU0T1qgnOU5YMqnBM2V7NUanNw==", 29 | "dev": true, 30 | "requires": { 31 | "array-from": "2.1.1" 32 | } 33 | } 34 | } 35 | }, 36 | "@sinonjs/samsam": { 37 | "version": "2.1.1", 38 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/@sinonjs/samsam/-/samsam-2.1.1.tgz", 39 | "integrity": "sha512-7oX6PXMulvdN37h88dvlvRyu61GYZau40fL4wEZvPEHvrjpJc3lDv6xDM5n4Z0StufUVB5nDvVZUM+jZHdMOOQ==", 40 | "dev": true, 41 | "requires": { 42 | "array-from": "2.1.1" 43 | } 44 | }, 45 | "@types/chai": { 46 | "version": "4.1.4", 47 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.4.tgz", 48 | "integrity": "sha512-h6+VEw2Vr3ORiFCyyJmcho2zALnUq9cvdB/IO8Xs9itrJVCenC7o26A6+m7D0ihTTr65eS259H5/Ghl/VjYs6g==", 49 | "dev": true 50 | }, 51 | "@types/documentdb": { 52 | "version": "1.10.5", 53 | "resolved": "https://registry.npmjs.org/@types/documentdb/-/documentdb-1.10.5.tgz", 54 | "integrity": "sha512-FHQV9Nc1ffrLkQxO0zFlDCRPyHZtuKmAAuJIi278COhtkKBuBRuKOzoO3JlT0yfUrivPjAzNae+gh9fS++r0Ag==", 55 | "dev": true, 56 | "requires": { 57 | "@types/node": "9.6.23" 58 | } 59 | }, 60 | "@types/mocha": { 61 | "version": "5.2.5", 62 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", 63 | "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==", 64 | "dev": true 65 | }, 66 | "@types/node": { 67 | "version": "9.6.23", 68 | "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.23.tgz", 69 | "integrity": "sha512-d2SJJpwkiPudEQ3+9ysANN2Nvz4QJKUPoe/WL5zyQzI0RaEeZWH5K5xjvUIGszTItHQpFPdH+u51f6G/LkS8Cg==", 70 | "dev": true 71 | }, 72 | "@types/sinon": { 73 | "version": "5.0.2", 74 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/@types/sinon/-/sinon-5.0.2.tgz", 75 | "integrity": "sha512-ifYuFq3GWyvRbqebGB4ZKLqezMGLXzhHv1Uefhg+uARYs/iO+v6Gu/BkpxTxsyM9NI++N/RCf5sWl3X9wBVLaw==", 76 | "dev": true 77 | }, 78 | "ansi-regex": { 79 | "version": "2.1.1", 80 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 81 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 82 | "dev": true 83 | }, 84 | "ansi-styles": { 85 | "version": "2.2.1", 86 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 87 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", 88 | "dev": true 89 | }, 90 | "argparse": { 91 | "version": "1.0.10", 92 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 93 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 94 | "dev": true, 95 | "requires": { 96 | "sprintf-js": "1.0.3" 97 | } 98 | }, 99 | "array-from": { 100 | "version": "2.1.1", 101 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/array-from/-/array-from-2.1.1.tgz", 102 | "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", 103 | "dev": true 104 | }, 105 | "arrify": { 106 | "version": "1.0.1", 107 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 108 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 109 | "dev": true 110 | }, 111 | "assert": { 112 | "version": "1.4.1", 113 | "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", 114 | "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", 115 | "dev": true, 116 | "requires": { 117 | "util": "0.10.3" 118 | } 119 | }, 120 | "assertion-error": { 121 | "version": "1.1.0", 122 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 123 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 124 | "dev": true 125 | }, 126 | "babel-code-frame": { 127 | "version": "6.26.0", 128 | "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", 129 | "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", 130 | "dev": true, 131 | "requires": { 132 | "chalk": "1.1.3", 133 | "esutils": "2.0.2", 134 | "js-tokens": "3.0.2" 135 | }, 136 | "dependencies": { 137 | "chalk": { 138 | "version": "1.1.3", 139 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 140 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 141 | "dev": true, 142 | "requires": { 143 | "ansi-styles": "2.2.1", 144 | "escape-string-regexp": "1.0.5", 145 | "has-ansi": "2.0.0", 146 | "strip-ansi": "3.0.1", 147 | "supports-color": "2.0.0" 148 | } 149 | } 150 | } 151 | }, 152 | "balanced-match": { 153 | "version": "1.0.0", 154 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 155 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 156 | "dev": true 157 | }, 158 | "big-integer": { 159 | "version": "1.6.32", 160 | "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.32.tgz", 161 | "integrity": "sha512-ljKJdR3wk9thHfLj4DtrNiOSTxvGFaMjWrG4pW75juXC4j7+XuKJVFdg4kgFMYp85PVkO05dFMj2dk2xVsH4xw==", 162 | "dev": true 163 | }, 164 | "binary-search-bounds": { 165 | "version": "2.0.3", 166 | "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz", 167 | "integrity": "sha1-X/hhbW3SylOIvIWy1iZuK52lAtw=", 168 | "dev": true 169 | }, 170 | "botbuilder-core": { 171 | "version": "4.0.6", 172 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/botbuilder-core/-/botbuilder-core-4.0.6.tgz", 173 | "integrity": "sha1-sDiiKJ8E/+8pu6YVoHd1e1Q8WZY=", 174 | "dev": true, 175 | "requires": { 176 | "assert": "1.4.1", 177 | "botframework-schema": "4.0.6" 178 | } 179 | }, 180 | "botframework-schema": { 181 | "version": "4.0.6", 182 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/botframework-schema/-/botframework-schema-4.0.6.tgz", 183 | "integrity": "sha1-SD8JoSqnNjheHmdG9cBl2yrhTEI=", 184 | "dev": true, 185 | "requires": { 186 | "@types/node": "9.6.23" 187 | } 188 | }, 189 | "brace-expansion": { 190 | "version": "1.1.11", 191 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 192 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 193 | "dev": true, 194 | "requires": { 195 | "balanced-match": "1.0.0", 196 | "concat-map": "0.0.1" 197 | } 198 | }, 199 | "browser-stdout": { 200 | "version": "1.3.1", 201 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 202 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 203 | "dev": true 204 | }, 205 | "buffer-from": { 206 | "version": "1.1.1", 207 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/buffer-from/-/buffer-from-1.1.1.tgz", 208 | "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=", 209 | "dev": true 210 | }, 211 | "builtin-modules": { 212 | "version": "1.1.1", 213 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 214 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 215 | "dev": true 216 | }, 217 | "chai": { 218 | "version": "4.1.2", 219 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 220 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 221 | "dev": true, 222 | "requires": { 223 | "assertion-error": "1.1.0", 224 | "check-error": "1.0.2", 225 | "deep-eql": "3.0.1", 226 | "get-func-name": "2.0.0", 227 | "pathval": "1.1.0", 228 | "type-detect": "4.0.8" 229 | } 230 | }, 231 | "chalk": { 232 | "version": "2.4.1", 233 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 234 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 235 | "dev": true, 236 | "requires": { 237 | "ansi-styles": "3.2.1", 238 | "escape-string-regexp": "1.0.5", 239 | "supports-color": "5.4.0" 240 | }, 241 | "dependencies": { 242 | "ansi-styles": { 243 | "version": "3.2.1", 244 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 245 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 246 | "dev": true, 247 | "requires": { 248 | "color-convert": "1.9.2" 249 | } 250 | }, 251 | "supports-color": { 252 | "version": "5.4.0", 253 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 254 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 255 | "dev": true, 256 | "requires": { 257 | "has-flag": "3.0.0" 258 | } 259 | } 260 | } 261 | }, 262 | "charenc": { 263 | "version": "0.0.2", 264 | "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", 265 | "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", 266 | "dev": true 267 | }, 268 | "check-error": { 269 | "version": "1.0.2", 270 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 271 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 272 | "dev": true 273 | }, 274 | "color-convert": { 275 | "version": "1.9.2", 276 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", 277 | "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", 278 | "dev": true, 279 | "requires": { 280 | "color-name": "1.1.1" 281 | } 282 | }, 283 | "color-name": { 284 | "version": "1.1.1", 285 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", 286 | "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", 287 | "dev": true 288 | }, 289 | "commander": { 290 | "version": "2.16.0", 291 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", 292 | "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", 293 | "dev": true 294 | }, 295 | "concat-map": { 296 | "version": "0.0.1", 297 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 298 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 299 | "dev": true 300 | }, 301 | "crypt": { 302 | "version": "0.0.2", 303 | "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", 304 | "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", 305 | "dev": true 306 | }, 307 | "debug": { 308 | "version": "3.1.0", 309 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 310 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 311 | "dev": true, 312 | "requires": { 313 | "ms": "2.0.0" 314 | } 315 | }, 316 | "deep-eql": { 317 | "version": "3.0.1", 318 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 319 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 320 | "dev": true, 321 | "requires": { 322 | "type-detect": "4.0.8" 323 | } 324 | }, 325 | "diff": { 326 | "version": "3.5.0", 327 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 328 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 329 | "dev": true 330 | }, 331 | "documentdb": { 332 | "version": "1.14.5", 333 | "resolved": "https://registry.npmjs.org/documentdb/-/documentdb-1.14.5.tgz", 334 | "integrity": "sha512-0nDoQQiq5jzGIxOQF2y2bUOrFYehvk9pIrXy0dscXc3JsepNYhNVmjIsug5sgYPbt+XUYtMXpsfjzGCnYgNXgw==", 335 | "dev": true, 336 | "requires": { 337 | "big-integer": "1.6.32", 338 | "binary-search-bounds": "2.0.3", 339 | "int64-buffer": "0.1.10", 340 | "priorityqueuejs": "1.0.0", 341 | "semaphore": "1.0.5", 342 | "tunnel": "0.0.5", 343 | "underscore": "1.8.3" 344 | } 345 | }, 346 | "escape-string-regexp": { 347 | "version": "1.0.5", 348 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 349 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 350 | "dev": true 351 | }, 352 | "esprima": { 353 | "version": "4.0.1", 354 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 355 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 356 | "dev": true 357 | }, 358 | "esutils": { 359 | "version": "2.0.2", 360 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 361 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 362 | "dev": true 363 | }, 364 | "fs.realpath": { 365 | "version": "1.0.0", 366 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 367 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 368 | "dev": true 369 | }, 370 | "get-func-name": { 371 | "version": "2.0.0", 372 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 373 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 374 | "dev": true 375 | }, 376 | "glob": { 377 | "version": "7.1.2", 378 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 379 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 380 | "dev": true, 381 | "requires": { 382 | "fs.realpath": "1.0.0", 383 | "inflight": "1.0.6", 384 | "inherits": "2.0.3", 385 | "minimatch": "3.0.4", 386 | "once": "1.4.0", 387 | "path-is-absolute": "1.0.1" 388 | } 389 | }, 390 | "growl": { 391 | "version": "1.10.5", 392 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 393 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 394 | "dev": true 395 | }, 396 | "has-ansi": { 397 | "version": "2.0.0", 398 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 399 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 400 | "dev": true, 401 | "requires": { 402 | "ansi-regex": "2.1.1" 403 | } 404 | }, 405 | "has-flag": { 406 | "version": "3.0.0", 407 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 408 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 409 | "dev": true 410 | }, 411 | "he": { 412 | "version": "1.1.1", 413 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 414 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 415 | "dev": true 416 | }, 417 | "inflight": { 418 | "version": "1.0.6", 419 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 420 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 421 | "dev": true, 422 | "requires": { 423 | "once": "1.4.0", 424 | "wrappy": "1.0.2" 425 | } 426 | }, 427 | "inherits": { 428 | "version": "2.0.3", 429 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 430 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 431 | "dev": true 432 | }, 433 | "int64-buffer": { 434 | "version": "0.1.10", 435 | "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", 436 | "integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=", 437 | "dev": true 438 | }, 439 | "is-buffer": { 440 | "version": "1.1.6", 441 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/is-buffer/-/is-buffer-1.1.6.tgz", 442 | "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=", 443 | "dev": true 444 | }, 445 | "isarray": { 446 | "version": "0.0.1", 447 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 448 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", 449 | "dev": true 450 | }, 451 | "js-tokens": { 452 | "version": "3.0.2", 453 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 454 | "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", 455 | "dev": true 456 | }, 457 | "js-yaml": { 458 | "version": "3.12.0", 459 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", 460 | "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", 461 | "dev": true, 462 | "requires": { 463 | "argparse": "1.0.10", 464 | "esprima": "4.0.1" 465 | } 466 | }, 467 | "just-extend": { 468 | "version": "3.0.0", 469 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/just-extend/-/just-extend-3.0.0.tgz", 470 | "integrity": "sha512-Fu3T6pKBuxjWT/p4DkqGHFRsysc8OauWr4ZRTY9dIx07Y9O0RkoR5jcv28aeD1vuAwhm3nLkDurwLXoALp4DpQ==", 471 | "dev": true 472 | }, 473 | "lodash.get": { 474 | "version": "4.4.2", 475 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 476 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", 477 | "dev": true 478 | }, 479 | "lolex": { 480 | "version": "2.7.4", 481 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/lolex/-/lolex-2.7.4.tgz", 482 | "integrity": "sha512-Gh6Vffq/piTeHwunLNFR1jFVaqlwK9GMNUxFcsO1cwHyvbRKHwX8UDkxmrDnbcPdHNmpv7z2kxtkkSx5xkNpMw==", 483 | "dev": true 484 | }, 485 | "make-error": { 486 | "version": "1.3.5", 487 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/make-error/-/make-error-1.3.5.tgz", 488 | "integrity": "sha1-7+ToH22yjK3WBccPKcgxtY73dsg=", 489 | "dev": true 490 | }, 491 | "md5": { 492 | "version": "2.2.1", 493 | "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", 494 | "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", 495 | "dev": true, 496 | "requires": { 497 | "charenc": "0.0.2", 498 | "crypt": "0.0.2", 499 | "is-buffer": "1.1.6" 500 | } 501 | }, 502 | "minimatch": { 503 | "version": "3.0.4", 504 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 505 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 506 | "dev": true, 507 | "requires": { 508 | "brace-expansion": "1.1.11" 509 | } 510 | }, 511 | "minimist": { 512 | "version": "0.0.8", 513 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 514 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 515 | "dev": true 516 | }, 517 | "mkdirp": { 518 | "version": "0.5.1", 519 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 520 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 521 | "dev": true, 522 | "requires": { 523 | "minimist": "0.0.8" 524 | } 525 | }, 526 | "mocha": { 527 | "version": "5.2.0", 528 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 529 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 530 | "dev": true, 531 | "requires": { 532 | "browser-stdout": "1.3.1", 533 | "commander": "2.15.1", 534 | "debug": "3.1.0", 535 | "diff": "3.5.0", 536 | "escape-string-regexp": "1.0.5", 537 | "glob": "7.1.2", 538 | "growl": "1.10.5", 539 | "he": "1.1.1", 540 | "minimatch": "3.0.4", 541 | "mkdirp": "0.5.1", 542 | "supports-color": "5.4.0" 543 | }, 544 | "dependencies": { 545 | "commander": { 546 | "version": "2.15.1", 547 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 548 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 549 | "dev": true 550 | }, 551 | "supports-color": { 552 | "version": "5.4.0", 553 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 554 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 555 | "dev": true, 556 | "requires": { 557 | "has-flag": "3.0.0" 558 | } 559 | } 560 | } 561 | }, 562 | "mocha-junit-reporter": { 563 | "version": "1.18.0", 564 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/mocha-junit-reporter/-/mocha-junit-reporter-1.18.0.tgz", 565 | "integrity": "sha512-y3XuqKa2+HRYtg0wYyhW/XsLm2Ps+pqf9HaTAt7+MVUAKFJaNAHOrNseTZo9KCxjfIbxUWwckP5qCDDPUmjSWA==", 566 | "dev": true, 567 | "requires": { 568 | "debug": "2.6.9", 569 | "md5": "2.2.1", 570 | "mkdirp": "0.5.1", 571 | "strip-ansi": "4.0.0", 572 | "xml": "1.0.1" 573 | }, 574 | "dependencies": { 575 | "ansi-regex": { 576 | "version": "3.0.0", 577 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 578 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 579 | "dev": true 580 | }, 581 | "debug": { 582 | "version": "2.6.9", 583 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/debug/-/debug-2.6.9.tgz", 584 | "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", 585 | "dev": true, 586 | "requires": { 587 | "ms": "2.0.0" 588 | } 589 | }, 590 | "strip-ansi": { 591 | "version": "4.0.0", 592 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 593 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 594 | "dev": true, 595 | "requires": { 596 | "ansi-regex": "3.0.0" 597 | } 598 | } 599 | } 600 | }, 601 | "ms": { 602 | "version": "2.0.0", 603 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 604 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 605 | "dev": true 606 | }, 607 | "nise": { 608 | "version": "1.4.5", 609 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/nise/-/nise-1.4.5.tgz", 610 | "integrity": "sha512-OHRVvdxKgwZELf2DTgsJEIA4MOq8XWvpSUzoOXyxJ2mY0mMENWC66+70AShLR2z05B1dzrzWlUQJmJERlOUpZw==", 611 | "dev": true, 612 | "requires": { 613 | "@sinonjs/formatio": "3.0.0", 614 | "just-extend": "3.0.0", 615 | "lolex": "2.7.4", 616 | "path-to-regexp": "1.7.0", 617 | "text-encoding": "0.6.4" 618 | } 619 | }, 620 | "once": { 621 | "version": "1.4.0", 622 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 623 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 624 | "dev": true, 625 | "requires": { 626 | "wrappy": "1.0.2" 627 | } 628 | }, 629 | "path-is-absolute": { 630 | "version": "1.0.1", 631 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 632 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 633 | "dev": true 634 | }, 635 | "path-parse": { 636 | "version": "1.0.5", 637 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 638 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 639 | "dev": true 640 | }, 641 | "path-to-regexp": { 642 | "version": "1.7.0", 643 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", 644 | "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", 645 | "dev": true, 646 | "requires": { 647 | "isarray": "0.0.1" 648 | } 649 | }, 650 | "pathval": { 651 | "version": "1.1.0", 652 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 653 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 654 | "dev": true 655 | }, 656 | "priorityqueuejs": { 657 | "version": "1.0.0", 658 | "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", 659 | "integrity": "sha1-LuTyPCVgkT4IwHzlzN1t498sWvg=", 660 | "dev": true 661 | }, 662 | "resolve": { 663 | "version": "1.8.1", 664 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", 665 | "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", 666 | "dev": true, 667 | "requires": { 668 | "path-parse": "1.0.5" 669 | } 670 | }, 671 | "semaphore": { 672 | "version": "1.0.5", 673 | "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.0.5.tgz", 674 | "integrity": "sha1-tJJXbmavGT25XWXiXsU/Xxl5jWA=", 675 | "dev": true 676 | }, 677 | "semver": { 678 | "version": "5.5.0", 679 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", 680 | "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", 681 | "dev": true 682 | }, 683 | "sinon": { 684 | "version": "6.3.3", 685 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/sinon/-/sinon-6.3.3.tgz", 686 | "integrity": "sha512-LTZ3vnkscWQHyRI5mN7NrCVC9V01wgl3XWCspFqLKJ8yKhrkj8iOfvQLjdrYqcGoo+Q+sCMOMSBMlcUwua4pbQ==", 687 | "dev": true, 688 | "requires": { 689 | "@sinonjs/commons": "1.0.2", 690 | "@sinonjs/formatio": "3.0.0", 691 | "@sinonjs/samsam": "2.1.1", 692 | "diff": "3.5.0", 693 | "lodash.get": "4.4.2", 694 | "lolex": "2.7.4", 695 | "nise": "1.4.5", 696 | "supports-color": "5.5.0", 697 | "type-detect": "4.0.8" 698 | }, 699 | "dependencies": { 700 | "supports-color": { 701 | "version": "5.5.0", 702 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/supports-color/-/supports-color-5.5.0.tgz", 703 | "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", 704 | "dev": true, 705 | "requires": { 706 | "has-flag": "3.0.0" 707 | } 708 | } 709 | } 710 | }, 711 | "source-map": { 712 | "version": "0.6.1", 713 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/source-map/-/source-map-0.6.1.tgz", 714 | "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", 715 | "dev": true 716 | }, 717 | "source-map-support": { 718 | "version": "0.5.9", 719 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/source-map-support/-/source-map-support-0.5.9.tgz", 720 | "integrity": "sha1-QbyVOyU0Jn6i1gW8z6e/oxEc7V8=", 721 | "dev": true, 722 | "requires": { 723 | "buffer-from": "1.1.1", 724 | "source-map": "0.6.1" 725 | } 726 | }, 727 | "sprintf-js": { 728 | "version": "1.0.3", 729 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 730 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 731 | "dev": true 732 | }, 733 | "strip-ansi": { 734 | "version": "3.0.1", 735 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 736 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 737 | "dev": true, 738 | "requires": { 739 | "ansi-regex": "2.1.1" 740 | } 741 | }, 742 | "supports-color": { 743 | "version": "2.0.0", 744 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 745 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", 746 | "dev": true 747 | }, 748 | "text-encoding": { 749 | "version": "0.6.4", 750 | "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", 751 | "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", 752 | "dev": true 753 | }, 754 | "ts-node": { 755 | "version": "7.0.1", 756 | "resolved": "https://msdata.pkgs.visualstudio.com/_packaging/botbuilder-utils/npm/registry/ts-node/-/ts-node-7.0.1.tgz", 757 | "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", 758 | "dev": true, 759 | "requires": { 760 | "arrify": "1.0.1", 761 | "buffer-from": "1.1.1", 762 | "diff": "3.5.0", 763 | "make-error": "1.3.5", 764 | "minimist": "1.2.0", 765 | "mkdirp": "0.5.1", 766 | "source-map-support": "0.5.9", 767 | "yn": "2.0.0" 768 | }, 769 | "dependencies": { 770 | "minimist": { 771 | "version": "1.2.0", 772 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 773 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 774 | "dev": true 775 | } 776 | } 777 | }, 778 | "tslib": { 779 | "version": "1.9.3", 780 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 781 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", 782 | "dev": true 783 | }, 784 | "tslint": { 785 | "version": "5.11.0", 786 | "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", 787 | "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", 788 | "dev": true, 789 | "requires": { 790 | "babel-code-frame": "6.26.0", 791 | "builtin-modules": "1.1.1", 792 | "chalk": "2.4.1", 793 | "commander": "2.16.0", 794 | "diff": "3.5.0", 795 | "glob": "7.1.2", 796 | "js-yaml": "3.12.0", 797 | "minimatch": "3.0.4", 798 | "resolve": "1.8.1", 799 | "semver": "5.5.0", 800 | "tslib": "1.9.3", 801 | "tsutils": "2.28.0" 802 | } 803 | }, 804 | "tsutils": { 805 | "version": "2.28.0", 806 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", 807 | "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", 808 | "dev": true, 809 | "requires": { 810 | "tslib": "1.9.3" 811 | } 812 | }, 813 | "tunnel": { 814 | "version": "0.0.5", 815 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz", 816 | "integrity": "sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA==", 817 | "dev": true 818 | }, 819 | "type-detect": { 820 | "version": "4.0.8", 821 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 822 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 823 | "dev": true 824 | }, 825 | "typescript": { 826 | "version": "2.9.2", 827 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", 828 | "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", 829 | "dev": true 830 | }, 831 | "underscore": { 832 | "version": "1.8.3", 833 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", 834 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", 835 | "dev": true 836 | }, 837 | "util": { 838 | "version": "0.10.3", 839 | "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", 840 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", 841 | "dev": true, 842 | "requires": { 843 | "inherits": "2.0.1" 844 | }, 845 | "dependencies": { 846 | "inherits": { 847 | "version": "2.0.1", 848 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", 849 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 850 | "dev": true 851 | } 852 | } 853 | }, 854 | "wrappy": { 855 | "version": "1.0.2", 856 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 857 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 858 | "dev": true 859 | }, 860 | "xml": { 861 | "version": "1.0.1", 862 | "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", 863 | "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", 864 | "dev": true 865 | }, 866 | "yn": { 867 | "version": "2.0.0", 868 | "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", 869 | "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", 870 | "dev": true 871 | } 872 | } 873 | } 874 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-transcript-cosmosdb", 3 | "version": "4.2.3", 4 | "description": "Plugin to store Microsoft Bot Framework transcript activities on Azure CosmosDb", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "mocha --require ts-node/register \"test/**/*.{ts,tsx}\"", 10 | "pretest": "npm run build", 11 | "prepare": "npm run build", 12 | "prepublishOnly": "npm --no-git-tag-version version patch" 13 | }, 14 | "private": true, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Microsoft/botbuilder-utils-js.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/Microsoft/botbuilder-utils-js/issues" 21 | }, 22 | "keywords": [ 23 | "botbuilder", 24 | "botframework", 25 | "bots", 26 | "chatbots", 27 | "logging", 28 | "transcript", 29 | "documentdb", 30 | "cosmosdb", 31 | "nosql" 32 | ], 33 | "author": "Microsoft Corp.", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "@types/chai": "^4.1.4", 37 | "@types/documentdb": "^1.10.5", 38 | "@types/mocha": "^5.2.5", 39 | "@types/sinon": "^5.0.2", 40 | "botbuilder-core": "^4.0.6", 41 | "chai": "^4.1.2", 42 | "documentdb": "^1.14.5", 43 | "mocha": "^5.2.0", 44 | "mocha-junit-reporter": "^1.18.0", 45 | "sinon": "^6.3.3", 46 | "ts-node": "^7.0.1", 47 | "tslint": "^5.11.0", 48 | "typescript": "^2.9.2" 49 | }, 50 | "peerDependencies": { 51 | "botbuilder-core": "^4.0.6", 52 | "documentdb": "^1.14.5" 53 | }, 54 | "dependencies": {} 55 | } 56 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/src/cosmosdb.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { Collection, DocumentClient, DocumentQuery, FeedOptions, NewDocument, QueryError, RequestOptions, RetrievedDocument } from 'documentdb'; 5 | 6 | const HTTP_CONFLICT = 409; 7 | 8 | /** 9 | * Create a DocumentDb database if it does not exist 10 | * @param client DocumentDb client 11 | * @param id Database name 12 | */ 13 | export function createDatabaseIfNotExists(client: DocumentClient, id: string) { 14 | return new Promise((resolve, reject) => { 15 | client.createDatabase({ id }, (err) => created(err, resolve, reject)); 16 | }); 17 | } 18 | 19 | /** 20 | * Create a DocumentDb collection if it does not exist 21 | * @param client DocumentDb client 22 | * @param db The self-link of the database 23 | * @param coll Definition of the collection to create if it does not already exist 24 | * @param options Request options that will be used to create the collection 25 | */ 26 | export function createCollectionIfNotExists(client: DocumentClient, db: string, coll: Collection, options?: RequestOptions ) { 27 | return new Promise((resolve, reject) => { 28 | client.createCollection(db, coll, options, (err) => created(err, resolve, reject)); 29 | }); 30 | } 31 | 32 | /** 33 | * Create a DocumentDb document 34 | * @param client DocumentDb client 35 | * @param link Document feed link 36 | * @param doc Document content 37 | */ 38 | export function createDocument(client: DocumentClient, link: string, doc: NewDocument) { 39 | return new Promise((resolve, reject) => { 40 | client.createDocument(link, doc, (err) => created(err, resolve, reject)); 41 | }); 42 | } 43 | 44 | /** 45 | * Fetch next batch of documents for a DocumentDb query 46 | * @param client DocumentDb client 47 | * @param coll Collection link 48 | * @param query Document query 49 | * @param options Query Options 50 | */ 51 | export function queryDocuments(client: DocumentClient, coll: string, query: DocumentQuery, options?: FeedOptions) { 52 | return new Promise<[Array, any]>((resolve, reject) => { 53 | client.queryDocuments(coll, query, options).executeNext((err, resource, headers) => { 54 | if (err) { 55 | reject(err); 56 | } else { 57 | resolve([resource as any[], headers]); 58 | } 59 | }); 60 | }); 61 | } 62 | 63 | /** 64 | * Delete a document from a DocumentDb collection 65 | * @param client DocumentDb client 66 | * @param docLink Document link 67 | */ 68 | export function deleteDocument(client: DocumentClient, docLink: string) { 69 | return new Promise((resolve, reject) => { 70 | client.deleteDocument(docLink, (err) => { 71 | if (err) { 72 | reject(err); 73 | } else { 74 | resolve(); 75 | } 76 | }); 77 | }); 78 | } 79 | 80 | function created(err: QueryError, resolve: () => void, reject: (reason?: any) => void): void { 81 | if (err && err.code !== HTTP_CONFLICT) { 82 | reject(new Error(err.body)); 83 | } else { 84 | resolve(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { Activity, PagedResult, TranscriptInfo, TranscriptStore } from 'botbuilder-core'; 5 | import { Collection, DocumentClient } from 'documentdb'; 6 | 7 | import { createCollectionIfNotExists, createDatabaseIfNotExists, createDocument, deleteDocument, queryDocuments } from './cosmosdb'; 8 | import { Initializer } from './initializer'; 9 | import { FIND_CONVERSATION_START, LIST_TRANSCRIPT_ACTIVITIES, LIST_TRANSCRIPTS } from './queries'; 10 | 11 | const DEFAULT_DB_NAME = 'botframework'; 12 | const DEFAULT_COLL_NAME = 'transcripts'; 13 | const DEFAULT_COLL_THROUGHPUT = 1000; 14 | const DEFAULT_COLL_TTL = 0; 15 | 16 | const DEFAULT_COLL: any = { 17 | id: null, 18 | indexingPolicy: { 19 | automatic: true, 20 | indexingMode: 'consistent', 21 | includedPaths: [ 22 | { 23 | path: '/*', 24 | indexes: [ 25 | { 26 | kind: 'Range', 27 | dataType: 'Number', 28 | precision: -1, 29 | }, 30 | { 31 | kind: 'Range', 32 | dataType: 'String', 33 | precision: -1, 34 | }, 35 | { 36 | kind: 'Spatial', 37 | dataType: 'Point', 38 | }, 39 | ], 40 | }, 41 | indexForDate('/activity/timestamp/?'), 42 | indexForDate('/activity/localTimestamp/?'), 43 | ], 44 | excludedPaths: [ 45 | { 46 | path: '/attachments/[]/content/*', 47 | }, 48 | ], 49 | }, 50 | }; 51 | 52 | function indexForDate(path: string): any { 53 | return { 54 | path, 55 | indexes: [ 56 | { 57 | kind: 'Range', 58 | dataType: 'Number', 59 | precision: -1, 60 | }, 61 | { 62 | kind: 'Range', 63 | dataType: 'String', 64 | precision: -1, 65 | }, 66 | ], 67 | }; 68 | } 69 | 70 | export interface CosmosDbTranscriptStoreOptions { 71 | /** Database name (default = 'botframework'; created if it does not exist) */ 72 | databaseName?: string; 73 | 74 | /** Collection name (default = 'transcripts'; created if it does not exist) */ 75 | collectionName?: string; 76 | 77 | /** Default configuration for created collections */ 78 | onCreateCollection?: { 79 | 80 | /** Throughput for created collections (default = 1000) */ 81 | throughput?: number; 82 | 83 | /** Time-to-live for created collections (default = none) */ 84 | ttl?: number; 85 | }; 86 | } 87 | 88 | export class CosmosDbTranscriptStore implements TranscriptStore { 89 | 90 | private initializer: Initializer; 91 | private transcriptIdCache = new Set(); 92 | private get db() { return `dbs/${this.options.databaseName}`; } 93 | private get coll() { return `${this.db}/colls/${this.options.collectionName}`; } 94 | 95 | constructor(private client: DocumentClient, private options?: CosmosDbTranscriptStoreOptions) { 96 | options = this.options = options || { }; 97 | options.onCreateCollection = options.onCreateCollection || {}; 98 | options.databaseName = options.databaseName || DEFAULT_DB_NAME; 99 | options.collectionName = options.collectionName || DEFAULT_COLL_NAME; 100 | options.onCreateCollection.throughput = options.onCreateCollection.throughput || DEFAULT_COLL_THROUGHPUT; 101 | options.onCreateCollection.ttl = options.onCreateCollection.ttl || DEFAULT_COLL_TTL; 102 | this.initializer = new Initializer(async () => { 103 | const collection: Collection = Object.assign({}, DEFAULT_COLL, { id: options.collectionName }); 104 | const createOptions = { offerThroughput: options.onCreateCollection.throughput }; 105 | if (options.onCreateCollection.ttl) { 106 | collection.defaultTtl = options.onCreateCollection.ttl; 107 | } 108 | await createDatabaseIfNotExists(client, options.databaseName); 109 | await createCollectionIfNotExists(client, this.db, collection, createOptions); 110 | }); 111 | } 112 | 113 | async logActivity(activity: Activity): Promise { 114 | await this.initializer.wait(); 115 | 116 | // set a 'start' flag on the db record if this is the first activity in a transcript 117 | // due to concurrency, a transcript record may have >1 activity with the 'start' flag 118 | // store known transcript ids in a local cache to avoid the extra DB lookup. 119 | let start = false; 120 | const transcriptId = activity.channelId + activity.conversation.id; 121 | if (!this.transcriptIdCache.has(transcriptId)) { 122 | this.transcriptIdCache.add(transcriptId); 123 | const parameters = [ 124 | { name: '@channelId', value: activity.channelId }, 125 | { name: '@conversationId', value: activity.conversation.id }, 126 | ]; 127 | const sql = { query: FIND_CONVERSATION_START, parameters }; 128 | const [results] = await queryDocuments(this.client, this.coll, sql); 129 | start = !results.length; 130 | } 131 | 132 | await createDocument(this.client, this.coll, {activity, start} as any); 133 | } 134 | 135 | async getTranscriptActivities(channelId: string, conversationId: string, continuationToken?: string, startDate?: Date): Promise> { 136 | await this.initializer.wait(); 137 | const [results, headers] = await this.listTranscriptActivities(channelId, conversationId, continuationToken, startDate); 138 | 139 | return { 140 | items: results.map((x) => x.activity), 141 | continuationToken: headers['x-ms-continuation'], 142 | }; 143 | } 144 | 145 | async listTranscripts(channelId: string, continuationToken?: string): Promise> { 146 | await this.initializer.wait(); 147 | const parameters = [ 148 | { name: '@channelId', value: channelId }, 149 | ]; 150 | const sql = { query: LIST_TRANSCRIPTS, parameters }; 151 | const options = { continuation: continuationToken }; 152 | const [results, headers] = await queryDocuments(this.client, this.coll, sql, options); 153 | 154 | // due to concurrency, a transcript may have duplicate records 155 | // use a Map to limit each transcript to a single (earliest by date) output record 156 | const transcripts = new Map(); 157 | for (const result of results) { 158 | const id = result.channelId + result.id; 159 | 160 | // add the transcript to output 161 | if (!transcripts.has(id)) { 162 | transcripts.set(id, result); 163 | 164 | // replace newer transcript with current older transcript 165 | } else if (result.created < transcripts.get(id).created) { 166 | transcripts.set(id, result); 167 | } 168 | } 169 | 170 | return { 171 | items: Array.from(transcripts.values()), 172 | continuationToken: headers['x-ms-continuation'], 173 | }; 174 | } 175 | 176 | async deleteTranscript(channelId: string, conversationId: string): Promise { 177 | await this.initializer.wait(); 178 | 179 | let results: any[]; 180 | let headers: any; 181 | let ct = null; 182 | 183 | do { 184 | [results, headers] = await this.listTranscriptActivities(channelId, conversationId, ct); 185 | for (const doc of results) { 186 | await deleteDocument(this.client, doc._self); 187 | } 188 | ct = headers['x-ms-continuation']; 189 | } while (ct); 190 | } 191 | 192 | private listTranscriptActivities(channelId: string, conversationId: string, continuationToken?: string, startDate?: Date) { 193 | const parameters = [ 194 | { name: '@channelId', value: channelId }, 195 | { name: '@conversationId', value: conversationId }, 196 | { name: '@startDate', value: startDate || '' }, 197 | ]; 198 | const sql = { query: LIST_TRANSCRIPT_ACTIVITIES, parameters }; 199 | const options = { continuation: continuationToken }; 200 | return queryDocuments(this.client, this.coll, sql, options); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/src/initializer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | enum InitializerState { 5 | none, 6 | ready, 7 | working, 8 | } 9 | 10 | /** 11 | * Perform an initialization task exactly once for multiple callers 12 | */ 13 | export class Initializer { 14 | 15 | private state = InitializerState.none; 16 | private worker: Promise; 17 | 18 | /** 19 | * Create a new Initializer instance 20 | * @param task The initialization task 21 | */ 22 | constructor(private task: () => Promise) { } 23 | 24 | /** 25 | * Return a promise that is resolved when initialization is done 26 | */ 27 | wait(): Promise { 28 | switch (this.state) { 29 | case InitializerState.ready: 30 | return Promise.resolve(); 31 | 32 | case InitializerState.working: 33 | return this.worker; 34 | 35 | default: 36 | this.state = InitializerState.working; 37 | return this.worker = this.task().then(() => { 38 | this.state = InitializerState.ready; 39 | }); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/src/queries.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | export const LIST_TRANSCRIPT_ACTIVITIES = ` 5 | SELECT * 6 | FROM c 7 | WHERE 8 | c.activity.channelId = @channelId 9 | AND c.activity.conversation.id = @conversationId 10 | AND c.activity.timestamp > @startDate 11 | ORDER BY 12 | c.activity.timestamp`; 13 | 14 | export const LIST_TRANSCRIPTS = ` 15 | SELECT 16 | c.activity.channelId, 17 | c.activity.conversation.id, 18 | c.activity.timestamp as created 19 | FROM c 20 | WHERE 21 | c.activity.channelId = @channelId 22 | AND c.start 23 | ORDER BY 24 | c.activity.timestamp DESC`; 25 | 26 | export const FIND_CONVERSATION_START = ` 27 | SELECT TOP 1 c.id 28 | FROM c 29 | WHERE 30 | c.activity.conversation.id = @conversationId 31 | AND c.activity.channelId = @channelId 32 | AND c.start 33 | `; 34 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/test/cosmosdb-transcript-store-spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { Activity } from 'botframework-schema'; 5 | import { expect } from 'chai'; 6 | import { DocumentQuery, FeedOptions, SqlQuerySpec } from 'documentdb'; 7 | import { stub } from 'sinon'; 8 | 9 | import { CosmosDbTranscriptStore } from '../src'; 10 | import { createMockDocumentDb, createMockQueryExecutor, MockDocumentDb } from './mock-documentdb'; 11 | 12 | describe('CosmosDb Transcript Store', () => { 13 | const channelId = 'foo'; 14 | const conversationId = 'bar'; 15 | let client: MockDocumentDb; 16 | let store: CosmosDbTranscriptStore; 17 | 18 | beforeEach(() => { 19 | client = createMockDocumentDb(); 20 | store = new CosmosDbTranscriptStore(client as any); 21 | }); 22 | 23 | describe('Logging', () => { 24 | 25 | describe('First Activity', () => { 26 | it('should log activity as start', async () => { 27 | const activity = { channelId: 'test', conversation: { id: 'convo1' } } as any as Activity; 28 | await store.logActivity(activity); 29 | const [, doc] = client.createDocument.args[0]; 30 | expect(doc).to.deep.equal({ activity, start: true }); 31 | }); 32 | }); 33 | 34 | describe('Second Activity', () => { 35 | beforeEach(() => { 36 | client.queryDocuments = stub().returns(createMockQueryExecutor(null, [{}], {})); 37 | }); 38 | it('should log activity as not start', async () => { 39 | const activity = { channelId: 'test', conversation: { id: 'convo1' } } as any as Activity; 40 | await store.logActivity(activity); 41 | const [, doc] = client.createDocument.args[0]; 42 | expect(doc).to.deep.equal({ activity, start: false }); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('Transcript Retrieval', () => { 48 | const storedActivities = [{ activity: { foo: 'bar' } }]; 49 | const continuationToken = 'abc'; 50 | 51 | beforeEach(() => { 52 | client.queryDocuments = stub().returns(createMockQueryExecutor(null, storedActivities, {})); 53 | }); 54 | 55 | it('should return transcript activities', async () => { 56 | const retrieved = await store.getTranscriptActivities(channelId, conversationId); 57 | expect(retrieved.items).to.deep.equal(storedActivities.map((x) => x.activity)); 58 | expect(retrieved.continuationToken).to.be.undefined; 59 | }); 60 | 61 | describe('With Continuation in Request', () => { 62 | it('should return transcript activities', async () => { 63 | const retrieved = await store.getTranscriptActivities(channelId, conversationId, continuationToken); 64 | const [[, , queryOptions]] = client.queryDocuments.args as [[string, DocumentQuery, FeedOptions]]; 65 | expect(retrieved.items).to.deep.equal(storedActivities.map((x) => x.activity)); 66 | expect(retrieved.continuationToken).to.be.undefined; 67 | expect(queryOptions.continuation).to.equal(continuationToken); 68 | }); 69 | }); 70 | 71 | describe('With StartDate in Request', () => { 72 | const startDate = new Date(); 73 | it('should return transcript activities', async () => { 74 | const retrieved = await store.getTranscriptActivities(channelId, conversationId, null, startDate); 75 | const [[, query]] = client.queryDocuments.args as [[string, SqlQuerySpec]]; 76 | expect(retrieved.items).to.deep.equal(storedActivities.map((x) => x.activity)); 77 | expect(retrieved.continuationToken).to.be.undefined; 78 | expect(query.parameters).to.deep.include({name: '@startDate', value: startDate}); 79 | }); 80 | }); 81 | 82 | describe('With Continuation in Response', () => { 83 | beforeEach(() => { 84 | client.queryDocuments = stub().returns(createMockQueryExecutor(null, storedActivities, {'x-ms-continuation': continuationToken})); 85 | }); 86 | it('should return transcript activities and a continuation token', async () => { 87 | const retrieved = await store.getTranscriptActivities(channelId, conversationId); 88 | expect(retrieved.items).to.deep.equal(storedActivities.map((x) => x.activity)); 89 | expect(retrieved.continuationToken).to.equal(continuationToken); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('Transcript Listing', () => { 95 | const continuationToken = 'abc'; 96 | const storedActivities = [ 97 | { channelId: 'test1', id: '1', created: new Date().toISOString() }, 98 | { channelId: 'test1', id: '2', created: new Date().toISOString() }, 99 | ]; 100 | 101 | beforeEach(() => { 102 | client.queryDocuments = stub().returns(createMockQueryExecutor(null, storedActivities, {})); 103 | }); 104 | 105 | it('should list transcripts', async () => { 106 | const retrieved = await store.listTranscripts(channelId); 107 | expect(retrieved.items).to.deep.equal(storedActivities); 108 | expect(retrieved.continuationToken).to.be.undefined; 109 | }); 110 | 111 | describe('With Continuation in Request', () => { 112 | it('should list transcripts', async () => { 113 | const retrieved = await store.listTranscripts(channelId, continuationToken); 114 | const [[, , queryOptions]] = client.queryDocuments.args as [[string, DocumentQuery, FeedOptions]]; 115 | expect(retrieved.items).to.deep.equal(storedActivities); 116 | expect(retrieved.continuationToken).to.be.undefined; 117 | expect(queryOptions.continuation).to.equal(continuationToken); 118 | }); 119 | }); 120 | 121 | describe('With Continuation in Response', () => { 122 | beforeEach(() => { 123 | client.queryDocuments = stub().returns(createMockQueryExecutor(null, storedActivities, { 'x-ms-continuation': continuationToken })); 124 | }); 125 | 126 | it('should list transcripts and a continuation token', async () => { 127 | const retrieved = await store.listTranscripts(channelId); 128 | expect(retrieved.items).to.deep.equal(storedActivities); 129 | expect(retrieved.continuationToken).to.equal(continuationToken); 130 | }); 131 | }); 132 | 133 | describe('With Extra Conversation Activity Record', () => { 134 | beforeEach(() => { 135 | const extra = { channelId: 'test1', id: '2', created: new Date().toISOString() }; 136 | client.queryDocuments = stub().returns(createMockQueryExecutor(null, storedActivities.concat(extra), {})); 137 | }); 138 | 139 | it('should list transcripts', async () => { 140 | const retrieved = await store.listTranscripts(channelId); 141 | expect(retrieved.items).to.deep.equal(storedActivities); 142 | expect(retrieved.continuationToken).to.be.undefined; 143 | }); 144 | }); 145 | }); 146 | 147 | describe('Delete Transcript', () => { 148 | const storedActivities = [ 149 | { _self: '1', activity: {foo: 'bar' } }, 150 | { _self: '2', activity: {foo: 'bar' } }, 151 | ]; 152 | 153 | beforeEach(() => { 154 | client.queryDocuments = stub().returns(createMockQueryExecutor(null, storedActivities, {})); 155 | }); 156 | 157 | it('should delete all activities', async () => { 158 | await store.deleteTranscript(channelId, conversationId); 159 | const deletes = client.deleteDocument.getCalls(); 160 | expect(deletes.length).to.equal(storedActivities.length); 161 | storedActivities.forEach((activity, i) => { 162 | expect(deletes[i].calledWith(activity._self)).to.be.true; 163 | }); 164 | }); 165 | 166 | describe('With Continuation in Response', () => { 167 | const continuationToken = 'abc'; 168 | const storedActivitiesContinued = [ 169 | { _self: '3', activity: { foo: 'bar' } }, 170 | { _self: '4', activity: { foo: 'bar' } }, 171 | ]; 172 | const storedActivitiesAll = storedActivities.concat(storedActivitiesContinued); 173 | beforeEach(() => { 174 | client.queryDocuments = stub() 175 | .onFirstCall().returns(createMockQueryExecutor(null, storedActivities, { 'x-ms-continuation': continuationToken })) 176 | .onSecondCall().returns(createMockQueryExecutor(null, storedActivitiesContinued, { })); 177 | }); 178 | it('should delete all activities', async () => { 179 | await store.deleteTranscript(channelId, conversationId); 180 | const deletes = client.deleteDocument.getCalls(); 181 | expect(deletes.length).to.equal(storedActivitiesAll.length); 182 | expect(client.queryDocuments.callCount).to.equal(2); 183 | storedActivitiesAll.forEach((activity, i) => { 184 | expect(deletes[i].calledWith(activity._self)).to.be.true; 185 | }); 186 | }); 187 | }); 188 | }); 189 | 190 | afterEach(() => { 191 | expect(client.createDatabase.calledOnce).to.be.true; 192 | expect(client.createCollection.calledOnce).to.be.true; 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/test/initializer-spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { expect } from 'chai'; 5 | 6 | import { Initializer } from '../src/initializer'; 7 | 8 | describe('Initializer', () => { 9 | describe('Successful init', () => { 10 | let initializer: Initializer; 11 | let initCount: number; 12 | beforeEach(() => { 13 | initCount = 0; 14 | initializer = new Initializer(() => { 15 | initCount++; 16 | return Promise.resolve(); 17 | }); 18 | }); 19 | 20 | it('inits only once', async () => { 21 | const p1 = initializer.wait(); 22 | const p2 = initializer.wait(); 23 | 24 | await Promise.all([p1, p2]); 25 | 26 | expect(initCount).to.equal(1); 27 | }); 28 | }); 29 | 30 | describe('Failed init', () => { 31 | const err = new Error('foo'); 32 | let initializer: Initializer; 33 | beforeEach(() => { 34 | initializer = new Initializer(() => Promise.reject(err)); 35 | }); 36 | 37 | it('propogates init error', async () => { 38 | let caught: Error; 39 | try { 40 | await initializer.wait(); 41 | expect.fail(); 42 | } catch (err) { 43 | caught = err; 44 | } 45 | expect(caught).to.equal(err); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/test/mock-documentdb.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { QueryError } from 'documentdb'; 5 | import { SinonStub, stub } from 'sinon'; 6 | 7 | export interface MockDocumentDb { 8 | createDatabase: SinonStub; 9 | createCollection: SinonStub; 10 | createDocument: SinonStub; 11 | queryDocuments: SinonStub; 12 | deleteDocument: SinonStub; 13 | } 14 | 15 | export interface MockDocumentDbExecutor { 16 | executeNext: SinonStub; 17 | } 18 | 19 | export function createMockDocumentDb(): MockDocumentDb { 20 | return { 21 | createDatabase: stub().yields(), 22 | createCollection: stub().yields(), 23 | createDocument: stub().yields(), 24 | queryDocuments: stub().returns(createMockQueryExecutor(null, [], {})), 25 | deleteDocument: stub().yields(), 26 | }; 27 | } 28 | 29 | export function createMockQueryExecutor(error: QueryError, results: any[], headers: {[key: string]: string}) { 30 | return { 31 | executeNext: stub().yields(error, results.slice(), headers), 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | } -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "no-unused-expression": false 5 | } 6 | } -------------------------------------------------------------------------------- /packages/botbuilder-transcript-cosmosdb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "dist", 9 | "test" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "noImplicitAny": true, 6 | "removeComments": false, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "declaration": true 10 | } 11 | } -------------------------------------------------------------------------------- /packages/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "member-access": [true, "no-public"], 5 | "max-line-length": [true, 200], 6 | "quotemark": [false], 7 | "no-shadowed-variable": false, 8 | "interface-name": [ 9 | false 10 | ], 11 | "object-literal-sort-keys": false, 12 | "no-console": false 13 | } 14 | } --------------------------------------------------------------------------------