@webex/cc-ui-logging 1.28.0-ccwidgets.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +98 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/metricsLogger.d.ts +65 -0
- package/dist/types/withMetrics.d.ts +2 -0
- package/jest.config.js +6 -0
- package/package.json +39 -0
- package/src/index.ts +5 -0
- package/src/metricsLogger.ts +102 -0
- package/src/withMetrics.tsx +29 -0
- package/tests/metricsLogger.test.ts +86 -0
- package/tests/withMetrics.test.tsx +105 -0
- package/tsconfig.json +11 -0
- package/tsconfig.test.json +9 -0
- package/webpack.config.js +17 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
|
|
3
|
+
* This devtool is neither made for production nor for readable output files.
|
|
4
|
+
* It uses "eval()" calls to create a separate source file in the browser devtools.
|
|
5
|
+
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
|
6
|
+
* or disable the default devtool with "devtool: false".
|
|
7
|
+
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
|
8
|
+
*/
|
|
9
|
+
/******/ (() => { // webpackBootstrap
|
|
10
|
+
/******/ "use strict";
|
|
11
|
+
/******/ var __webpack_modules__ = ({
|
|
12
|
+
|
|
13
|
+
/***/ "./src/index.ts":
|
|
14
|
+
/*!**********************!*\
|
|
15
|
+
!*** ./src/index.ts ***!
|
|
16
|
+
\**********************/
|
|
17
|
+
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
18
|
+
|
|
19
|
+
eval("\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nexports.withMetrics = void 0;\nconst withMetrics_1 = __importDefault(__webpack_require__(/*! ./withMetrics */ \"./src/withMetrics.tsx\"));\nexports.withMetrics = withMetrics_1.default;\n\n\n//# sourceURL=webpack://@webex/cc-ui-logging/./src/index.ts?");
|
|
20
|
+
|
|
21
|
+
/***/ }),
|
|
22
|
+
|
|
23
|
+
/***/ "./src/metricsLogger.ts":
|
|
24
|
+
/*!******************************!*\
|
|
25
|
+
!*** ./src/metricsLogger.ts ***!
|
|
26
|
+
\******************************/
|
|
27
|
+
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
28
|
+
|
|
29
|
+
eval("\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nexports.logMetrics = void 0;\nexports.havePropsChanged = havePropsChanged;\nconst cc_store_1 = __importDefault(__webpack_require__(/*! @webex/cc-store */ \"@webex/cc-store\"));\n/**\n * Logs UI metrics for contact center widgets.\n *\n * This function logs widget lifecycle events and errors to help monitor\n * widget performance and user interactions. If no logger is available,\n * it will emit a warning and skip logging.\n *\n * @param metric - The metrics data to be logged\n * @param metric.widgetName - Name of the widget generating the metric\n * @param metric.event - Type of event being logged\n * @param metric.props - Optional properties associated with the widget\n * @param metric.timestamp - Unix timestamp when the event occurred\n * @param metric.additionalContext - Optional additional context data\n *\n * @example\n * ```typescript\n * logMetrics({\n * widgetName: 'CallControl',\n * event: 'WIDGET_MOUNTED',\n * props: { callId: '123' },\n * timestamp: Date.now(),\n * additionalContext: { userId: 'user123' }\n * });\n * ```\n */\nconst logMetrics = (metric) => {\n if (!cc_store_1.default.logger) {\n console.warn('CC-Widgets: UI Metrics: No logger found');\n return;\n }\n cc_store_1.default.logger.log(`CC-Widgets: UI Metrics: ${JSON.stringify(metric, null, 2)}`, {\n module: 'metricsLogger.tsx',\n method: 'logMetrics',\n });\n};\nexports.logMetrics = logMetrics;\n/**\n * Determines if props have changed between two objects using shallow comparison.\n *\n * This function performs a shallow comparison between two objects to detect changes.\n * It compares object keys and primitive values, but does not recursively compare\n * nested objects. This is useful for determining when to log metrics based on prop changes.\n *\n * @param prev - The previous props object\n * @param next - The next props object to compare against\n * @returns `true` if the props have changed, `false` otherwise\n *\n * @example\n * ```typescript\n * const oldProps = { name: 'John', age: 30 };\n * const newProps = { name: 'John', age: 31 };\n *\n * if (havePropsChanged(oldProps, newProps)) {\n * // Props have changed, log metrics\n * logMetrics({\n * widgetName: 'UserProfile',\n * event: 'WIDGET_MOUNTED',\n * props: newProps,\n * timestamp: Date.now()\n * });\n * } * ```\n *\n * @remarks\n * The function is important as we dont sanitize our props right now.\n * Once we start sanitizing we can do a deep comparison. This is used to only re-render\n * the HOC if the props have changed.\n */\nfunction havePropsChanged(prev, next) {\n if (prev === next)\n return false;\n // Do shallow comparison\n if (typeof prev !== typeof next)\n return true;\n if (!prev || !next)\n return prev !== next;\n const prevKeys = Object.keys(prev);\n const nextKeys = Object.keys(next);\n if (prevKeys.length !== nextKeys.length)\n return true;\n // Check if any primitive values changed\n for (const key of prevKeys) {\n const prevVal = prev[key];\n const nextVal = next[key];\n if (prevVal === nextVal)\n continue;\n if (typeof prevVal !== 'object' || prevVal === null)\n return true;\n if (typeof nextVal !== 'object' || nextVal === null)\n return true;\n }\n // All shallow comparisons passed, consider props unchanged\n return false;\n}\n\n\n//# sourceURL=webpack://@webex/cc-ui-logging/./src/metricsLogger.ts?");
|
|
30
|
+
|
|
31
|
+
/***/ }),
|
|
32
|
+
|
|
33
|
+
/***/ "./src/withMetrics.tsx":
|
|
34
|
+
/*!*****************************!*\
|
|
35
|
+
!*** ./src/withMetrics.tsx ***!
|
|
36
|
+
\*****************************/
|
|
37
|
+
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
38
|
+
|
|
39
|
+
eval("\nvar __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n}));\nvar __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n});\nvar __importStar = (this && this.__importStar) || (function () {\n var ownKeys = function(o) {\n ownKeys = Object.getOwnPropertyNames || function (o) {\n var ar = [];\n for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;\n return ar;\n };\n return ownKeys(o);\n };\n return function (mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== \"default\") __createBinding(result, mod, k[i]);\n __setModuleDefault(result, mod);\n return result;\n };\n})();\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nexports[\"default\"] = withMetrics;\nconst react_1 = __importStar(__webpack_require__(/*! react */ \"react\"));\nconst metricsLogger_1 = __webpack_require__(/*! ./metricsLogger */ \"./src/metricsLogger.ts\");\nfunction withMetrics(Component, widgetName) {\n return react_1.default.memo((props) => {\n (0, react_1.useEffect)(() => {\n (0, metricsLogger_1.logMetrics)({\n widgetName,\n event: 'WIDGET_MOUNTED',\n timestamp: Date.now(),\n });\n return () => {\n (0, metricsLogger_1.logMetrics)({\n widgetName,\n event: 'WIDGET_UNMOUNTED',\n timestamp: Date.now(),\n });\n };\n }, []);\n // TODO: https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6890 PROPS_UPDATED event\n return react_1.default.createElement(Component, Object.assign({}, props));\n }, (prevProps, nextProps) => !(0, metricsLogger_1.havePropsChanged)(prevProps, nextProps));\n}\n\n\n//# sourceURL=webpack://@webex/cc-ui-logging/./src/withMetrics.tsx?");
|
|
40
|
+
|
|
41
|
+
/***/ }),
|
|
42
|
+
|
|
43
|
+
/***/ "@webex/cc-store":
|
|
44
|
+
/*!**********************************!*\
|
|
45
|
+
!*** external "@webex/cc-store" ***!
|
|
46
|
+
\**********************************/
|
|
47
|
+
/***/ ((module) => {
|
|
48
|
+
|
|
49
|
+
module.exports = require("@webex/cc-store");
|
|
50
|
+
|
|
51
|
+
/***/ }),
|
|
52
|
+
|
|
53
|
+
/***/ "react":
|
|
54
|
+
/*!************************!*\
|
|
55
|
+
!*** external "react" ***!
|
|
56
|
+
\************************/
|
|
57
|
+
/***/ ((module) => {
|
|
58
|
+
|
|
59
|
+
module.exports = require("react");
|
|
60
|
+
|
|
61
|
+
/***/ })
|
|
62
|
+
|
|
63
|
+
/******/ });
|
|
64
|
+
/************************************************************************/
|
|
65
|
+
/******/ // The module cache
|
|
66
|
+
/******/ var __webpack_module_cache__ = {};
|
|
67
|
+
/******/
|
|
68
|
+
/******/ // The require function
|
|
69
|
+
/******/ function __webpack_require__(moduleId) {
|
|
70
|
+
/******/ // Check if module is in cache
|
|
71
|
+
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
72
|
+
/******/ if (cachedModule !== undefined) {
|
|
73
|
+
/******/ return cachedModule.exports;
|
|
74
|
+
/******/ }
|
|
75
|
+
/******/ // Create a new module (and put it into the cache)
|
|
76
|
+
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
77
|
+
/******/ // no module.id needed
|
|
78
|
+
/******/ // no module.loaded needed
|
|
79
|
+
/******/ exports: {}
|
|
80
|
+
/******/ };
|
|
81
|
+
/******/
|
|
82
|
+
/******/ // Execute the module function
|
|
83
|
+
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
84
|
+
/******/
|
|
85
|
+
/******/ // Return the exports of the module
|
|
86
|
+
/******/ return module.exports;
|
|
87
|
+
/******/ }
|
|
88
|
+
/******/
|
|
89
|
+
/************************************************************************/
|
|
90
|
+
/******/
|
|
91
|
+
/******/ // startup
|
|
92
|
+
/******/ // Load entry module and return exports
|
|
93
|
+
/******/ // This entry module is referenced by other modules so it can't be inlined
|
|
94
|
+
/******/ var __webpack_exports__ = __webpack_require__("./src/index.ts");
|
|
95
|
+
/******/ module.exports = __webpack_exports__;
|
|
96
|
+
/******/
|
|
97
|
+
/******/ })()
|
|
98
|
+
;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type WidgetMetrics = {
|
|
2
|
+
widgetName: string;
|
|
3
|
+
event: 'WIDGET_MOUNTED' | 'ERROR' | 'WIDGET_UNMOUNTED' | 'PROPS_UPDATED';
|
|
4
|
+
props?: Record<string, any>;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
additionalContext?: Record<string, any>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Logs UI metrics for contact center widgets.
|
|
10
|
+
*
|
|
11
|
+
* This function logs widget lifecycle events and errors to help monitor
|
|
12
|
+
* widget performance and user interactions. If no logger is available,
|
|
13
|
+
* it will emit a warning and skip logging.
|
|
14
|
+
*
|
|
15
|
+
* @param metric - The metrics data to be logged
|
|
16
|
+
* @param metric.widgetName - Name of the widget generating the metric
|
|
17
|
+
* @param metric.event - Type of event being logged
|
|
18
|
+
* @param metric.props - Optional properties associated with the widget
|
|
19
|
+
* @param metric.timestamp - Unix timestamp when the event occurred
|
|
20
|
+
* @param metric.additionalContext - Optional additional context data
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* logMetrics({
|
|
25
|
+
* widgetName: 'CallControl',
|
|
26
|
+
* event: 'WIDGET_MOUNTED',
|
|
27
|
+
* props: { callId: '123' },
|
|
28
|
+
* timestamp: Date.now(),
|
|
29
|
+
* additionalContext: { userId: 'user123' }
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare const logMetrics: (metric: WidgetMetrics) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Determines if props have changed between two objects using shallow comparison.
|
|
36
|
+
*
|
|
37
|
+
* This function performs a shallow comparison between two objects to detect changes.
|
|
38
|
+
* It compares object keys and primitive values, but does not recursively compare
|
|
39
|
+
* nested objects. This is useful for determining when to log metrics based on prop changes.
|
|
40
|
+
*
|
|
41
|
+
* @param prev - The previous props object
|
|
42
|
+
* @param next - The next props object to compare against
|
|
43
|
+
* @returns `true` if the props have changed, `false` otherwise
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const oldProps = { name: 'John', age: 30 };
|
|
48
|
+
* const newProps = { name: 'John', age: 31 };
|
|
49
|
+
*
|
|
50
|
+
* if (havePropsChanged(oldProps, newProps)) {
|
|
51
|
+
* // Props have changed, log metrics
|
|
52
|
+
* logMetrics({
|
|
53
|
+
* widgetName: 'UserProfile',
|
|
54
|
+
* event: 'WIDGET_MOUNTED',
|
|
55
|
+
* props: newProps,
|
|
56
|
+
* timestamp: Date.now()
|
|
57
|
+
* });
|
|
58
|
+
* } * ```
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* The function is important as we dont sanitize our props right now.
|
|
62
|
+
* Once we start sanitizing we can do a deep comparison. This is used to only re-render
|
|
63
|
+
* the HOC if the props have changed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function havePropsChanged(prev: any, next: any): boolean;
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webex/cc-ui-logging",
|
|
3
|
+
"version": "1.28.0-ccwidgets.108",
|
|
4
|
+
"description": "UI metrics tracking for Webex widgets",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build:src": "webpack --mode=development",
|
|
12
|
+
"clean": "rm -rf dist",
|
|
13
|
+
"clean:dist": "rm -rf dist",
|
|
14
|
+
"test:unit": "jest --coverage",
|
|
15
|
+
"test:styles": "echo 'No styles to test'"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@webex/cc-store": "1.28.0-ccwidgets.108"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@testing-library/dom": "10.4.0",
|
|
22
|
+
"@testing-library/jest-dom": "6.6.2",
|
|
23
|
+
"@testing-library/react": "16.0.1",
|
|
24
|
+
"@types/jest": "29.5.14",
|
|
25
|
+
"@types/react-test-renderer": "18",
|
|
26
|
+
"@webex/test-fixtures": "0.0.0",
|
|
27
|
+
"babel-jest": "29.7.0",
|
|
28
|
+
"jest": "29.7.0",
|
|
29
|
+
"jest-environment-jsdom": "29.7.0",
|
|
30
|
+
"typescript": "^5.6.3",
|
|
31
|
+
"uuid": "^9.0.0",
|
|
32
|
+
"webpack": "^5.96.1",
|
|
33
|
+
"webpack-cli": "^5.1.4"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=18.3.1",
|
|
37
|
+
"react-dom": ">=18.3.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import store from '@webex/cc-store';
|
|
2
|
+
|
|
3
|
+
export type WidgetMetrics = {
|
|
4
|
+
widgetName: string;
|
|
5
|
+
event: 'WIDGET_MOUNTED' | 'ERROR' | 'WIDGET_UNMOUNTED' | 'PROPS_UPDATED';
|
|
6
|
+
props?: Record<string, any>;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
additionalContext?: Record<string, any>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Logs UI metrics for contact center widgets.
|
|
13
|
+
*
|
|
14
|
+
* This function logs widget lifecycle events and errors to help monitor
|
|
15
|
+
* widget performance and user interactions. If no logger is available,
|
|
16
|
+
* it will emit a warning and skip logging.
|
|
17
|
+
*
|
|
18
|
+
* @param metric - The metrics data to be logged
|
|
19
|
+
* @param metric.widgetName - Name of the widget generating the metric
|
|
20
|
+
* @param metric.event - Type of event being logged
|
|
21
|
+
* @param metric.props - Optional properties associated with the widget
|
|
22
|
+
* @param metric.timestamp - Unix timestamp when the event occurred
|
|
23
|
+
* @param metric.additionalContext - Optional additional context data
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* logMetrics({
|
|
28
|
+
* widgetName: 'CallControl',
|
|
29
|
+
* event: 'WIDGET_MOUNTED',
|
|
30
|
+
* props: { callId: '123' },
|
|
31
|
+
* timestamp: Date.now(),
|
|
32
|
+
* additionalContext: { userId: 'user123' }
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const logMetrics = (metric: WidgetMetrics) => {
|
|
37
|
+
if (!store.logger) {
|
|
38
|
+
console.warn('CC-Widgets: UI Metrics: No logger found');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
store.logger.log(`CC-Widgets: UI Metrics: ${JSON.stringify(metric, null, 2)}`, {
|
|
42
|
+
module: 'metricsLogger.tsx',
|
|
43
|
+
method: 'logMetrics',
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Determines if props have changed between two objects using shallow comparison.
|
|
49
|
+
*
|
|
50
|
+
* This function performs a shallow comparison between two objects to detect changes.
|
|
51
|
+
* It compares object keys and primitive values, but does not recursively compare
|
|
52
|
+
* nested objects. This is useful for determining when to log metrics based on prop changes.
|
|
53
|
+
*
|
|
54
|
+
* @param prev - The previous props object
|
|
55
|
+
* @param next - The next props object to compare against
|
|
56
|
+
* @returns `true` if the props have changed, `false` otherwise
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const oldProps = { name: 'John', age: 30 };
|
|
61
|
+
* const newProps = { name: 'John', age: 31 };
|
|
62
|
+
*
|
|
63
|
+
* if (havePropsChanged(oldProps, newProps)) {
|
|
64
|
+
* // Props have changed, log metrics
|
|
65
|
+
* logMetrics({
|
|
66
|
+
* widgetName: 'UserProfile',
|
|
67
|
+
* event: 'WIDGET_MOUNTED',
|
|
68
|
+
* props: newProps,
|
|
69
|
+
* timestamp: Date.now()
|
|
70
|
+
* });
|
|
71
|
+
* } * ```
|
|
72
|
+
*
|
|
73
|
+
* @remarks
|
|
74
|
+
* The function is important as we dont sanitize our props right now.
|
|
75
|
+
* Once we start sanitizing we can do a deep comparison. This is used to only re-render
|
|
76
|
+
* the HOC if the props have changed.
|
|
77
|
+
*/
|
|
78
|
+
export function havePropsChanged(prev: any, next: any): boolean {
|
|
79
|
+
if (prev === next) return false;
|
|
80
|
+
|
|
81
|
+
// Do shallow comparison
|
|
82
|
+
if (typeof prev !== typeof next) return true;
|
|
83
|
+
if (!prev || !next) return prev !== next;
|
|
84
|
+
|
|
85
|
+
const prevKeys = Object.keys(prev);
|
|
86
|
+
const nextKeys = Object.keys(next);
|
|
87
|
+
|
|
88
|
+
if (prevKeys.length !== nextKeys.length) return true;
|
|
89
|
+
|
|
90
|
+
// Check if any primitive values changed
|
|
91
|
+
for (const key of prevKeys) {
|
|
92
|
+
const prevVal = prev[key];
|
|
93
|
+
const nextVal = next[key];
|
|
94
|
+
|
|
95
|
+
if (prevVal === nextVal) continue;
|
|
96
|
+
if (typeof prevVal !== 'object' || prevVal === null) return true;
|
|
97
|
+
if (typeof nextVal !== 'object' || nextVal === null) return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// All shallow comparisons passed, consider props unchanged
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, {useEffect, useRef} from 'react';
|
|
2
|
+
import {havePropsChanged, logMetrics} from './metricsLogger';
|
|
3
|
+
|
|
4
|
+
export default function withMetrics<P extends object>(Component: any, widgetName: string) {
|
|
5
|
+
return React.memo(
|
|
6
|
+
(props: P) => {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
logMetrics({
|
|
9
|
+
widgetName,
|
|
10
|
+
event: 'WIDGET_MOUNTED',
|
|
11
|
+
timestamp: Date.now(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return () => {
|
|
15
|
+
logMetrics({
|
|
16
|
+
widgetName,
|
|
17
|
+
event: 'WIDGET_UNMOUNTED',
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
// TODO: https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6890 PROPS_UPDATED event
|
|
24
|
+
|
|
25
|
+
return <Component {...props} />;
|
|
26
|
+
},
|
|
27
|
+
(prevProps, nextProps) => !havePropsChanged(prevProps, nextProps)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import store from '@webex/cc-store';
|
|
2
|
+
import {logMetrics, havePropsChanged, WidgetMetrics} from '../src/metricsLogger';
|
|
3
|
+
|
|
4
|
+
describe('metricsLogger', () => {
|
|
5
|
+
store.store.logger = {
|
|
6
|
+
log: jest.fn(),
|
|
7
|
+
info: jest.fn(),
|
|
8
|
+
warn: jest.fn(),
|
|
9
|
+
error: jest.fn(),
|
|
10
|
+
trace: jest.fn(),
|
|
11
|
+
};
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('logMetrics', () => {
|
|
17
|
+
it('should log metrics when logger is available', () => {
|
|
18
|
+
const metric: WidgetMetrics = {
|
|
19
|
+
widgetName: 'TestWidget',
|
|
20
|
+
event: 'WIDGET_MOUNTED',
|
|
21
|
+
timestamp: 1234567890,
|
|
22
|
+
props: {test: 'prop'},
|
|
23
|
+
additionalContext: {context: 'test'},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
logMetrics(metric);
|
|
27
|
+
|
|
28
|
+
expect(store.logger.log).toHaveBeenCalledWith(`CC-Widgets: UI Metrics: ${JSON.stringify(metric, null, 2)}`, {
|
|
29
|
+
module: 'metricsLogger.tsx',
|
|
30
|
+
method: 'logMetrics',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should handle case when logger is not available', () => {
|
|
35
|
+
const consoleSpy = jest.spyOn(console, 'warn');
|
|
36
|
+
store.store.logger = undefined;
|
|
37
|
+
|
|
38
|
+
const metric: WidgetMetrics = {
|
|
39
|
+
widgetName: 'TestWidget',
|
|
40
|
+
event: 'WIDGET_MOUNTED',
|
|
41
|
+
timestamp: 1234567890,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
logMetrics(metric);
|
|
45
|
+
|
|
46
|
+
expect(consoleSpy).toHaveBeenCalledWith('CC-Widgets: UI Metrics: No logger found');
|
|
47
|
+
consoleSpy.mockRestore();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('havePropsChanged', () => {
|
|
52
|
+
it('should return false for identical primitives', () => {
|
|
53
|
+
expect(havePropsChanged(1, 1)).toBe(false);
|
|
54
|
+
expect(havePropsChanged('test', 'test')).toBe(false);
|
|
55
|
+
expect(havePropsChanged(true, true)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return true for different primitives', () => {
|
|
59
|
+
expect(havePropsChanged('test', 'test2')).toBe(true);
|
|
60
|
+
expect(havePropsChanged(true, false)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return true for different types', () => {
|
|
64
|
+
expect(havePropsChanged(1, '1')).toBe(true);
|
|
65
|
+
expect(havePropsChanged(null, undefined)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return true when object keys differ', () => {
|
|
69
|
+
const obj1 = {a: 1, b: 2};
|
|
70
|
+
const obj2 = {a: 1};
|
|
71
|
+
expect(havePropsChanged(obj1, obj2)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return false when nested values differ', () => {
|
|
75
|
+
const obj1 = {a: {b: 1}};
|
|
76
|
+
const obj2 = {a: {b: 2}};
|
|
77
|
+
expect(havePropsChanged(obj1, obj2)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle null and undefined', () => {
|
|
81
|
+
expect(havePropsChanged(null, null)).toBe(false);
|
|
82
|
+
expect(havePropsChanged(undefined, undefined)).toBe(false);
|
|
83
|
+
expect(havePropsChanged(null, undefined)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {render} from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import withMetrics from '../src/withMetrics';
|
|
5
|
+
import store from '@webex/cc-store';
|
|
6
|
+
import * as metricsLogger from '../src/metricsLogger';
|
|
7
|
+
|
|
8
|
+
interface TestComponentProps {
|
|
9
|
+
name?: string;
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('withMetrics HOC', () => {
|
|
14
|
+
store.store.logger = {
|
|
15
|
+
log: jest.fn(),
|
|
16
|
+
info: jest.fn(),
|
|
17
|
+
warn: jest.fn(),
|
|
18
|
+
error: jest.fn(),
|
|
19
|
+
trace: jest.fn(),
|
|
20
|
+
};
|
|
21
|
+
const logMetricsSpy = jest.spyOn(metricsLogger, 'logMetrics');
|
|
22
|
+
|
|
23
|
+
const TestComponent: React.FC<TestComponentProps> = (props) => <div>Test Component {props.name}</div>;
|
|
24
|
+
const WrappedComponent = withMetrics<TestComponentProps>(TestComponent, 'TestWidget');
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
jest.useFakeTimers();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
jest.useRealTimers();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should log metrics on mount', () => {
|
|
36
|
+
const mockTime = 1234567890;
|
|
37
|
+
jest.setSystemTime(mockTime);
|
|
38
|
+
|
|
39
|
+
render(<WrappedComponent name="test" />);
|
|
40
|
+
|
|
41
|
+
expect(logMetricsSpy).toHaveBeenCalledWith({
|
|
42
|
+
widgetName: 'TestWidget',
|
|
43
|
+
event: 'WIDGET_MOUNTED',
|
|
44
|
+
timestamp: mockTime,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should log metrics on unmount', () => {
|
|
49
|
+
const mockTime = 1234567890;
|
|
50
|
+
jest.setSystemTime(mockTime);
|
|
51
|
+
|
|
52
|
+
const {unmount} = render(<WrappedComponent name="test" />);
|
|
53
|
+
|
|
54
|
+
// Clear the mount log
|
|
55
|
+
logMetricsSpy.mockClear();
|
|
56
|
+
|
|
57
|
+
// Unmount the component
|
|
58
|
+
unmount();
|
|
59
|
+
|
|
60
|
+
expect(logMetricsSpy).toHaveBeenCalledWith({
|
|
61
|
+
widgetName: 'TestWidget',
|
|
62
|
+
event: 'WIDGET_UNMOUNTED',
|
|
63
|
+
timestamp: mockTime,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should pass through props to wrapped component', () => {
|
|
68
|
+
const {getByText} = render(<WrappedComponent name="test-name" />);
|
|
69
|
+
expect(getByText('Test Component test-name')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should not re-render when props have not changed', () => {
|
|
73
|
+
const renderSpy = jest.fn();
|
|
74
|
+
const SpyComponent: React.FC<TestComponentProps> = (props) => {
|
|
75
|
+
renderSpy();
|
|
76
|
+
return <div>Test Component {props.name}</div>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const WrappedSpy = withMetrics<TestComponentProps>(SpyComponent, 'TestWidget');
|
|
80
|
+
|
|
81
|
+
const {rerender} = render(<WrappedSpy name="test" />);
|
|
82
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
83
|
+
|
|
84
|
+
// Re-render with same props
|
|
85
|
+
rerender(<WrappedSpy name="test" />);
|
|
86
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should re-render when props have changed', () => {
|
|
90
|
+
const renderSpy = jest.fn();
|
|
91
|
+
const SpyComponent: React.FC<TestComponentProps> = (props) => {
|
|
92
|
+
renderSpy();
|
|
93
|
+
return <div>Test Component {props.name}</div>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const WrappedSpy = withMetrics<TestComponentProps>(SpyComponent, 'TestWidget');
|
|
97
|
+
|
|
98
|
+
const {rerender} = render(<WrappedSpy name="test" />);
|
|
99
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
100
|
+
|
|
101
|
+
// Re-render with different props
|
|
102
|
+
rerender(<WrappedSpy name="different" />);
|
|
103
|
+
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
104
|
+
});
|
|
105
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const {merge} = require('webpack-merge');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const baseConfig = require('../../../webpack.config');
|
|
5
|
+
|
|
6
|
+
module.exports = merge(baseConfig, {
|
|
7
|
+
output: {
|
|
8
|
+
path: path.resolve(__dirname, 'dist'),
|
|
9
|
+
filename: 'index.js', // Set the output filename to index.js
|
|
10
|
+
libraryTarget: 'commonjs2',
|
|
11
|
+
},
|
|
12
|
+
externals: {
|
|
13
|
+
react: 'react',
|
|
14
|
+
'react-dom': 'react-dom',
|
|
15
|
+
'@webex/cc-store': '@webex/cc-store',
|
|
16
|
+
},
|
|
17
|
+
});
|