diff --git a/README.md b/README.md index f190d97f..0a6b34bb 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ For direct use in a browser script: ```{"language":"html"} - + @@ -149,6 +149,78 @@ webChatApi.postWebchatGuestConversations(createChatBody) .catch(console.error); ``` +## SDK Logging + +Logging of API requests and responses can be controlled by a number of parameters on the `Configuration`'s `Logger` instance. + +`log_level` values: +1. LTrace (HTTP Method, URL, Request Body, HTTP Status Code, Request Headers, Response Headers) +2. LDebug (HTTP Method, URL, Request Body, HTTP Status Code, Request Headers) +3. LError (HTTP Method, URL, Request Body, Response Body, HTTP Status Code, Request Headers, Response Headers) +4. LNone - default + +`log_format` values: +1. JSON +2. TEXT - default + +By default, the request and response bodies are not logged because these can contain PII. Be mindful of this data if choosing to log it. +To log to a file, provide a `log_file_path` value. SDK users are responsible for the rotation of the log file. This feature is not available in browser-based applications. + +Example logging configuration: +```{"language":"javascript"} +client.config.logger.log_level = client.config.logger.logLevelEnum.level.LTrace; +client.config.logger.log_format = client.config.logger.logFormatEnum.formats.JSON; +client.config.logger.log_request_body = true; +client.config.logger.log_response_body = true; +client.config.logger.log_to_console = true; +client.config.logger.log_file_path = "/var/log/javascriptguestsdk.log"; + +client.config.logger.setLogger(); // To apply above changes +``` + +#### Configuration file + +**Note:** This feature is not available in browser-based applications + +A number of configuration parameters can be applied using a configuration file. There are two sources for this file: + +1. The SDK will look for `%USERPROFILE%\.genesyscloudjavascript-guest\config` on Windows if the environment variable USERPROFILE is defined, otherwise uses the path to the profile directory of the current user as home, or `$HOME/.genesyscloudjavascript-guest/config` on Unix. +2. Provide a valid file path to `client.config.setConfigPath()` + +The SDK will constantly check to see if the config file has been updated, regardless of whether a config file was present at start-up. To disable this behaviour, set `client.config.live_reload_config` to false. +INI and JSON formats are supported. See below for examples of configuration values in both formats: + +INI: +```{"language":"ini"} +[logging] +log_level = trace +log_format = text +log_to_console = false +log_file_path = /var/log/javascriptguestsdk.log +log_response_body = false +log_request_body = false +[general] +live_reload_config = true +host = https://api.mypurecloud.com +``` + +JSON: +```{"language":"json"} +{ + "logging": { + "log_level": "trace", + "log_format": "text", + "log_to_console": false, + "log_file_path": "/var/log/javascriptguestsdk.log", + "log_response_body": false, + "log_request_body": false + }, + "general": { + "live_reload_config": true, + "host": "https://api.mypurecloud.com" + } +} +``` ## Environments @@ -281,16 +353,6 @@ Example error response object: ``` -## Debug Logging - -There are hooks to trace requests and responses. To enable debug tracing, provide a log object. Optionally, specify a maximum number of lines. If specified, the response body trace will be truncated. If not specified, the entire response body will be traced out. - -```{"language":"javascript"} -const client = platformClient.ApiClient.instance; -client.setDebugLog(console.log, 25); -``` - - ## Versioning The SDK's version is incremented according to the [Semantic Versioning Specification](https://semver.org/). The decision to increment version numbers is determined by [diffing the Platform API's swagger](https://github.com/purecloudlabs/platform-client-sdk-common/blob/master/modules/swaggerDiff.js) for automated builds, and optionally forcing a version bump when a build is triggered manually (e.g. releasing a bugfix). diff --git a/build/README.md b/build/README.md index f190d97f..0a6b34bb 100644 --- a/build/README.md +++ b/build/README.md @@ -31,7 +31,7 @@ For direct use in a browser script: ```{"language":"html"} - + @@ -149,6 +149,78 @@ webChatApi.postWebchatGuestConversations(createChatBody) .catch(console.error); ``` +## SDK Logging + +Logging of API requests and responses can be controlled by a number of parameters on the `Configuration`'s `Logger` instance. + +`log_level` values: +1. LTrace (HTTP Method, URL, Request Body, HTTP Status Code, Request Headers, Response Headers) +2. LDebug (HTTP Method, URL, Request Body, HTTP Status Code, Request Headers) +3. LError (HTTP Method, URL, Request Body, Response Body, HTTP Status Code, Request Headers, Response Headers) +4. LNone - default + +`log_format` values: +1. JSON +2. TEXT - default + +By default, the request and response bodies are not logged because these can contain PII. Be mindful of this data if choosing to log it. +To log to a file, provide a `log_file_path` value. SDK users are responsible for the rotation of the log file. This feature is not available in browser-based applications. + +Example logging configuration: +```{"language":"javascript"} +client.config.logger.log_level = client.config.logger.logLevelEnum.level.LTrace; +client.config.logger.log_format = client.config.logger.logFormatEnum.formats.JSON; +client.config.logger.log_request_body = true; +client.config.logger.log_response_body = true; +client.config.logger.log_to_console = true; +client.config.logger.log_file_path = "/var/log/javascriptguestsdk.log"; + +client.config.logger.setLogger(); // To apply above changes +``` + +#### Configuration file + +**Note:** This feature is not available in browser-based applications + +A number of configuration parameters can be applied using a configuration file. There are two sources for this file: + +1. The SDK will look for `%USERPROFILE%\.genesyscloudjavascript-guest\config` on Windows if the environment variable USERPROFILE is defined, otherwise uses the path to the profile directory of the current user as home, or `$HOME/.genesyscloudjavascript-guest/config` on Unix. +2. Provide a valid file path to `client.config.setConfigPath()` + +The SDK will constantly check to see if the config file has been updated, regardless of whether a config file was present at start-up. To disable this behaviour, set `client.config.live_reload_config` to false. +INI and JSON formats are supported. See below for examples of configuration values in both formats: + +INI: +```{"language":"ini"} +[logging] +log_level = trace +log_format = text +log_to_console = false +log_file_path = /var/log/javascriptguestsdk.log +log_response_body = false +log_request_body = false +[general] +live_reload_config = true +host = https://api.mypurecloud.com +``` + +JSON: +```{"language":"json"} +{ + "logging": { + "log_level": "trace", + "log_format": "text", + "log_to_console": false, + "log_file_path": "/var/log/javascriptguestsdk.log", + "log_response_body": false, + "log_request_body": false + }, + "general": { + "live_reload_config": true, + "host": "https://api.mypurecloud.com" + } +} +``` ## Environments @@ -281,16 +353,6 @@ Example error response object: ``` -## Debug Logging - -There are hooks to trace requests and responses. To enable debug tracing, provide a log object. Optionally, specify a maximum number of lines. If specified, the response body trace will be truncated. If not specified, the entire response body will be traced out. - -```{"language":"javascript"} -const client = platformClient.ApiClient.instance; -client.setDebugLog(console.log, 25); -``` - - ## Versioning The SDK's version is incremented according to the [Semantic Versioning Specification](https://semver.org/). The decision to increment version numbers is determined by [diffing the Platform API's swagger](https://github.com/purecloudlabs/platform-client-sdk-common/blob/master/modules/swaggerDiff.js) for automated builds, and optionally forcing a version bump when a build is triggered manually (e.g. releasing a bugfix). diff --git a/build/dist/node/purecloud-guest-chat-client.js b/build/dist/node/purecloud-guest-chat-client.js index 7111affb..0b473798 100644 --- a/build/dist/node/purecloud-guest-chat-client.js +++ b/build/dist/node/purecloud-guest-chat-client.js @@ -16,9 +16,358 @@ var PureCloudRegionHosts = { eu_west_2: "euw2.pure.cloud" } +const logLevels = { + levels: { + none: 0, + error: 1, + debug: 2, + trace: 3, + }, +}; + +const logLevelEnum = { + level: { + LNone: 'none', + LError: 'error', + LDebug: 'debug', + LTrace: 'trace', + }, +}; + +const logFormatEnum = { + formats: { + JSON: 'json', + TEXT: 'text', + }, +}; + +class Logger { + get logLevelEnum() { + return logLevelEnum; + } + + get logFormatEnum() { + return logFormatEnum; + } + + constructor() { + this.log_level = logLevelEnum.level.LNone; + this.log_format = logFormatEnum.formats.TEXT; + this.log_to_console = true; + this.log_file_path; + this.log_response_body = false; + this.log_request_body = false; + + this.setLogger(); + } + + setLogger() { + if(typeof window === 'undefined') { + const winston = require('winston'); + this.logger = winston.createLogger({ + levels: logLevels.levels, + level: this.log_level, + }); + if (this.log_file_path && this.log_file_path !== '') { + if (this.log_format === logFormatEnum.formats.JSON) { + this.logger.add(new winston.transports.File({ format: winston.format.json(), filename: this.log_file_path })); + } else { + this.logger.add( + new winston.transports.File({ + format: winston.format.combine( + winston.format((info) => { + info.level = info.level.toUpperCase(); + return info; + })(), + winston.format.simple() + ), + filename: this.log_file_path, + }) + ); + } + } + if (this.log_to_console) { + if (this.log_format === logFormatEnum.formats.JSON) { + this.logger.add(new winston.transports.Console({ format: winston.format.json() })); + } else { + this.logger.add( + new winston.transports.Console({ + format: winston.format.combine( + winston.format((info) => { + info.level = info.level.toUpperCase(); + return info; + })(), + winston.format.simple() + ), + }) + ); + } + } + } + } + + log(level, statusCode, method, url, requestHeaders, responseHeaders, requestBody, responseBody) { + var content = this.formatLog(level, statusCode, method, url, requestHeaders, responseHeaders, requestBody, responseBody); + if (typeof window !== 'undefined') { + var shouldLog = this.calculateLogLevel(level); + if (shouldLog > 0 && this.log_to_console === true) { + if(this.log_format === this.logFormatEnum.formats.JSON) { + console.log(content); + } else { + console.log(`${level.toUpperCase()}: ${content}`); + } + } + } else { + if (this.logger.transports.length > 0) this.logger.log(level, content); + } + } + + calculateLogLevel(level) { + switch (this.log_level) { + case this.logLevelEnum.level.LError: + if (level !== this.logLevelEnum.level.LError) { + return -1; + } + return 1; + case this.logLevelEnum.level.LDebug: + if (level === this.logLevelEnum.level.LTrace) { + return -1; + } + return 1; + case this.logLevelEnum.level.LTrace: + return 1; + default: + return -1; + } + } + + formatLog(level, statusCode, method, url, requestHeaders, responseHeaders, requestBody, responseBody) { + var result; + if (requestHeaders) requestHeaders['Authorization'] = '[REDACTED]'; + if (!this.log_request_body) requestBody = undefined; + if (!this.log_response_body) responseBody = undefined; + if (this.log_format && this.log_format === logFormatEnum.formats.JSON) { + result = { + level: level, + date: new Date().toISOString(), + method: method, + url: decodeURIComponent(url), + correlationId: responseHeaders ? (responseHeaders['inin-correlation-id'] ? responseHeaders['inin-correlation-id'] : '') : '', + statusCode: statusCode, + }; + if (requestHeaders) result.requestHeaders = requestHeaders; + if (responseHeaders) result.responseHeaders = responseHeaders; + if (requestBody) result.requestBody = requestBody; + if (responseBody) result.responseBody = responseBody; + } else { + result = `${new Date().toISOString()} +=== REQUEST === +${this.formatValue('URL', decodeURIComponent(url))}${this.formatValue('Method', method)}${this.formatValue( + 'Headers', + this.formatHeaderString(requestHeaders) + )}${this.formatValue('Body', requestBody ? JSON.stringify(requestBody, null, 2) : '')} +=== RESPONSE === +${this.formatValue('Status', statusCode)}${this.formatValue('Headers', this.formatHeaderString(responseHeaders))}${this.formatValue( + 'CorrelationId', + responseHeaders ? (responseHeaders['inin-correlation-id'] ? responseHeaders['inin-correlation-id'] : '') : '' + )}${this.formatValue('Body', responseBody ? JSON.stringify(responseBody, null, 2) : '')}`; + } + + return result; + } + + formatHeaderString(headers) { + var headerString = ''; + if (!headers) return headerString; + for (const [key, value] of Object.entries(headers)) { + headerString += `\n\t${key}: ${value}`; + } + return headerString; + } + + formatValue(key, value) { + if (!value || value === '' || value === '{}') return ''; + return `${key}: ${value}\n`; + } + + getLogLevel(level) { + switch (level) { + case 'error': + return logLevelEnum.level.LError; + case 'debug': + return logLevelEnum.level.LDebug; + case 'trace': + return logLevelEnum.level.LTrace; + default: + return logLevelEnum.level.LNone; + } + } + + getLogFormat(format) { + switch (format) { + case 'json': + return logFormatEnum.formats.JSON; + default: + return logFormatEnum.formats.TEXT; + } + } +} + +class Configuration { + /** + * Singleton getter + */ + get instance() { + return Configuration.instance; + } + + /** + * Singleton setter + */ + set instance(value) { + Configuration.instance = value; + } + + constructor() { + if (!Configuration.instance) { + Configuration.instance = this; + } + + if (typeof window !== 'undefined') { + this.configPath = ''; + } else { + const os = require('os'); + const path = require('path'); + this.configPath = path.join(os.homedir(), '.genesyscloudjavascript-guest', 'config'); + } + + this.live_reload_config = true; + this.host; + this.environment; + this.basePath; + this.authUrl; + this.config; + this.logger = new Logger(); + this.setEnvironment(); + this.liveLoadConfig(); + } + + liveLoadConfig() { + // If in browser, don't read config file, use default values + if (typeof window !== 'undefined') { + this.configPath = ''; + return; + } + + this.updateConfigFromFile(); + + if (this.live_reload_config && this.live_reload_config === true) { + try { + const fs = require('fs'); + fs.watchFile(this.configPath, { persistent: false }, (eventType, filename) => { + this.updateConfigFromFile(); + if (!this.live_reload_config) { + fs.unwatchFile(this.configPath); + } + }); + } catch (err) { + // do nothing + } + } + } + + setConfigPath(path) { + if (path && path !== this.configPath) { + this.configPath = path; + this.liveLoadConfig(); + } + } + + updateConfigFromFile() { + const ConfigParser = require('configparser'); + var configparser = new ConfigParser(); + + try { + configparser.read(this.configPath); // If no error catched, indicates it's INI format + this.config = configparser; + } catch (error) { + if (error.name && error.name === 'MissingSectionHeaderError') { + // Not INI format, see if it's JSON format + const fs = require('fs'); + var configData = fs.readFileSync(this.configPath, 'utf8'); + this.config = { + _sections: JSON.parse(configData), // To match INI data format + }; + } + } + + if (this.config) this.updateConfigValues(); + } + + updateConfigValues() { + this.logger.log_level = this.logger.getLogLevel(this.getConfigString('logging', 'log_level')); + this.logger.log_format = this.logger.getLogFormat(this.getConfigString('logging', 'log_format')); + this.logger.log_to_console = + this.getConfigBoolean('logging', 'log_to_console') !== undefined + ? this.getConfigBoolean('logging', 'log_to_console') + : this.logger.log_to_console; + this.logger.log_file_path = + this.getConfigString('logging', 'log_file_path') !== undefined + ? this.getConfigString('logging', 'log_file_path') + : this.logger.log_file_path; + this.logger.log_response_body = + this.getConfigBoolean('logging', 'log_response_body') !== undefined + ? this.getConfigBoolean('logging', 'log_response_body') + : this.logger.log_response_body; + this.logger.log_request_body = + this.getConfigBoolean('logging', 'log_request_body') !== undefined + ? this.getConfigBoolean('logging', 'log_request_body') + : this.logger.log_request_body; + this.live_reload_config = + this.getConfigBoolean('general', 'live_reload_config') !== undefined + ? this.getConfigBoolean('general', 'live_reload_config') + : this.live_reload_config; + this.host = this.getConfigString('general', 'host') !== undefined ? this.getConfigString('general', 'host') : this.host; + + this.setEnvironment(); + + // Update logging configs + this.logger.setLogger(); + } + + setEnvironment(env) { + // Default value + if (env) this.environment = env; + else this.environment = this.host ? this.host : 'mypurecloud.com'; + + // Strip trailing slash + this.environment = this.environment.replace(/\/+$/, ''); + + // Strip protocol and subdomain + if (this.environment.startsWith('https://')) this.environment = this.environment.substring(8); + if (this.environment.startsWith('http://')) this.environment = this.environment.substring(7); + if (this.environment.startsWith('api.')) this.environment = this.environment.substring(4); + + this.basePath = `https://api.${this.environment}`; + this.authUrl = `https://login.${this.environment}`; + } + + getConfigString(section, key) { + if (this.config._sections[section]) return this.config._sections[section][key]; + } + + getConfigBoolean(section, key) { + if (this.config._sections[section] && this.config._sections[section][key] !== undefined) { + if (typeof this.config._sections[section][key] === 'string') { + return this.config._sections[section][key] === 'true'; + } else return this.config._sections[section][key]; + } + } +} + /** * @module purecloud-guest-chat-client/ApiClient - * @version 7.1.0 + * @version 7.2.0 */ class ApiClient { /** @@ -95,6 +444,11 @@ class ApiClient { this.hasLocalStorage = false; } + /** + * Create configuration instance for ApiClient and prepare logger. + */ + this.config = new Configuration(); + /** * The base URL against which to resolve every API call's (relative) path. * @type {String} @@ -134,16 +488,6 @@ class ApiClient { if (typeof(window) !== 'undefined') window.ApiClient = this; } - /** - * @description Sets the debug log to enable debug logging - * @param {log} debugLog - In most cases use `console.log` - * @param {integer} maxLines - (optional) The max number of lines to write to the log. Must be > 0. - */ - setDebugLog(debugLog, maxLines) { - this.debugLog = debugLog; - this.debugLogMaxLines = (maxLines && maxLines > 0) ? maxLines : undefined; - } - /** * @description If set to `true`, the response object will contain additional information about the HTTP response. When `false` (default) only the body object will be returned. * @param {boolean} returnExtended - `true` to return extended responses @@ -160,7 +504,6 @@ class ApiClient { setPersistSettings(doPersist, prefix) { this.persistSettings = doPersist; this.settingsPrefix = prefix ? prefix.replace(/\W+/g, '_') : 'purecloud'; - this._debugTrace(`this.settingsPrefix=${this.settingsPrefix}`); } /** @@ -185,7 +528,6 @@ class ApiClient { // Ensure we can access local storage if (!this.hasLocalStorage) { - this._debugTrace('Warning: Cannot access local storage. Settings will not be saved.'); return; } @@ -195,7 +537,6 @@ class ApiClient { // Save updated auth data localStorage.setItem(`${this.settingsPrefix}_auth_data`, JSON.stringify(tempData)); - this._debugTrace('Auth data saved to local storage'); } catch (e) { console.error(e); } @@ -210,7 +551,6 @@ class ApiClient { // Ensure we can access local storage if (!this.hasLocalStorage) { - this._debugTrace('Warning: Cannot access local storage. Settings will not be loaded.'); return; } @@ -230,24 +570,7 @@ class ApiClient { * @param {string} environment - (Optional, default "mypurecloud.com") Environment the session use, e.g. mypurecloud.ie, mypurecloud.com.au, etc. */ setEnvironment(environment) { - if (!environment) - environment = 'mypurecloud.com'; - - // Strip trailing slash - environment = environment.replace(/\/+$/, ''); - - // Strip protocol and subdomain - if (environment.startsWith('https://')) - environment = environment.substring(8); - if (environment.startsWith('http://')) - environment = environment.substring(7); - if (environment.startsWith('api.')) - environment = environment.substring(4); - - // Set vars - this.environment = environment; - this.basePath = `https://api.${environment}`; - this.authUrl = `https://login.${environment}`; + this.config.setEnvironment(environment); } /** @@ -304,7 +627,7 @@ class ApiClient { */ _buildAuthUrl(path, query) { if (!query) query = {}; - return Object.keys(query).reduce((url, key) => !query[key] ? url : `${url}&${key}=${query[key]}`, `${this.authUrl}/${path}?`); + return Object.keys(query).reduce((url, key) => !query[key] ? url : `${url}&${key}=${query[key]}`, `${this.config.authUrl}/${path}?`); } /** @@ -333,7 +656,7 @@ class ApiClient { if (!path.match(/^\//)) { path = `/${path}`; } - var url = this.basePath + path; + var url = this.config.basePath + path; url = url.replace(/\{([\w-]+)\}/g, (fullMatch, key) => { var value; if (pathParams.hasOwnProperty(key)) { @@ -518,23 +841,6 @@ class ApiClient { request.proxy(this.proxy); } - if(this.debugLog){ - var trace = `[REQUEST] ${httpMethod} ${url}`; - if(pathParams && Object.keys(pathParams).count > 0 && pathParams[Object.keys(pathParams)[0]]){ - trace += `\nPath Params: ${JSON.stringify(pathParams)}`; - } - - if(queryParams && Object.keys(queryParams).count > 0 && queryParams[Object.keys(queryParams)[0]]){ - trace += `\nQuery Params: ${JSON.stringify(queryParams)}`; - } - - if(bodyParam){ - trace += `\nnBody: ${JSON.stringify(bodyParam)}`; - } - - this._debugTrace(trace); - } - // apply authentications this.applyAuthToRequest(request, authNames); @@ -543,7 +849,7 @@ class ApiClient { // set header parameters request.set(this.defaultHeaders).set(this.normalizeParams(headerParams)); - //request.set({ 'purecloud-sdk': '7.1.0' }); + //request.set({ 'purecloud-sdk': '7.2.0' }); // set request timeout request.timeout(this.timeout); @@ -605,23 +911,22 @@ class ApiClient { } : response.body ? response.body : response.text; // Debug logging - if (this.debugLog) { - var trace = `[RESPONSE] ${response.status}: ${httpMethod} ${url}`; - if (response.headers) - trace += `\ninin-correlation-id: ${response.headers['inin-correlation-id']}`; - if (response.body) - trace += `\nBody: ${JSON.stringify(response.body,null,2)}`; - - // Log trace message - this._debugTrace(trace); - - // Log stack trace - if (error) - this._debugTrace(error); - } + this.config.logger.log('trace', response.statusCode, httpMethod, url, request.header, response.headers, bodyParam, undefined); + this.config.logger.log('debug', response.statusCode, httpMethod, url, request.header, undefined, bodyParam, undefined); // Resolve promise if (error) { + // Log error + this.config.logger.log( + 'error', + response.statusCode, + httpMethod, + url, + request.header, + response.headers, + bodyParam, + response.body + ); reject(data); } else { resolve(data); @@ -629,46 +934,13 @@ class ApiClient { }); }); } - - /** - * @description Parses an ISO-8601 string representation of a date value. - * @param {String} str The date value as a string. - * @returns {Date} The parsed date object. - */ - parseDate(str) { - return new Date(str.replace(/T/i, ' ')); - } - - /** - * @description Logs to the debug log - * @param {String} str The date value as a string. - * @returns {Date} The parsed date object. - */ - _debugTrace(trace) { - if (!this.debugLog) return; - - if (typeof(trace) === 'string') { - // Truncate - var truncTrace = ''; - var lines = trace.split('\n'); - if (this.debugLogMaxLines && lines.length > this.debugLogMaxLines) { - for (var i = 0; i < this.debugLogMaxLines; i++) { - truncTrace += `${lines[i]}\n`; - } - truncTrace += '...response truncated...'; - trace = truncTrace; - } - } - - this.debugLog(trace); - } } class WebChatApi { /** * WebChat service. * @module purecloud-guest-chat-client/api/WebChatApi - * @version 7.1.0 + * @version 7.2.0 */ /** @@ -1047,7 +1319,7 @@ class WebChatApi { * *
* @module purecloud-guest-chat-client/index - * @version 7.1.0 + * @version 7.2.0 */ class platformClient { constructor() { diff --git a/build/dist/web-amd/purecloud-guest-chat-client.js b/build/dist/web-amd/purecloud-guest-chat-client.js index 57c34a09..020cc0ae 100644 --- a/build/dist/web-amd/purecloud-guest-chat-client.js +++ b/build/dist/web-amd/purecloud-guest-chat-client.js @@ -14,9 +14,358 @@ define(['superagent'], function (superagent) { 'use strict'; eu_west_2: "euw2.pure.cloud" } + const logLevels = { + levels: { + none: 0, + error: 1, + debug: 2, + trace: 3, + }, + }; + + const logLevelEnum = { + level: { + LNone: 'none', + LError: 'error', + LDebug: 'debug', + LTrace: 'trace', + }, + }; + + const logFormatEnum = { + formats: { + JSON: 'json', + TEXT: 'text', + }, + }; + + class Logger { + get logLevelEnum() { + return logLevelEnum; + } + + get logFormatEnum() { + return logFormatEnum; + } + + constructor() { + this.log_level = logLevelEnum.level.LNone; + this.log_format = logFormatEnum.formats.TEXT; + this.log_to_console = true; + this.log_file_path; + this.log_response_body = false; + this.log_request_body = false; + + this.setLogger(); + } + + setLogger() { + if(typeof window === 'undefined') { + const winston = require('winston'); + this.logger = winston.createLogger({ + levels: logLevels.levels, + level: this.log_level, + }); + if (this.log_file_path && this.log_file_path !== '') { + if (this.log_format === logFormatEnum.formats.JSON) { + this.logger.add(new winston.transports.File({ format: winston.format.json(), filename: this.log_file_path })); + } else { + this.logger.add( + new winston.transports.File({ + format: winston.format.combine( + winston.format((info) => { + info.level = info.level.toUpperCase(); + return info; + })(), + winston.format.simple() + ), + filename: this.log_file_path, + }) + ); + } + } + if (this.log_to_console) { + if (this.log_format === logFormatEnum.formats.JSON) { + this.logger.add(new winston.transports.Console({ format: winston.format.json() })); + } else { + this.logger.add( + new winston.transports.Console({ + format: winston.format.combine( + winston.format((info) => { + info.level = info.level.toUpperCase(); + return info; + })(), + winston.format.simple() + ), + }) + ); + } + } + } + } + + log(level, statusCode, method, url, requestHeaders, responseHeaders, requestBody, responseBody) { + var content = this.formatLog(level, statusCode, method, url, requestHeaders, responseHeaders, requestBody, responseBody); + if (typeof window !== 'undefined') { + var shouldLog = this.calculateLogLevel(level); + if (shouldLog > 0 && this.log_to_console === true) { + if(this.log_format === this.logFormatEnum.formats.JSON) { + console.log(content); + } else { + console.log(`${level.toUpperCase()}: ${content}`); + } + } + } else { + if (this.logger.transports.length > 0) this.logger.log(level, content); + } + } + + calculateLogLevel(level) { + switch (this.log_level) { + case this.logLevelEnum.level.LError: + if (level !== this.logLevelEnum.level.LError) { + return -1; + } + return 1; + case this.logLevelEnum.level.LDebug: + if (level === this.logLevelEnum.level.LTrace) { + return -1; + } + return 1; + case this.logLevelEnum.level.LTrace: + return 1; + default: + return -1; + } + } + + formatLog(level, statusCode, method, url, requestHeaders, responseHeaders, requestBody, responseBody) { + var result; + if (requestHeaders) requestHeaders['Authorization'] = '[REDACTED]'; + if (!this.log_request_body) requestBody = undefined; + if (!this.log_response_body) responseBody = undefined; + if (this.log_format && this.log_format === logFormatEnum.formats.JSON) { + result = { + level: level, + date: new Date().toISOString(), + method: method, + url: decodeURIComponent(url), + correlationId: responseHeaders ? (responseHeaders['inin-correlation-id'] ? responseHeaders['inin-correlation-id'] : '') : '', + statusCode: statusCode, + }; + if (requestHeaders) result.requestHeaders = requestHeaders; + if (responseHeaders) result.responseHeaders = responseHeaders; + if (requestBody) result.requestBody = requestBody; + if (responseBody) result.responseBody = responseBody; + } else { + result = `${new Date().toISOString()} +=== REQUEST === +${this.formatValue('URL', decodeURIComponent(url))}${this.formatValue('Method', method)}${this.formatValue( + 'Headers', + this.formatHeaderString(requestHeaders) + )}${this.formatValue('Body', requestBody ? JSON.stringify(requestBody, null, 2) : '')} +=== RESPONSE === +${this.formatValue('Status', statusCode)}${this.formatValue('Headers', this.formatHeaderString(responseHeaders))}${this.formatValue( + 'CorrelationId', + responseHeaders ? (responseHeaders['inin-correlation-id'] ? responseHeaders['inin-correlation-id'] : '') : '' + )}${this.formatValue('Body', responseBody ? JSON.stringify(responseBody, null, 2) : '')}`; + } + + return result; + } + + formatHeaderString(headers) { + var headerString = ''; + if (!headers) return headerString; + for (const [key, value] of Object.entries(headers)) { + headerString += `\n\t${key}: ${value}`; + } + return headerString; + } + + formatValue(key, value) { + if (!value || value === '' || value === '{}') return ''; + return `${key}: ${value}\n`; + } + + getLogLevel(level) { + switch (level) { + case 'error': + return logLevelEnum.level.LError; + case 'debug': + return logLevelEnum.level.LDebug; + case 'trace': + return logLevelEnum.level.LTrace; + default: + return logLevelEnum.level.LNone; + } + } + + getLogFormat(format) { + switch (format) { + case 'json': + return logFormatEnum.formats.JSON; + default: + return logFormatEnum.formats.TEXT; + } + } + } + + class Configuration { + /** + * Singleton getter + */ + get instance() { + return Configuration.instance; + } + + /** + * Singleton setter + */ + set instance(value) { + Configuration.instance = value; + } + + constructor() { + if (!Configuration.instance) { + Configuration.instance = this; + } + + if (typeof window !== 'undefined') { + this.configPath = ''; + } else { + const os = require('os'); + const path = require('path'); + this.configPath = path.join(os.homedir(), '.genesyscloudjavascript-guest', 'config'); + } + + this.live_reload_config = true; + this.host; + this.environment; + this.basePath; + this.authUrl; + this.config; + this.logger = new Logger(); + this.setEnvironment(); + this.liveLoadConfig(); + } + + liveLoadConfig() { + // If in browser, don't read config file, use default values + if (typeof window !== 'undefined') { + this.configPath = ''; + return; + } + + this.updateConfigFromFile(); + + if (this.live_reload_config && this.live_reload_config === true) { + try { + const fs = require('fs'); + fs.watchFile(this.configPath, { persistent: false }, (eventType, filename) => { + this.updateConfigFromFile(); + if (!this.live_reload_config) { + fs.unwatchFile(this.configPath); + } + }); + } catch (err) { + // do nothing + } + } + } + + setConfigPath(path) { + if (path && path !== this.configPath) { + this.configPath = path; + this.liveLoadConfig(); + } + } + + updateConfigFromFile() { + const ConfigParser = require('configparser'); + var configparser = new ConfigParser(); + + try { + configparser.read(this.configPath); // If no error catched, indicates it's INI format + this.config = configparser; + } catch (error) { + if (error.name && error.name === 'MissingSectionHeaderError') { + // Not INI format, see if it's JSON format + const fs = require('fs'); + var configData = fs.readFileSync(this.configPath, 'utf8'); + this.config = { + _sections: JSON.parse(configData), // To match INI data format + }; + } + } + + if (this.config) this.updateConfigValues(); + } + + updateConfigValues() { + this.logger.log_level = this.logger.getLogLevel(this.getConfigString('logging', 'log_level')); + this.logger.log_format = this.logger.getLogFormat(this.getConfigString('logging', 'log_format')); + this.logger.log_to_console = + this.getConfigBoolean('logging', 'log_to_console') !== undefined + ? this.getConfigBoolean('logging', 'log_to_console') + : this.logger.log_to_console; + this.logger.log_file_path = + this.getConfigString('logging', 'log_file_path') !== undefined + ? this.getConfigString('logging', 'log_file_path') + : this.logger.log_file_path; + this.logger.log_response_body = + this.getConfigBoolean('logging', 'log_response_body') !== undefined + ? this.getConfigBoolean('logging', 'log_response_body') + : this.logger.log_response_body; + this.logger.log_request_body = + this.getConfigBoolean('logging', 'log_request_body') !== undefined + ? this.getConfigBoolean('logging', 'log_request_body') + : this.logger.log_request_body; + this.live_reload_config = + this.getConfigBoolean('general', 'live_reload_config') !== undefined + ? this.getConfigBoolean('general', 'live_reload_config') + : this.live_reload_config; + this.host = this.getConfigString('general', 'host') !== undefined ? this.getConfigString('general', 'host') : this.host; + + this.setEnvironment(); + + // Update logging configs + this.logger.setLogger(); + } + + setEnvironment(env) { + // Default value + if (env) this.environment = env; + else this.environment = this.host ? this.host : 'mypurecloud.com'; + + // Strip trailing slash + this.environment = this.environment.replace(/\/+$/, ''); + + // Strip protocol and subdomain + if (this.environment.startsWith('https://')) this.environment = this.environment.substring(8); + if (this.environment.startsWith('http://')) this.environment = this.environment.substring(7); + if (this.environment.startsWith('api.')) this.environment = this.environment.substring(4); + + this.basePath = `https://api.${this.environment}`; + this.authUrl = `https://login.${this.environment}`; + } + + getConfigString(section, key) { + if (this.config._sections[section]) return this.config._sections[section][key]; + } + + getConfigBoolean(section, key) { + if (this.config._sections[section] && this.config._sections[section][key] !== undefined) { + if (typeof this.config._sections[section][key] === 'string') { + return this.config._sections[section][key] === 'true'; + } else return this.config._sections[section][key]; + } + } + } + /** * @module purecloud-guest-chat-client/ApiClient - * @version 7.1.0 + * @version 7.2.0 */ class ApiClient { /** @@ -93,6 +442,11 @@ define(['superagent'], function (superagent) { 'use strict'; this.hasLocalStorage = false; } + /** + * Create configuration instance for ApiClient and prepare logger. + */ + this.config = new Configuration(); + /** * The base URL against which to resolve every API call's (relative) path. * @type {String} @@ -132,16 +486,6 @@ define(['superagent'], function (superagent) { 'use strict'; if (typeof(window) !== 'undefined') window.ApiClient = this; } - /** - * @description Sets the debug log to enable debug logging - * @param {log} debugLog - In most cases use `console.log` - * @param {integer} maxLines - (optional) The max number of lines to write to the log. Must be > 0. - */ - setDebugLog(debugLog, maxLines) { - this.debugLog = debugLog; - this.debugLogMaxLines = (maxLines && maxLines > 0) ? maxLines : undefined; - } - /** * @description If set to `true`, the response object will contain additional information about the HTTP response. When `false` (default) only the body object will be returned. * @param {boolean} returnExtended - `true` to return extended responses @@ -158,7 +502,6 @@ define(['superagent'], function (superagent) { 'use strict'; setPersistSettings(doPersist, prefix) { this.persistSettings = doPersist; this.settingsPrefix = prefix ? prefix.replace(/\W+/g, '_') : 'purecloud'; - this._debugTrace(`this.settingsPrefix=${this.settingsPrefix}`); } /** @@ -183,7 +526,6 @@ define(['superagent'], function (superagent) { 'use strict'; // Ensure we can access local storage if (!this.hasLocalStorage) { - this._debugTrace('Warning: Cannot access local storage. Settings will not be saved.'); return; } @@ -193,7 +535,6 @@ define(['superagent'], function (superagent) { 'use strict'; // Save updated auth data localStorage.setItem(`${this.settingsPrefix}_auth_data`, JSON.stringify(tempData)); - this._debugTrace('Auth data saved to local storage'); } catch (e) { console.error(e); } @@ -208,7 +549,6 @@ define(['superagent'], function (superagent) { 'use strict'; // Ensure we can access local storage if (!this.hasLocalStorage) { - this._debugTrace('Warning: Cannot access local storage. Settings will not be loaded.'); return; } @@ -228,24 +568,7 @@ define(['superagent'], function (superagent) { 'use strict'; * @param {string} environment - (Optional, default "mypurecloud.com") Environment the session use, e.g. mypurecloud.ie, mypurecloud.com.au, etc. */ setEnvironment(environment) { - if (!environment) - environment = 'mypurecloud.com'; - - // Strip trailing slash - environment = environment.replace(/\/+$/, ''); - - // Strip protocol and subdomain - if (environment.startsWith('https://')) - environment = environment.substring(8); - if (environment.startsWith('http://')) - environment = environment.substring(7); - if (environment.startsWith('api.')) - environment = environment.substring(4); - - // Set vars - this.environment = environment; - this.basePath = `https://api.${environment}`; - this.authUrl = `https://login.${environment}`; + this.config.setEnvironment(environment); } /** @@ -302,7 +625,7 @@ define(['superagent'], function (superagent) { 'use strict'; */ _buildAuthUrl(path, query) { if (!query) query = {}; - return Object.keys(query).reduce((url, key) => !query[key] ? url : `${url}&${key}=${query[key]}`, `${this.authUrl}/${path}?`); + return Object.keys(query).reduce((url, key) => !query[key] ? url : `${url}&${key}=${query[key]}`, `${this.config.authUrl}/${path}?`); } /** @@ -331,7 +654,7 @@ define(['superagent'], function (superagent) { 'use strict'; if (!path.match(/^\//)) { path = `/${path}`; } - var url = this.basePath + path; + var url = this.config.basePath + path; url = url.replace(/\{([\w-]+)\}/g, (fullMatch, key) => { var value; if (pathParams.hasOwnProperty(key)) { @@ -516,23 +839,6 @@ define(['superagent'], function (superagent) { 'use strict'; request.proxy(this.proxy); } - if(this.debugLog){ - var trace = `[REQUEST] ${httpMethod} ${url}`; - if(pathParams && Object.keys(pathParams).count > 0 && pathParams[Object.keys(pathParams)[0]]){ - trace += `\nPath Params: ${JSON.stringify(pathParams)}`; - } - - if(queryParams && Object.keys(queryParams).count > 0 && queryParams[Object.keys(queryParams)[0]]){ - trace += `\nQuery Params: ${JSON.stringify(queryParams)}`; - } - - if(bodyParam){ - trace += `\nnBody: ${JSON.stringify(bodyParam)}`; - } - - this._debugTrace(trace); - } - // apply authentications this.applyAuthToRequest(request, authNames); @@ -541,7 +847,7 @@ define(['superagent'], function (superagent) { 'use strict'; // set header parameters request.set(this.defaultHeaders).set(this.normalizeParams(headerParams)); - //request.set({ 'purecloud-sdk': '7.1.0' }); + //request.set({ 'purecloud-sdk': '7.2.0' }); // set request timeout request.timeout(this.timeout); @@ -603,23 +909,22 @@ define(['superagent'], function (superagent) { 'use strict'; } : response.body ? response.body : response.text; // Debug logging - if (this.debugLog) { - var trace = `[RESPONSE] ${response.status}: ${httpMethod} ${url}`; - if (response.headers) - trace += `\ninin-correlation-id: ${response.headers['inin-correlation-id']}`; - if (response.body) - trace += `\nBody: ${JSON.stringify(response.body,null,2)}`; - - // Log trace message - this._debugTrace(trace); - - // Log stack trace - if (error) - this._debugTrace(error); - } + this.config.logger.log('trace', response.statusCode, httpMethod, url, request.header, response.headers, bodyParam, undefined); + this.config.logger.log('debug', response.statusCode, httpMethod, url, request.header, undefined, bodyParam, undefined); // Resolve promise if (error) { + // Log error + this.config.logger.log( + 'error', + response.statusCode, + httpMethod, + url, + request.header, + response.headers, + bodyParam, + response.body + ); reject(data); } else { resolve(data); @@ -627,46 +932,13 @@ define(['superagent'], function (superagent) { 'use strict'; }); }); } - - /** - * @description Parses an ISO-8601 string representation of a date value. - * @param {String} str The date value as a string. - * @returns {Date} The parsed date object. - */ - parseDate(str) { - return new Date(str.replace(/T/i, ' ')); - } - - /** - * @description Logs to the debug log - * @param {String} str The date value as a string. - * @returns {Date} The parsed date object. - */ - _debugTrace(trace) { - if (!this.debugLog) return; - - if (typeof(trace) === 'string') { - // Truncate - var truncTrace = ''; - var lines = trace.split('\n'); - if (this.debugLogMaxLines && lines.length > this.debugLogMaxLines) { - for (var i = 0; i < this.debugLogMaxLines; i++) { - truncTrace += `${lines[i]}\n`; - } - truncTrace += '...response truncated...'; - trace = truncTrace; - } - } - - this.debugLog(trace); - } } class WebChatApi { /** * WebChat service. * @module purecloud-guest-chat-client/api/WebChatApi - * @version 7.1.0 + * @version 7.2.0 */ /** @@ -1045,7 +1317,7 @@ define(['superagent'], function (superagent) { 'use strict'; * * * @module purecloud-guest-chat-client/index - * @version 7.1.0 + * @version 7.2.0 */ class platformClient { constructor() { diff --git a/build/dist/web-amd/purecloud-guest-chat-client.min.js b/build/dist/web-amd/purecloud-guest-chat-client.min.js index e1806a51..d25628f0 100644 --- a/build/dist/web-amd/purecloud-guest-chat-client.min.js +++ b/build/dist/web-amd/purecloud-guest-chat-client.min.js @@ -1 +1 @@ -define(["superagent"],function(e){"use strict";e=e&&e.hasOwnProperty("default")?e.default:e;var t={us_east_1:"mypurecloud.com",eu_west_1:"mypurecloud.ie",ap_southeast_2:"mypurecloud.com.au",ap_northeast_1:"mypurecloud.jp",eu_central_1:"mypurecloud.de",us_west_2:"usw2.pure.cloud",ca_central_1:"cac1.pure.cloud",ap_northeast_2:"apne2.pure.cloud",eu_west_2:"euw2.pure.cloud"};class a{get instance(){return a.instance}set instance(e){a.instance=e}constructor(){a.instance||(a.instance=this),this.CollectionFormatEnum={CSV:",",SSV:" ",TSV:"\t",PIPES:"|",MULTI:"multi"};try{localStorage.setItem("purecloud_local_storage_test","purecloud_local_storage_test"),localStorage.removeItem("purecloud_local_storage_test"),this.hasLocalStorage=!0}catch(e){this.hasLocalStorage=!1}this.setEnvironment("https://api.mypurecloud.com"),this.authentications={"PureCloud OAuth":{type:"oauth2"},"Guest Chat JWT":{type:"apiKey",in:"header",name:"Authorization",apiKeyPrefix:"Bearer"}},this.defaultHeaders={},this.timeout=16e3,this.authData={},this.settingsPrefix="purecloud",this.superagent=e,"undefined"!=typeof window&&(window.ApiClient=this)}setDebugLog(e,t){this.debugLog=e,this.debugLogMaxLines=t&&t>0?t:void 0}setReturnExtendedResponses(e){this.returnExtended=e}setPersistSettings(e,t){this.persistSettings=e,this.settingsPrefix=t?t.replace(/\W+/g,"_"):"purecloud",this._debugTrace(`this.settingsPrefix=${this.settingsPrefix}`)}_saveSettings(e){try{if(this.authData.apiKey=e.apiKey,this.authentications["Guest Chat JWT"].apiKey=e.apiKey,e.state&&(this.authData.state=e.state),e.tokenExpiryTime&&(this.authData.tokenExpiryTime=e.tokenExpiryTime,this.authData.tokenExpiryTimeString=e.tokenExpiryTimeString),!0!==this.persistSettings)return;if(!this.hasLocalStorage)return void this._debugTrace("Warning: Cannot access local storage. Settings will not be saved.");let t=JSON.parse(JSON.stringify(this.authData));delete t.state,localStorage.setItem(`${this.settingsPrefix}_auth_data`,JSON.stringify(t)),this._debugTrace("Auth data saved to local storage")}catch(e){console.error(e)}}_loadSettings(){if(!0!==this.persistSettings)return;if(!this.hasLocalStorage)return void this._debugTrace("Warning: Cannot access local storage. Settings will not be loaded.");const e=this.authData.state;this.authData=localStorage.getItem(`${this.settingsPrefix}_auth_data`),this.authData?this.authData=JSON.parse(this.authData):this.authData={},this.authData.apiKey&&this.setJwt(this.authData.apiKey),this.authData.state=e}setEnvironment(e){e||(e="mypurecloud.com"),(e=e.replace(/\/+$/,"")).startsWith("https://")&&(e=e.substring(8)),e.startsWith("http://")&&(e=e.substring(7)),e.startsWith("api.")&&(e=e.substring(4)),this.environment=e,this.basePath=`https://api.${e}`,this.authUrl=`https://login.${e}`}_testTokenAccess(){return new Promise((e,t)=>{this._loadSettings(),this.authentications["Guest Chat JWT"].apiKey?this.callApi("/api/v2/authorization/permissions","GET",null,null,null,null,null,["Guest Chat JWT"],["application/json"],["application/json"]).then(()=>{e()}).catch(e=>{this._saveSettings({apiKey:void 0}),t(e)}):t(new Error("Token is not set"))})}setJwt(e){this._saveSettings({apiKey:e})}setStorageKey(e){this.storageKey=e,this.setJwt(this.authentications["Guest Chat JWT"].apiKey)}_buildAuthUrl(e,t){return t||(t={}),Object.keys(t).reduce((e,a)=>t[a]?`${e}&${a}=${t[a]}`:e,`${this.authUrl}/${e}?`)}paramToString(e){return e?e instanceof Date?e.toJSON():e.toString():""}buildUrl(e,t){e.match(/^\//)||(e=`/${e}`);var a=this.basePath+e;return a=a.replace(/\{([\w-]+)\}/g,(e,a)=>{var i;return i=t.hasOwnProperty(a)?this.paramToString(t[a]):e,encodeURIComponent(i)})}isJsonMime(e){return Boolean(e&&e.match(/^application\/json(;.*)?$/i))}jsonPreferredMime(e){for(var t=0;t+ | Fecha | +Moment | +
---|---|---|
Size (Min. and Gzipped) | +2.1KBs | +13.1KBs | +
Date Parsing | +✓ | +✓ | +
Date Formatting | +✓ | +✓ | +
Date Manipulation | ++ | ✓ | +
I18n Support | +✓ | +✓ | +
+ | Token | +Output | +
---|---|---|
Month | +M | +1 2 ... 11 12 | +
+ | MM | +01 02 ... 11 12 | +
+ | MMM | +Jan Feb ... Nov Dec | +
+ | MMMM | +January February ... November December | +
Day of Month | +D | +1 2 ... 30 31 | +
+ | Do | +1st 2nd ... 30th 31st | +
+ | DD | +01 02 ... 30 31 | +
Day of Week | +d | +0 1 ... 5 6 | +
+ | ddd | +Sun Mon ... Fri Sat | +
+ | dddd | +Sunday Monday ... Friday Saturday | +
Year | +YY | +70 71 ... 29 30 | +
+ | YYYY | +1970 1971 ... 2029 2030 | +
AM/PM | +A | +AM PM | +
+ | a | +am pm | +
Hour | +H | +0 1 ... 22 23 | +
+ | HH | +00 01 ... 22 23 | +
+ | h | +1 2 ... 11 12 | +
+ | hh | +01 02 ... 11 12 | +
Minute | +m | +0 1 ... 58 59 | +
+ | mm | +00 01 ... 58 59 | +
Second | +s | +0 1 ... 58 59 | +
+ | ss | +00 01 ... 58 59 | +
Fractional Second | +S | +0 1 ... 8 9 | +
+ | SS | +0 1 ... 98 99 | +
+ | SSS | +0 1 ... 998 999 | +
Timezone | +Z | ++ -07:00 -06:00 ... +06:00 +07:00 + | +
+ | ZZ | ++ -0700 -0600 ... +0600 +0700 + | +