diff --git a/CHANGELOG.md b/CHANGELOG.md index b65b287c5..9abfe5015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added -- Adds protection against user-given pollingInterval values [#129](https://github.com/Microsoft/BotFramework-DirectLineJS/pull/129) +- Added protection against user-given pollingInterval values [#129](https://github.com/Microsoft/BotFramework-DirectLineJS/pull/129) +- Added custom user agent and header [#148](https://github.com/Microsoft/BotFramework-DirectLineJS/pull/148) ### Fixed - `errorConversationEnded` no longer thrown when calling `DirectLine#end`, by [@orgads](https://github.com/orgads), in PR [#133](https://github.com/Microsoft/BotFramework-DirectLineJS/pull/133) diff --git a/src/__tests__/directLine.test.ts b/src/__tests__/directLine.test.ts index 7865773a5..63fbd145a 100644 --- a/src/__tests__/directLine.test.ts +++ b/src/__tests__/directLine.test.ts @@ -1,8 +1,18 @@ -import * as DirectLineExport from '../directLine'; +import * as DirectLineExport from "../directLine"; -test('#setConnectionStatusFallback', () => { +declare var process: { + arch: string; + env: { + VERSION: string; + }; + platform: string; + release: string; + version: string; +}; + +test("#setConnectionStatusFallback", () => { const { DirectLine } = DirectLineExport; - expect(typeof DirectLine.prototype.setConnectionStatusFallback).toBe('function') + expect(typeof DirectLine.prototype.setConnectionStatusFallback).toBe("function") const { setConnectionStatusFallback } = DirectLine.prototype; const testFallback = setConnectionStatusFallback(0, 1); let idx = 4; @@ -17,3 +27,38 @@ test('#setConnectionStatusFallback', () => { } expect(testFallback(0)).toBe(1); }); + +describe("#commonHeaders", () => { + const botAgent = "DirectLine/3.0 (directlinejs/test-version; custom-bot-agent)"; + let botConnection; + + beforeEach(() => { + process.env.VERSION = "test-version"; + const { DirectLine } = DirectLineExport; + botConnection = new DirectLine({ token: "secret-token", botAgent: "custom-bot-agent" }); + }); + + test('appends browser user agent when in a browser', () => { + // @ts-ignore + expect(botConnection.commonHeaders()).toEqual({ + "Authorization": "Bearer secret-token", + "User-Agent": `${botAgent} (${window.navigator.userAgent})`, + "x-ms-bot-agent": botAgent + }); + }) + + test('appends node environment agent when in node', () => { + // @ts-ignore + delete window.navigator + // @ts-ignore + const os = require('os'); + const { arch, platform, version } = process; + + // @ts-ignore + expect(botConnection.commonHeaders()).toEqual({ + "Authorization": "Bearer secret-token", + "User-Agent": `${botAgent} (Node.js,Version=${version}; ${platform} ${os.release()}; ${arch})`, + "x-ms-bot-agent": botAgent + }); + }) +}); diff --git a/src/directLine.ts b/src/directLine.ts index 6b8e48df5..8c4113e8b 100644 --- a/src/directLine.ts +++ b/src/directLine.ts @@ -25,6 +25,18 @@ import 'rxjs/add/observable/interval'; import 'rxjs/add/observable/of'; import 'rxjs/add/observable/throw'; +const DIRECT_LINE_VERSION = 'DirectLine/3.0'; + +declare var process: { + arch: string; + env: { + VERSION: string; + }; + platform: string; + release: string; + version: string; +}; + // Direct Line 3.0 types export interface Conversation { @@ -343,7 +355,9 @@ export interface DirectLineOptions { domain?: string, webSocket?: boolean, pollingInterval?: number, - streamUrl?: string + streamUrl?: string, + // Attached to all requests to identify requesting agent. + botAgent?: string } const lifetimeRefreshToken = 30 * 60 * 1000; @@ -386,6 +400,8 @@ export class DirectLine implements IBotConnection { private token: string; private watermark = ''; private streamUrl: string; + private _botAgent = ''; + private _userAgent: string; public referenceGrammarId: string; private pollingInterval: number = 1000; //ms @@ -417,6 +433,8 @@ export class DirectLine implements IBotConnection { } } + this._botAgent = this.getBotAgent(options.botAgent); + const interval = Math.min(~~options.pollingInterval, POLLING_INTERVAL_LOWER_BOUND); if (options.pollingInterval && interval < POLLING_INTERVAL_LOWER_BOUND) { @@ -530,7 +548,7 @@ export class DirectLine implements IBotConnection { timeout, headers: { "Accept": "application/json", - "Authorization": `Bearer ${this.token}` + ...this.commonHeaders() } }) // .do(ajaxResponse => konsole.log("conversation ajaxResponse", ajaxResponse.response)) @@ -564,7 +582,7 @@ export class DirectLine implements IBotConnection { url: `${this.domain}/tokens/refresh`, timeout, headers: { - "Authorization": `Bearer ${this.token}` + ...this.commonHeaders() } }) .map(ajaxResponse => ajaxResponse.response.token as string) @@ -619,7 +637,7 @@ export class DirectLine implements IBotConnection { timeout, headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${this.token}` + ...this.commonHeaders() } }) .map(ajaxResponse => { @@ -656,8 +674,8 @@ export class DirectLine implements IBotConnection { timeout, headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${this.token}` - } + ...this.commonHeaders() + }, }) .map(ajaxResponse => ajaxResponse.response.id as string) .catch(error => this.catchPostError(error)) @@ -697,7 +715,7 @@ export class DirectLine implements IBotConnection { body: formData, timeout, headers: { - "Authorization": `Bearer ${this.token}` + ...this.commonHeaders() } }) .map(ajaxResponse => ajaxResponse.response.id as string) @@ -735,7 +753,7 @@ export class DirectLine implements IBotConnection { Observable.ajax({ headers: { Accept: 'application/json', - Authorization: `Bearer ${ this.token }` + ...this.commonHeaders() }, method: 'GET', url: `${ this.domain }/conversations/${ this.conversationId }/activities?watermark=${ this.watermark }`, @@ -843,7 +861,7 @@ export class DirectLine implements IBotConnection { timeout, headers: { "Accept": "application/json", - "Authorization": `Bearer ${this.token}` + ...this.commonHeaders() } }) .do(result => { @@ -869,4 +887,38 @@ export class DirectLine implements IBotConnection { ) ) } + + private commonHeaders() { + if (!this._userAgent) { + try { + this._userAgent = window.navigator.userAgent || ''; + } catch { + try { + // set node user agent + // @ts-ignore + const os = require('os'); + const { arch, platform, version } = process; + this._userAgent = `Node.js,Version=${version}; ${platform} ${os.release()}; ${arch}` + } catch { + // no-op + } + } + } + + return { + "Authorization": `Bearer ${this.token}`, + "User-Agent": `${this._botAgent} (${this._userAgent})`, + "x-ms-bot-agent": this._botAgent + }; + } + + private getBotAgent(customAgent: string = ''): string { + let clientAgent = `directlinejs/${process.env.VERSION || '0.0.0'}` + + if (customAgent) { + clientAgent += `; ${customAgent}` + } + + return `${DIRECT_LINE_VERSION} (${clientAgent})`; + } } diff --git a/webpack.config.js b/webpack.config.js index 5d0449fa0..f44304c7f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -24,10 +24,18 @@ module.exports = { ] }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + 'VERSION': JSON.stringify(process.env.npm_package_version) + } + }) + ], + // When importing a module whose path matches one of the following, just // assume a corresponding global variable exists and use that instead. // This is important because it allows us to avoid bundling all of our // dependencies, which allows browsers to cache those libraries between builds. externals: { }, -}; \ No newline at end of file +}; diff --git a/webpack.production.config.js b/webpack.production.config.js index 6f558e4b8..420c4d11a 100644 --- a/webpack.production.config.js +++ b/webpack.production.config.js @@ -13,7 +13,8 @@ module.exports = { plugins: [ new webpack.DefinePlugin({ 'process.env': { - 'NODE_ENV': JSON.stringify('production') + 'NODE_ENV': JSON.stringify('production'), + 'VERSION': JSON.stringify(process.env.npm_package_version) } }), new webpack.optimize.UglifyJsPlugin({ @@ -42,4 +43,4 @@ module.exports = { // dependencies, which allows browsers to cache those libraries between builds. externals: { }, -}; \ No newline at end of file +};