From 39c0b61e9d08fad1577421adc152cc85fd2a1b96 Mon Sep 17 00:00:00 2001 From: Ryan Thenhaus <73962982+rc10house@users.noreply.github.com> Date: Wed, 15 May 2024 12:49:27 -0500 Subject: [PATCH] Pre-commit hooks, removed pretest from npm test (#454) Co-authored-by: rthenhaus --- .eslintrc.json | 10 +- .husky/pre-commit | 49 ++ eslint.config.mjs | 10 + package-lock.json | 453 ++++++++++++++- package.json | 19 +- src/UserALEWebExtension/README.md | 89 +-- src/UserALEWebExtension/background.js | 62 +- src/UserALEWebExtension/content.js | 18 +- src/UserALEWebExtension/globals.js | 58 +- src/UserALEWebExtension/manifest.json | 13 +- src/UserALEWebExtension/messageTypes.js | 38 +- src/UserALEWebExtension/options.js | 143 ++--- src/UserALEWebExtension/optionsPage.html | 62 +- src/UserALEWebExtension/public/index.html | 14 +- src/attachHandlers.js | 222 ++++--- src/configure.js | 28 +- src/getInitialSettings.js | 152 ++--- src/main.js | 119 ++-- src/packageLogs.js | 342 ++++++----- src/sendLogs.js | 20 +- src/utils/auth/index.js | 12 +- src/utils/headers/index.js | 16 +- src/utils/index.js | 24 +- test/attachHandlers_spec.js | 75 ++- test/auth_spec.js | 236 ++++---- test/configure_spec.js | 52 +- test/getInitialSettings_fetchAll.html | 33 +- test/getInitialSettings_spec.js | 126 ++-- test/getInitialSettings_userParam.html | 19 +- test/headers_spec.js | 264 +++++---- test/main.html | 14 +- test/main_spec.js | 144 ++--- test/packageLogs_spec.js | 673 +++++++++++----------- test/sendLogs_spec.js | 373 ++++++------ test/testUtils.js | 36 +- 35 files changed, 2366 insertions(+), 1652 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 eslint.config.mjs diff --git a/.eslintrc.json b/.eslintrc.json index ebded62a..3757175c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,13 +5,15 @@ "modules" : true } }, + "plugins": ["@typescript-eslint"], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], "env" : { "browser" : true, "es6" : true }, - "extends" : "eslint:recommended", "rules" : { - "no-cond-assign" : 0, - "no-constant-condition" : 0 - } + "no-cond-assign" : "warn", + "no-constant-condition" : "warn", + "@typescript-eslint/no-unused-vars": "warn", + }, } diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..f3d8dc63 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,49 @@ +bash << EOF + sound() { + echo '\a' + } + + echo "Running pre-commit hooks..." + +# Check Prettier standards + npm run format || + ( + sound + echo "❌ Prettier Check Failed. Run npm run format, add changes and try commit again."; + exit 1; + ) + +# Check ESLint Standards + npm run lint || + ( + sound + echo "❌ ESLint Check Failed. Make the required changes listed above, add changes and try to commit again." + exit 1; + ) + +# TODO: add typescript checks + +# If everything passes... Now we can commit + echo "✅ Checks passed, trying to build..." + + "npm" run build || + ( + sound + echo "❌ Build failed, check errors." + exit 1; + ) + + echo "✅ Successful build, running tests..." +# After build, run unit tests +# Right now, runs all tests. Later scope to just unit tets, we can add e2e/integration as github actions on merge + npm run test || + ( + sound + echo "❌ Tests failed: View the logs to see what broke and fix it before re-committing." + exit 1; + ) + +# If everything passes... Now we can commit + echo '✅ All tests passed' + +EOF diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..d76d825c --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,10 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/package-lock.json b/package-lock.json index 0588c941..efaab2ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,29 +14,37 @@ "@babel/plugin-transform-runtime": "^7.23.4", "@babel/preset-env": "^7.23.5", "@babel/register": "^7.22.15", + "@eslint/js": "^9.2.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.0.1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", + "@typescript-eslint/eslint-plugin": "^7.8.0", + "@typescript-eslint/parser": "^7.8.0", "body-parser": "^1.20.2", "chai": "^4.3.10", "chai-subset": "^1.6.0", "cypress": "^13.6.0", "detect-browser": "^5.3.0", "dom-storage": "^2.1.0", - "eslint": "^8.55.0", + "eslint": "^8.57.0", "express": "^4.18.2", "global-jsdom": "^24.0.0", + "globals": "^15.2.0", + "husky": "^9.0.11", "jsdom": "^24.0.0", "jsonschema": "^1.4.1", "mocha": "^10.2.0", "nodemon": "^3.0.2", + "prettier": "^3.2.5", "rollup": "^4.6.1", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-license": "^3.2.0", "sinon": "^17.0.1", - "start-server-and-test": "^2.0.3" + "start-server-and-test": "^2.0.3", + "typescript": "^5.4.5", + "typescript-eslint": "^7.8.0" }, "engines": { "node": "^18.x || ^20.x", @@ -908,6 +916,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", @@ -1785,6 +1802,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", @@ -1947,12 +1973,12 @@ } }, "node_modules/@eslint/js": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", - "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.2.0.tgz", + "integrity": "sha512-ESiIudvhoYni+MdsI8oD7skpprZ89qKocwRM2KEvhhBJ9nl5MRh7BXU5GTod7Mdygq+AUl+QzId6iWJKR/wABA==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@hapi/hoek": { @@ -1971,13 +1997,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -1998,9 +2024,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "node_modules/@jridgewell/gen-mapping": { @@ -2534,6 +2560,12 @@ "@types/node": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -2555,6 +2587,12 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", @@ -2577,6 +2615,276 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", + "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/type-utils": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", + "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", + "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", + "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", + "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "semver": "^7.6.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", + "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -4046,16 +4354,16 @@ } }, "node_modules/eslint": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", - "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.55.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -4128,6 +4436,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4920,12 +5237,15 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.2.0.tgz", + "integrity": "sha512-FQ5YwCHZM3nCmtb5FzEWwdUc9K5d3V/w9mzcz8iGD1gC/aOTHc6PouYu0kkKipNJqHAT7m51sqzQjEjIP+cK0A==", "dev": true, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globby": { @@ -5134,6 +5454,21 @@ "node": ">=8.12.0" } }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5167,9 +5502,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -6843,6 +7178,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -8137,6 +8487,18 @@ "node": ">=18" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -8207,6 +8569,45 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.8.0.tgz", + "integrity": "sha512-sheFG+/D8N/L7gC3WT0Q8sB97Nm573Yfr+vZFzl/4nBdYcmviBPtwGSX9TJ7wpVg28ocerKVOt+k2eGmHzcgVA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "7.8.0", + "@typescript-eslint/parser": "7.8.0", + "@typescript-eslint/utils": "7.8.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/package.json b/package.json index e2e2950c..a24a6ceb 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,17 @@ "description": "UserALE.js is the UserALE client for DOM and JavaScript-based applications. It automatically attaches event handlers to log every user interaction on a web page, including rich JS single-page apps.", "main": "build/userale-2.4.0.js", "scripts": { + "format": "prettier --ignore-path .gitignore --write src/ test/", "lint": "eslint ./src --fix", - "pretest": "npm run lint && npm run clean && npm run build", "test": "mocha --require @babel/register && npm run journey:ci", - "build": "rollup -c --bundleConfigAsCjs rollup.config.js", + "build": "npm run clean && rollup -c --bundleConfigAsCjs rollup.config.js", "clean": "rm -rf ./build", "journey": "cypress run", "journey:debug": "cypress open", "journey:ci": "start-server-and-test example:run 8000 journey", "example:run": "node example/server.js", - "example:watch": "nodemon -w ./example example/server.js" + "example:watch": "nodemon -w ./example example/server.js", + "prepare": "husky" }, "repository": { "type": "git", @@ -54,28 +55,36 @@ "@babel/plugin-transform-runtime": "^7.23.4", "@babel/preset-env": "^7.23.5", "@babel/register": "^7.22.15", + "@eslint/js": "^9.2.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.0.1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", + "@typescript-eslint/eslint-plugin": "^7.8.0", + "@typescript-eslint/parser": "^7.8.0", "body-parser": "^1.20.2", "chai": "^4.3.10", "chai-subset": "^1.6.0", "cypress": "^13.6.0", "detect-browser": "^5.3.0", "dom-storage": "^2.1.0", - "eslint": "^8.55.0", + "eslint": "^8.57.0", "express": "^4.18.2", "global-jsdom": "^24.0.0", + "globals": "^15.2.0", + "husky": "^9.0.11", "jsdom": "^24.0.0", "jsonschema": "^1.4.1", "mocha": "^10.2.0", "nodemon": "^3.0.2", + "prettier": "^3.2.5", "rollup": "^4.6.1", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-license": "^3.2.0", "sinon": "^17.0.1", - "start-server-and-test": "^2.0.3" + "start-server-and-test": "^2.0.3", + "typescript": "^5.4.5", + "typescript-eslint": "^7.8.0" } } diff --git a/src/UserALEWebExtension/README.md b/src/UserALEWebExtension/README.md index 0f1b3092..2514a27f 100644 --- a/src/UserALEWebExtension/README.md +++ b/src/UserALEWebExtension/README.md @@ -16,6 +16,7 @@ specific language governing permissions and limitations under the License. --> + # UserALE Web Extension The UserALE.js Web Extension is designed to enable user activity logging across any page they visit, regardless of domain. To achieve this, we repackaged our UserALE.js library into a WebExtension compliant format to allow portability between browsers. @@ -24,56 +25,55 @@ The UserALE.js Web Extension is designed to enable user activity logging across Here is a rundown of the UserALE Extension hierarchy. -* ../ - * The parent directory should be the src for the core of UserALE -* README.md - * You're looking at me! -* icons/ - * Used by the web extension to load icon resources. -* public/ - * Used for sample/test webpages and resources. Not actually part of the Firefox plugin! Used by NPM's http-server module (and other web servers). -* globals.js - * Holds default values for the web extension's options. -* manifest.json - * The main web extension project file, where it all begins. -* options.js - * JavaScript code used by the web extension's options page. -* optionsPage.html - * HTML for the options page. -* user-ale-ext.js - * The JavaScript code used by the web extension. +- ../ + - The parent directory should be the src for the core of UserALE +- README.md + - You're looking at me! +- icons/ + - Used by the web extension to load icon resources. +- public/ + - Used for sample/test webpages and resources. Not actually part of the Firefox plugin! Used by NPM's http-server module (and other web servers). +- globals.js + - Holds default values for the web extension's options. +- manifest.json + - The main web extension project file, where it all begins. +- options.js + - JavaScript code used by the web extension's options page. +- optionsPage.html + - HTML for the options page. +- user-ale-ext.js + - The JavaScript code used by the web extension. ## Quick Start 1. You need to have a UserALE server running; one way is to clone the UserALE repository, run the build, and then start the included test server. - 1. git clone https://github.com/apache/incubator-flagon-useralejs.git - 1. cd incubator-flagon-useralejs - 1. npm install && npm run build - 1. npm run example:watch - 1. A UserALE logging server should now be running on http://localhost:8000 + 1. git clone https://github.com/apache/incubator-flagon-useralejs.git + 1. cd incubator-flagon-useralejs + 1. npm install && npm run build + 1. npm run example:watch + 1. A UserALE logging server should now be running on http://localhost:8000 1. Load the web extension into your browser. - 1. Firefox - 1. Open Firefox - 1. Enter about:debugging into the URL bar and press enter - 1. Check the box to 'Enable add-on debugging' - 1. Press the 'Load Temporary Add-on' button - 1. Navigate to the root of the web extension directory (e.g. 'build/UserAleWebExtension') - 1. Press Open, and confirm that 'User ALE Extension' appears in the list - 1. You may now navigate to a web page to inject the User ALE script! (e.g. http://localhost:8080) - 1. Chrome - 1. Open Chrome - 1. Enter chrome://extensions into the URL bar and press enter - 1. Check the 'Developer mode' box - 1. Press the 'Load unpacked extension' button - 1. Navigate to the root of the build directory (e.g. 'build/UserAleWebExtension') - 1. Press Ok, and confirm that 'UserALE Extension' appears in the list - 1. You may now navigate to a web page to inject the User ALE script! (e.g. http://localhost:8080) - - + 1. Firefox + 1. Open Firefox + 1. Enter about:debugging into the URL bar and press enter + 1. Check the box to 'Enable add-on debugging' + 1. Press the 'Load Temporary Add-on' button + 1. Navigate to the root of the web extension directory (e.g. 'build/UserAleWebExtension') + 1. Press Open, and confirm that 'User ALE Extension' appears in the list + 1. You may now navigate to a web page to inject the User ALE script! (e.g. http://localhost:8080) + 1. Chrome + 1. Open Chrome + 1. Enter chrome://extensions into the URL bar and press enter + 1. Check the 'Developer mode' box + 1. Press the 'Load unpacked extension' button + 1. Navigate to the root of the build directory (e.g. 'build/UserAleWebExtension') + 1. Press Ok, and confirm that 'UserALE Extension' appears in the list + 1. You may now navigate to a web page to inject the User ALE script! (e.g. http://localhost:8080) + ## Options You can set options for the web extension in your browser by opening the extensions page, finding the extension, and choosing either "Preferences" for Firefox, or "Options" for Chrome. - + ## Updating UserALE client script This version of the web extension has been modified to automatically reflect the correct version of the UserALE core script during the build process. You should not need to change anything for it to "just work". @@ -85,5 +85,6 @@ However, if something appears wrong, you can look at the 'src/UserAleWebExtensio There is a known issue when attemping to gather logs from a page running on HTTPS. This occurs due to Mixed Active Content rules in the browser, since the current implementation of the Extension injects the script as HTTP. We are aware of the problem and are actively working towards a fix. In the meantime, the only workaround is to disable the related security option in the browser: -* [Chrome](https://superuser.com/questions/487748/how-to-allow-chrome-browser-to-load-insecure-content) -* [Firefox](https://support.mozilla.org/en-US/kb/mixed-content-blocking-firefox) \ No newline at end of file + +- [Chrome](https://superuser.com/questions/487748/how-to-allow-chrome-browser-to-load-insecure-content) +- [Firefox](https://support.mozilla.org/en-US/kb/mixed-content-blocking-firefox) diff --git a/src/UserALEWebExtension/background.js b/src/UserALEWebExtension/background.js index 0516727d..98b7e436 100644 --- a/src/UserALEWebExtension/background.js +++ b/src/UserALEWebExtension/background.js @@ -19,23 +19,23 @@ eslint-disable */ -import * as MessageTypes from './messageTypes.js'; -import * as userale from '../main.js'; -import { browser } from './globals.js'; +import * as MessageTypes from "./messageTypes.js"; +import * as userale from "../main.js"; +import { browser } from "./globals.js"; // Initalize userale plugin options const defaultConfig = { useraleConfig: { - url: 'http://localhost:8000', - userId: 'pluginUser', + url: "http://localhost:8000", + userId: "pluginUser", authHeader: null, - toolName: 'useralePlugin', + toolName: "useralePlugin", version: userale.version, }, pluginConfig: { // Default to a regex that will match no string - urlWhitelist: '(?!x)x' - } + urlWhitelist: "(?!x)x", + }, }; var urlWhitelist; @@ -73,8 +73,8 @@ function dispatchTabMessage(message) { * @return {Object} The transformed log */ function filterUrl(log) { - if(urlWhitelist.test(log.pageUrl)) { - return log + if (urlWhitelist.test(log.pageUrl)) { + return log; } return false; } @@ -85,21 +85,23 @@ function filterUrl(log) { * @return {Object} The transformed log */ function injectSessions(log) { - let id = log.details.id; - if(id in tabToHttpSession) { - log.httpSessionId = tabToHttpSession[id]; - } else { - log.httpSessionId = null - } - log.browserSessionId = browserSessionId; - return log; + let id = log.details.id; + if (id in tabToHttpSession) { + log.httpSessionId = tabToHttpSession[id]; + } else { + log.httpSessionId = null; + } + log.browserSessionId = browserSessionId; + return log; } browser.storage.local.get(defaultConfig, (res) => { // Apply url filter to logs generated by the background page. - userale.addCallbacks({filterUrl, injectSessions}); + userale.addCallbacks({ filterUrl, injectSessions }); updateConfig(res); - browserSessionId = JSON.parse(window.sessionStorage.getItem('userAleHttpSessionId')); + browserSessionId = JSON.parse( + window.sessionStorage.getItem("userAleHttpSessionId"), + ); }); browser.runtime.onMessage.addListener(function (message, sender, sendResponse) { @@ -110,13 +112,13 @@ browser.runtime.onMessage.addListener(function (message, sender, sendResponse) { log.browserSessionId = browserSessionId; // Apply url filter to logs generated outside the background page. log = filterUrl(log); - if(log) { + if (log) { userale.log(log); } break; case MessageTypes.HTTP_SESSION: - if("tab" in sender && "id" in sender.tab) { + if ("tab" in sender && "id" in sender.tab) { tabToHttpSession[sender.tab.id] = message.payload; } break; @@ -126,7 +128,7 @@ browser.runtime.onMessage.addListener(function (message, sender, sendResponse) { break; default: - console.log('got unknown message type ', message); + console.log("got unknown message type ", message); } }); @@ -151,8 +153,14 @@ function packageTabLog(tabId, data, type) { * @return {undefined} */ function packageDetailedTabLog(tab, data, type) { - Object.assign(data, {type}); - userale.packageCustomLog(data, ()=>{return tab}, true); + Object.assign(data, { type }); + userale.packageCustomLog( + data, + () => { + return tab; + }, + true, + ); } // Attach Handlers for tab events @@ -178,7 +186,7 @@ browser.tabs.onMoved.addListener((tabId, moveInfo) => { }); browser.tabs.onRemoved.addListener((tabId, removeInfo) => { - packageDetailedTabLog({id: tabId}, removeInfo, "tabs.onRemoved"); + packageDetailedTabLog({ id: tabId }, removeInfo, "tabs.onRemoved"); }); browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { @@ -191,4 +199,4 @@ browser.tabs.onZoomChange.addListener((ZoomChangeInfo) => { /* eslint-enable - */ \ No newline at end of file + */ diff --git a/src/UserALEWebExtension/content.js b/src/UserALEWebExtension/content.js index b0fe3937..f8d80f29 100644 --- a/src/UserALEWebExtension/content.js +++ b/src/UserALEWebExtension/content.js @@ -17,17 +17,19 @@ /* eslint-disable */ -import * as MessageTypes from './messageTypes.js'; -import * as userale from '../main.js'; -import { rerouteLog, browser } from './globals.js'; +import * as MessageTypes from "./messageTypes.js"; +import * as userale from "../main.js"; +import { rerouteLog, browser } from "./globals.js"; browser.storage.local.get("useraleConfig", (res) => { userale.options(res.useraleConfig); - userale.addCallbacks({rerouteLog}); - + userale.addCallbacks({ rerouteLog }); + // Send httpSession to background scirpt to inject into tab events. - let payload = JSON.parse(window.sessionStorage.getItem('userAleHttpSessionId')); - browser.runtime.sendMessage({type: MessageTypes.HTTP_SESSION, payload}); + let payload = JSON.parse( + window.sessionStorage.getItem("userAleHttpSessionId"), + ); + browser.runtime.sendMessage({ type: MessageTypes.HTTP_SESSION, payload }); }); browser.runtime.onMessage.addListener(function (message) { @@ -38,4 +40,4 @@ browser.runtime.onMessage.addListener(function (message) { /* eslint-enable - */ \ No newline at end of file + */ diff --git a/src/UserALEWebExtension/globals.js b/src/UserALEWebExtension/globals.js index 1f5d8440..86af51b1 100644 --- a/src/UserALEWebExtension/globals.js +++ b/src/UserALEWebExtension/globals.js @@ -1,29 +1,29 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - -/* eslint-disable */ -import * as MessageTypes from './messageTypes.js'; - -// browser is defined in firefox, but chrome uses the 'chrome' global. -export var browser = browser || chrome; - -export function rerouteLog(log) { - browser.runtime.sendMessage({ type: MessageTypes.ADD_LOG, payload: log }); - return false; -} - -/* eslint-enable */ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +import * as MessageTypes from "./messageTypes.js"; + +// browser is defined in firefox, but chrome uses the 'chrome' global. +export var browser = browser || chrome; + +export function rerouteLog(log) { + browser.runtime.sendMessage({ type: MessageTypes.ADD_LOG, payload: log }); + return false; +} + +/* eslint-enable */ diff --git a/src/UserALEWebExtension/manifest.json b/src/UserALEWebExtension/manifest.json index ca26fdcc..8c508e89 100644 --- a/src/UserALEWebExtension/manifest.json +++ b/src/UserALEWebExtension/manifest.json @@ -6,20 +6,13 @@ "icons": { "48": "icons/border-48.png" }, - "permissions": [ - "activeTab", - "storage", - "tabs", - "" - ], + "permissions": ["activeTab", "storage", "tabs", ""], "background": { "scripts": ["background.js"] }, "content_scripts": [ { - "matches": [ - "" - ], + "matches": [""], "js": ["content.js"], "all_frames": true } @@ -27,4 +20,4 @@ "options_ui": { "page": "optionsPage.html" } -} \ No newline at end of file +} diff --git a/src/UserALEWebExtension/messageTypes.js b/src/UserALEWebExtension/messageTypes.js index 6d553c9a..1328c8a3 100644 --- a/src/UserALEWebExtension/messageTypes.js +++ b/src/UserALEWebExtension/messageTypes.js @@ -1,22 +1,22 @@ /* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -const prefix = 'USERALE_'; +const prefix = "USERALE_"; -export const CONFIG_CHANGE = prefix + 'CONFIG_CHANGE'; -export const ADD_LOG = prefix + 'ADD_LOG'; -export const HTTP_SESSION = prefix + 'HTTP_SESSION'; +export const CONFIG_CHANGE = prefix + "CONFIG_CHANGE"; +export const ADD_LOG = prefix + "ADD_LOG"; +export const HTTP_SESSION = prefix + "HTTP_SESSION"; diff --git a/src/UserALEWebExtension/options.js b/src/UserALEWebExtension/options.js index caa442a6..53139bb0 100644 --- a/src/UserALEWebExtension/options.js +++ b/src/UserALEWebExtension/options.js @@ -1,71 +1,72 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - -/* eslint-disable */ -import * as MessageTypes from './messageTypes.js'; -import * as userale from '../main.js' -import { rerouteLog, browser } from './globals.js'; - -userale.addCallbacks({rerouteLog}); - -// TODO: Warn users when setting credentials with unsecured connection. -const mitmWarning = "Setting credentials with http will expose you to a MITM attack. Are you sure you want to continue?"; - -function setConfig() { - let config = { - url: document.getElementById("url").value, - userId: document.getElementById("user").value, - toolName: document.getElementById("tool").value, - version: document.getElementById("version").value - }; - - // Set a basic auth header if given credentials. - const password = document.getElementById("password").value; - if(config.userId && password) { - config.authHeader = "Basic " + btoa(`${config.userId}:${password}`); - } - - let payload = { - useraleConfig: config, - pluginConfig: {urlWhitelist: document.getElementById("filter").value} - }; - - browser.storage.local.set(payload, () => { - userale.options(config); - browser.runtime.sendMessage({ type: MessageTypes.CONFIG_CHANGE, payload }); - }); -} - -function getConfig() { - browser.storage.local.get("useraleConfig", (res) => { - let config = res.useraleConfig; - - userale.options(config); - document.getElementById("url").value = config.url; - document.getElementById("user").value = config.userId; - document.getElementById("tool").value = config.toolName; - document.getElementById("version").value = config.version; - }); - browser.storage.local.get("pluginConfig", (res) => { - document.getElementById("filter").value = res.pluginConfig.urlWhitelist; - }); -} - -document.addEventListener("DOMContentLoaded", getConfig); -document.addEventListener("submit", setConfig); - -/* eslint-enable */ \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +import * as MessageTypes from "./messageTypes.js"; +import * as userale from "../main.js"; +import { rerouteLog, browser } from "./globals.js"; + +userale.addCallbacks({ rerouteLog }); + +// TODO: Warn users when setting credentials with unsecured connection. +const mitmWarning = + "Setting credentials with http will expose you to a MITM attack. Are you sure you want to continue?"; + +function setConfig() { + let config = { + url: document.getElementById("url").value, + userId: document.getElementById("user").value, + toolName: document.getElementById("tool").value, + version: document.getElementById("version").value, + }; + + // Set a basic auth header if given credentials. + const password = document.getElementById("password").value; + if (config.userId && password) { + config.authHeader = "Basic " + btoa(`${config.userId}:${password}`); + } + + let payload = { + useraleConfig: config, + pluginConfig: { urlWhitelist: document.getElementById("filter").value }, + }; + + browser.storage.local.set(payload, () => { + userale.options(config); + browser.runtime.sendMessage({ type: MessageTypes.CONFIG_CHANGE, payload }); + }); +} + +function getConfig() { + browser.storage.local.get("useraleConfig", (res) => { + let config = res.useraleConfig; + + userale.options(config); + document.getElementById("url").value = config.url; + document.getElementById("user").value = config.userId; + document.getElementById("tool").value = config.toolName; + document.getElementById("version").value = config.version; + }); + browser.storage.local.get("pluginConfig", (res) => { + document.getElementById("filter").value = res.pluginConfig.urlWhitelist; + }); +} + +document.addEventListener("DOMContentLoaded", getConfig); +document.addEventListener("submit", setConfig); + +/* eslint-enable */ diff --git a/src/UserALEWebExtension/optionsPage.html b/src/UserALEWebExtension/optionsPage.html index 9a3f363b..c103d007 100644 --- a/src/UserALEWebExtension/optionsPage.html +++ b/src/UserALEWebExtension/optionsPage.html @@ -15,43 +15,43 @@ * limitations under the License. --> - + - + User ALE Web Extension - Options - - - -

Options

-
- - -
+ + + +

Options

+ + + +
- - -
+ + +
- - -
+ + +
- - -
+ + +
- - -
+ + +
- - -
+ + +
-
- -
-
- - \ No newline at end of file +
+ +
+ + + diff --git a/src/UserALEWebExtension/public/index.html b/src/UserALEWebExtension/public/index.html index 7ca3972a..9bf0378f 100644 --- a/src/UserALEWebExtension/public/index.html +++ b/src/UserALEWebExtension/public/index.html @@ -15,12 +15,12 @@ * limitations under the License. --> - + - + Firefox Plugin Test Page - - -Can we use a Firefox plugin to insert UserALE onto this page? - - \ No newline at end of file + + + Can we use a Firefox plugin to insert UserALE onto this page? + + diff --git a/src/attachHandlers.js b/src/attachHandlers.js index e440c995..8471da5b 100644 --- a/src/attachHandlers.js +++ b/src/attachHandlers.js @@ -5,9 +5,9 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,16 +15,24 @@ * limitations under the License. */ -import { packageLog } from './packageLogs.js'; -import { packageIntervalLog} from './packageLogs'; +import { packageLog } from "./packageLogs.js"; +import { packageIntervalLog } from "./packageLogs"; let events; let bufferBools; let bufferedEvents; //@todo: Investigate drag events and their behavior -const intervalEvents = ['click', 'focus', 'blur', 'input', 'change', 'mouseover', 'submit']; +const intervalEvents = [ + "click", + "focus", + "blur", + "input", + "change", + "mouseover", + "submit", +]; let refreshEvents; -const windowEvents = ['load', 'blur', 'focus']; +const windowEvents = ["load", "blur", "focus"]; /** * Maps an event to an object containing useful information. @@ -32,12 +40,12 @@ const windowEvents = ['load', 'blur', 'focus']; */ export function extractMouseEvent(e) { return { - 'clicks' : e.detail, - 'ctrl' : e.ctrlKey, - 'alt' : e.altKey, - 'shift' : e.shiftKey, - 'meta' : e.metaKey, -// 'text' : e.target.innerHTML + clicks: e.detail, + ctrl: e.ctrlKey, + alt: e.altKey, + shift: e.shiftKey, + meta: e.metaKey, + // 'text' : e.target.innerHTML }; } @@ -51,31 +59,55 @@ export function defineDetails(config) { // Keys are event types // Values are functions that return details object if applicable events = { - 'click' : extractMouseEvent, - 'dblclick' : extractMouseEvent, - 'mousedown' : extractMouseEvent, - 'mouseup' : extractMouseEvent, - 'focus' : null, - 'blur' : null, - 'input' : config.logDetails ? function(e) { return { 'value' : e.target.value }; } : null, - 'change' : config.logDetails ? function(e) { return { 'value' : e.target.value }; } : null, - 'dragstart' : null, - 'dragend' : null, - 'drag' : null, - 'drop' : null, - 'keydown' : config.logDetails ? function(e) { return { 'key' : e.keyCode, 'ctrl' : e.ctrlKey, 'alt' : e.altKey, 'shift' : e.shiftKey, 'meta' : e.metaKey }; } : null, - 'mouseover' : null + click: extractMouseEvent, + dblclick: extractMouseEvent, + mousedown: extractMouseEvent, + mouseup: extractMouseEvent, + focus: null, + blur: null, + input: config.logDetails + ? function (e) { + return { value: e.target.value }; + } + : null, + change: config.logDetails + ? function (e) { + return { value: e.target.value }; + } + : null, + dragstart: null, + dragend: null, + drag: null, + drop: null, + keydown: config.logDetails + ? function (e) { + return { + key: e.keyCode, + ctrl: e.ctrlKey, + alt: e.altKey, + shift: e.shiftKey, + meta: e.metaKey, + }; + } + : null, + mouseover: null, }; bufferBools = {}; bufferedEvents = { - 'wheel' : function(e) { return { 'x' : e.deltaX, 'y' : e.deltaY, 'z' : e.deltaZ }; }, - 'scroll' : function() { return { 'x' : window.scrollX, 'y' : window.scrollY }; }, - 'resize' : function() { return { 'width' : window.outerWidth, 'height' : window.outerHeight }; } + wheel: function (e) { + return { x: e.deltaX, y: e.deltaY, z: e.deltaZ }; + }, + scroll: function () { + return { x: window.scrollX, y: window.scrollY }; + }, + resize: function () { + return { width: window.outerWidth, height: window.outerHeight }; + }, }; refreshEvents = { - 'submit' : null + submit: null, }; } @@ -90,24 +122,48 @@ export function defineCustomDetails(options, type) { // Keys are event types // Values are functions that return details object if applicable const eventType = { - 'click' : extractMouseEvent, - 'dblclick' : extractMouseEvent, - 'mousedown' : extractMouseEvent, - 'mouseup' : extractMouseEvent, - 'focus' : null, - 'blur' : null, - 'input' : options.logDetails ? function(e) { return { 'value' : e.target.value }; } : null, - 'change' : options.logDetails ? function(e) { return { 'value' : e.target.value }; } : null, - 'dragstart' : null, - 'dragend' : null, - 'drag' : null, - 'drop' : null, - 'keydown' : options.logDetails ? function(e) { return { 'key' : e.keyCode, 'ctrl' : e.ctrlKey, 'alt' : e.altKey, 'shift' : e.shiftKey, 'meta' : e.metaKey }; } : null, - 'mouseover' : null, - 'wheel' : function(e) { return { 'x' : e.deltaX, 'y' : e.deltaY, 'z' : e.deltaZ }; }, - 'scroll' : function() { return { 'x' : window.scrollX, 'y' : window.scrollY }; }, - 'resize' : function() { return { 'width' : window.outerWidth, 'height' : window.outerHeight }; }, - 'submit' : null + click: extractMouseEvent, + dblclick: extractMouseEvent, + mousedown: extractMouseEvent, + mouseup: extractMouseEvent, + focus: null, + blur: null, + input: options.logDetails + ? function (e) { + return { value: e.target.value }; + } + : null, + change: options.logDetails + ? function (e) { + return { value: e.target.value }; + } + : null, + dragstart: null, + dragend: null, + drag: null, + drop: null, + keydown: options.logDetails + ? function (e) { + return { + key: e.keyCode, + ctrl: e.ctrlKey, + alt: e.altKey, + shift: e.shiftKey, + meta: e.metaKey, + }; + } + : null, + mouseover: null, + wheel: function (e) { + return { x: e.deltaX, y: e.deltaY, z: e.deltaZ }; + }, + scroll: function () { + return { x: window.scrollX, y: window.scrollY }; + }, + resize: function () { + return { width: window.outerWidth, height: window.outerHeight }; + }, + submit: null, }; return eventType[type]; } @@ -120,40 +176,64 @@ export function defineCustomDetails(options, type) { export function attachHandlers(config) { defineDetails(config); - Object.keys(events).forEach(function(ev) { - document.addEventListener(ev, function(e) { - packageLog(e, events[ev]); - }, true); + Object.keys(events).forEach(function (ev) { + document.addEventListener( + ev, + function (e) { + packageLog(e, events[ev]); + }, + true, + ); }); - intervalEvents.forEach(function(ev) { - document.addEventListener(ev, function(e) { + intervalEvents.forEach(function (ev) { + document.addEventListener( + ev, + function (e) { packageIntervalLog(e); - }, true); + }, + true, + ); }); - Object.keys(bufferedEvents).forEach(function(ev) { + Object.keys(bufferedEvents).forEach(function (ev) { bufferBools[ev] = true; - window.addEventListener(ev, function(e) { - if (bufferBools[ev]) { - bufferBools[ev] = false; - packageLog(e, bufferedEvents[ev]); - setTimeout(function() { bufferBools[ev] = true; }, config.resolution); - } - }, true); + window.addEventListener( + ev, + function (e) { + if (bufferBools[ev]) { + bufferBools[ev] = false; + packageLog(e, bufferedEvents[ev]); + setTimeout(function () { + bufferBools[ev] = true; + }, config.resolution); + } + }, + true, + ); }); - Object.keys(refreshEvents).forEach(function(ev) { - document.addEventListener(ev, function(e) { - packageLog(e, events[ev]); - }, true); + Object.keys(refreshEvents).forEach(function (ev) { + document.addEventListener( + ev, + function (e) { + packageLog(e, events[ev]); + }, + true, + ); }); - windowEvents.forEach(function(ev) { - window.addEventListener(ev, function(e) { - packageLog(e, function() { return { 'window' : true }; }); - }, true); + windowEvents.forEach(function (ev) { + window.addEventListener( + ev, + function (e) { + packageLog(e, function () { + return { window: true }; + }); + }, + true, + ); }); return true; diff --git a/src/configure.js b/src/configure.js index 8f10c8f6..79b5f9a0 100644 --- a/src/configure.js +++ b/src/configure.js @@ -5,9 +5,9 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,19 +22,19 @@ * @param {Object} newConfig Configuration object to merge into the current config. */ export function configure(config, newConfig) { - const configAutostart = config['autostart']; - const newConfigAutostart = newConfig['autostart']; + const configAutostart = config["autostart"]; + const newConfigAutostart = newConfig["autostart"]; Object.keys(newConfig).forEach(function (option) { - if (option === 'userFromParams') { - const userId = getUserIdFromParams(newConfig[option]); - if (userId) { - config.userId = userId; - } + if (option === "userFromParams") { + const userId = getUserIdFromParams(newConfig[option]); + if (userId) { + config.userId = userId; } - config[option] = newConfig[option]; - }); + } + config[option] = newConfig[option]; + }); if (configAutostart === false || newConfigAutostart === false) { - config['autostart'] = false; + config["autostart"] = false; } } @@ -45,11 +45,11 @@ export function configure(config, newConfig) { */ export function getUserIdFromParams(param) { const userField = param; - const regex = new RegExp('[?&]' + userField + '(=([^&#]*)|&|#|$)'); + const regex = new RegExp("[?&]" + userField + "(=([^&#]*)|&|#|$)"); const results = window.location.href.match(regex); if (results && results[2]) { - return decodeURIComponent(results[2].replace(/\+/g, ' ')); + return decodeURIComponent(results[2].replace(/\+/g, " ")); } else { return null; } diff --git a/src/getInitialSettings.js b/src/getInitialSettings.js index 99f35f02..3e3f11de 100644 --- a/src/getInitialSettings.js +++ b/src/getInitialSettings.js @@ -5,9 +5,9 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the 'License'); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,42 +24,52 @@ let httpSessionId = null; * @return {Object} The extracted configuration object */ export function getInitialSettings() { - const settings = {}; + const settings = {}; - if (sessionId === null) { - sessionId = getSessionId('userAleSessionId', 'session_' + String(Date.now())); - } + if (sessionId === null) { + sessionId = getSessionId( + "userAleSessionId", + "session_" + String(Date.now()), + ); + } - if (httpSessionId === null) { - httpSessionId = getSessionId('userAleHttpSessionId', generateHttpSessionId()); - } + if (httpSessionId === null) { + httpSessionId = getSessionId( + "userAleHttpSessionId", + generateHttpSessionId(), + ); + } - const script = document.currentScript || (function () { - const scripts = document.getElementsByTagName('script'); - return scripts[scripts.length - 1]; + const script = + document.currentScript || + (function () { + const scripts = document.getElementsByTagName("script"); + return scripts[scripts.length - 1]; })(); - const get = script ? script.getAttribute.bind(script) : function () { + const get = script + ? script.getAttribute.bind(script) + : function () { return null; - }; - settings.autostart = get('data-autostart') === 'false' ? false : true; - settings.url = get('data-url') || 'http://localhost:8000'; - settings.transmitInterval = +get('data-interval') || 5000; - settings.logCountThreshold = +get('data-threshold') || 5; - settings.userId = get('data-user') || null; - settings.version = get('data-version') || null; - settings.logDetails = get('data-log-details') === 'true' ? true : false; - settings.resolution = +get('data-resolution') || 500; - settings.toolName = get('data-tool') || null; - settings.userFromParams = get('data-user-from-params') || null; - settings.time = timeStampScale(document.createEvent('CustomEvent')); - settings.sessionID = get('data-session') || sessionId; - settings.httpSessionId = httpSessionId; - settings.browserSessionId = null; - settings.authHeader = get('data-auth') || null; - settings.custIndex = get('data-index') || null; - settings.headers = get('data-headers') || null; - return settings; + }; + settings.autostart = get("data-autostart") === "false" ? false : true; + settings.url = get("data-url") || "http://localhost:8000"; + settings.transmitInterval = +get("data-interval") || 5000; + settings.logCountThreshold = +get("data-threshold") || 5; + settings.userId = get("data-user") || null; + settings.version = get("data-version") || null; + settings.logDetails = get("data-log-details") === "true" ? true : false; + settings.resolution = +get("data-resolution") || 500; + settings.toolName = get("data-tool") || null; + settings.userFromParams = get("data-user-from-params") || null; + settings.time = timeStampScale(document.createEvent("CustomEvent")); + settings.sessionID = get("data-session") || sessionId; + settings.httpSessionId = httpSessionId; + settings.browserSessionId = null; + settings.authHeader = get("data-auth") || null; + settings.custIndex = get("data-index") || null; + settings.headers = get("data-headers") || null; + return settings; } /** @@ -69,12 +79,12 @@ export function getInitialSettings() { * */ export function getSessionId(sessionKey, value) { - if (window.sessionStorage.getItem(sessionKey) === null) { - window.sessionStorage.setItem(sessionKey, JSON.stringify(value)); - return value; - } + if (window.sessionStorage.getItem(sessionKey) === null) { + window.sessionStorage.setItem(sessionKey, JSON.stringify(value)); + return value; + } - return JSON.parse(window.sessionStorage.getItem(sessionKey)); + return JSON.parse(window.sessionStorage.getItem(sessionKey)); } /** @@ -83,36 +93,36 @@ export function getSessionId(sessionKey, value) { * @return {timeStampScale~tsScaler} The timestamp normalizing function. */ export function timeStampScale(e) { - let tsScaler; - if (e.timeStamp && e.timeStamp > 0) { - const delta = Date.now() - e.timeStamp; - /** - * Returns a timestamp depending on various browser quirks. - * @param {?Number} ts A timestamp to use for normalization. - * @return {Number} A normalized timestamp. - */ + let tsScaler; + if (e.timeStamp && e.timeStamp > 0) { + const delta = Date.now() - e.timeStamp; + /** + * Returns a timestamp depending on various browser quirks. + * @param {?Number} ts A timestamp to use for normalization. + * @return {Number} A normalized timestamp. + */ - if (delta < 0) { - tsScaler = function () { - return e.timeStamp / 1000; - }; - } else if (delta > e.timeStamp) { - const navStart = performance.timing.navigationStart; - tsScaler = function (ts) { - return ts + navStart; - } - } else { - tsScaler = function (ts) { - return ts; - } - } + if (delta < 0) { + tsScaler = function () { + return e.timeStamp / 1000; + }; + } else if (delta > e.timeStamp) { + const navStart = performance.timing.navigationStart; + tsScaler = function (ts) { + return ts + navStart; + }; } else { - tsScaler = function () { - return Date.now(); - }; + tsScaler = function (ts) { + return ts; + }; } + } else { + tsScaler = function () { + return Date.now(); + }; + } - return tsScaler; + return tsScaler; } /** @@ -120,13 +130,11 @@ export function timeStampScale(e) { * @return {String} A random 32 digit hex string */ function generateHttpSessionId() { - // 32 digit hex -> 128 bits of info -> 2^64 ~= 10^19 sessions needed for 50% chance of collison - let len = 32; - var arr = new Uint8Array(len / 2); - window.crypto.getRandomValues(arr); - return Array.from(arr, - (dec) => { - return dec.toString(16).padStart(2, "0"); - } - ).join(''); + // 32 digit hex -> 128 bits of info -> 2^64 ~= 10^19 sessions needed for 50% chance of collison + let len = 32; + var arr = new Uint8Array(len / 2); + window.crypto.getRandomValues(arr); + return Array.from(arr, (dec) => { + return dec.toString(16).padStart(2, "0"); + }).join(""); } diff --git a/src/main.js b/src/main.js index 297fab51..6c19c1e2 100644 --- a/src/main.js +++ b/src/main.js @@ -5,9 +5,9 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,33 +15,32 @@ * limitations under the License. */ -import {version as userAleVersion} from '../package.json'; -import {getInitialSettings} from './getInitialSettings.js'; -import {configure} from './configure.js'; -import {attachHandlers} from './attachHandlers.js'; -import {initPackager, packageCustomLog} from './packageLogs.js'; -import {initSender} from './sendLogs.js'; +import { version as userAleVersion } from "../package.json"; +import { getInitialSettings } from "./getInitialSettings.js"; +import { configure } from "./configure.js"; +import { attachHandlers } from "./attachHandlers.js"; +import { initPackager, packageCustomLog } from "./packageLogs.js"; +import { initSender } from "./sendLogs.js"; const config = {}; const logs = []; -const startLoadTimestamp = Date.now() -let endLoadTimestamp +const startLoadTimestamp = Date.now(); +let endLoadTimestamp; window.onload = function () { - endLoadTimestamp = Date.now() -} + endLoadTimestamp = Date.now(); +}; export let started = false; -export {defineCustomDetails as details} from './attachHandlers.js'; -export {registerAuthCallback as registerAuthCallback} from './utils'; +export { defineCustomDetails as details } from "./attachHandlers.js"; +export { registerAuthCallback as registerAuthCallback } from "./utils"; export { - addCallbacks as addCallbacks, - removeCallbacks as removeCallbacks, - packageLog as packageLog, - packageCustomLog as packageCustomLog, - getSelector as getSelector, - buildPath as buildPath, -} from './packageLogs.js'; - + addCallbacks as addCallbacks, + removeCallbacks as removeCallbacks, + packageLog as packageLog, + packageCustomLog as packageCustomLog, + getSelector as getSelector, + buildPath as buildPath, +} from "./packageLogs.js"; // Start up Userale config.on = false; @@ -51,7 +50,7 @@ configure(config, getInitialSettings()); initPackager(logs, config); if (config.autostart) { - setup(config); + setup(config); } /** @@ -60,26 +59,32 @@ if (config.autostart) { * @param {Object} config Configuration settings for the logger */ function setup(config) { - if (!started) { - setTimeout(function () { - const state = document.readyState; - - if (config.autostart && (state === 'interactive' || state === 'complete')) { - attachHandlers(config); - initSender(logs, config); - started = config.on = true; - packageCustomLog({ - type: 'load', - details: {pageLoadTime: endLoadTimestamp - startLoadTimestamp} - }, () => {},false) - } else { - setup(config); - } - }, 100); - } + if (!started) { + setTimeout(function () { + const state = document.readyState; + + if ( + config.autostart && + (state === "interactive" || state === "complete") + ) { + attachHandlers(config); + initSender(logs, config); + started = config.on = true; + packageCustomLog( + { + type: "load", + details: { pageLoadTime: endLoadTimestamp - startLoadTimestamp }, + }, + () => {}, + false, + ); + } else { + setup(config); + } + }, 100); + } } - // Export the Userale API export const version = userAleVersion; @@ -88,18 +93,18 @@ export const version = userAleVersion; * autostart configuration option is set to false. */ export function start() { - if (!started || config.autostart === false) { - started = config.on = true; - config.autostart = true; - } + if (!started || config.autostart === false) { + started = config.on = true; + config.autostart = true; + } } /** * Halts the logging process. Logs will no longer be sent. */ export function stop() { - started = config.on = false; - config.autostart = false; + started = config.on = false; + config.autostart = false; } /** @@ -109,11 +114,11 @@ export function stop() { * @return {Object} Returns the updated configuration. */ export function options(newConfig) { - if (newConfig !== undefined) { - configure(config, newConfig); - } + if (newConfig !== undefined) { + configure(config, newConfig); + } - return config; + return config; } /** @@ -122,10 +127,10 @@ export function options(newConfig) { * @return {boolean} Whether the operation succeeded. */ export function log(customLog) { - if (customLog !== null && typeof customLog === 'object') { - logs.push(customLog); - return true; - } else { - return false; - } + if (customLog !== null && typeof customLog === "object") { + logs.push(customLog); + return true; + } else { + return false; + } } diff --git a/src/packageLogs.js b/src/packageLogs.js index 0a239355..51806bc7 100644 --- a/src/packageLogs.js +++ b/src/packageLogs.js @@ -5,9 +5,9 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,7 +15,7 @@ * limitations under the License. */ -import { detect } from 'detect-browser'; +import { detect } from "detect-browser"; const browserInfo = detect(); export let logs; @@ -39,7 +39,9 @@ export let cbHandlers = {}; * @param {Function} callback The handler to invoke when logging. */ export function setLogFilter(callback) { - console.warn("setLogFilter() is deprecated and will be removed in a futre release"); + console.warn( + "setLogFilter() is deprecated and will be removed in a futre release", + ); filterHandler = callback; } @@ -49,7 +51,9 @@ export function setLogFilter(callback) { * @param {Function} callback The handler to invoke when logging. */ export function setLogMapper(callback) { - console.warn("setLogMapper() is deprecated and will be removed in a futre release"); + console.warn( + "setLogMapper() is deprecated and will be removed in a futre release", + ); mapHandler = callback; } @@ -80,8 +84,8 @@ export function addCallbacks(...newCallbacks) { * @param {String[]} targetKeys A list of names of functions to remove. */ export function removeCallbacks(targetKeys) { - targetKeys.forEach(key => { - if(Object.hasOwn(cbHandlers, key)) { + targetKeys.forEach((key) => { + if (Object.hasOwn(cbHandlers, key)) { delete cbHandlers[key]; } }); @@ -121,45 +125,45 @@ export function packageLog(e, detailFcn) { } const timeFields = extractTimeFields( - (e.timeStamp && e.timeStamp > 0) ? config.time(e.timeStamp) : Date.now() + e.timeStamp && e.timeStamp > 0 ? config.time(e.timeStamp) : Date.now(), ); let log = { - 'target' : getSelector(e.target), - 'path' : buildPath(e), - 'pageUrl': window.location.href, - 'pageTitle': document.title, - 'pageReferrer': document.referrer, - 'browser': detectBrowser(), - 'clientTime' : timeFields.milli, - 'microTime' : timeFields.micro, - 'location' : getLocation(e), - 'scrnRes' : getSreenRes(), - 'type' : e.type, - 'logType': 'raw', - 'userAction' : true, - 'details' : details, - 'userId' : config.userId, - 'toolVersion' : config.version, - 'toolName' : config.toolName, - 'useraleVersion': config.useraleVersion, - 'sessionID': config.sessionID, - 'httpSessionId': config.httpSessionId, - 'browserSessionId': config.browserSessionId, + target: getSelector(e.target), + path: buildPath(e), + pageUrl: window.location.href, + pageTitle: document.title, + pageReferrer: document.referrer, + browser: detectBrowser(), + clientTime: timeFields.milli, + microTime: timeFields.micro, + location: getLocation(e), + scrnRes: getSreenRes(), + type: e.type, + logType: "raw", + userAction: true, + details: details, + userId: config.userId, + toolVersion: config.version, + toolName: config.toolName, + useraleVersion: config.useraleVersion, + sessionID: config.sessionID, + httpSessionId: config.httpSessionId, + browserSessionId: config.browserSessionId, }; - if ((typeof filterHandler === 'function') && !filterHandler(log)) { + if (typeof filterHandler === "function" && !filterHandler(log)) { return false; } - if (typeof mapHandler === 'function') { + if (typeof mapHandler === "function") { log = mapHandler(log, e); } for (const func of Object.values(cbHandlers)) { - if (typeof func === 'function') { + if (typeof func === "function") { log = func(log, e); - if(!log) { + if (!log) { return false; } } @@ -177,56 +181,56 @@ export function packageLog(e, detailFcn) { * @return {boolean} Whether the event was logged. */ export function packageCustomLog(customLog, detailFcn, userAction) { - if (!config.on) { - return false; - } + if (!config.on) { + return false; + } - let details = null; - if (detailFcn) { - details = detailFcn(); - } + let details = null; + if (detailFcn) { + details = detailFcn(); + } - const metaData = { - 'pageUrl': window.location.href, - 'pageTitle': document.title, - 'pageReferrer': document.referrer, - 'browser': detectBrowser(), - 'clientTime' : Date.now(), - 'scrnRes' : getSreenRes(), - 'logType': 'custom', - 'userAction' : userAction, - 'details' : details, - 'userId' : config.userId, - 'toolVersion' : config.version, - 'toolName' : config.toolName, - 'useraleVersion': config.useraleVersion, - 'sessionID': config.sessionID, - 'httpSessionId': config.httpSessionId, - 'browserSessionId': config.browserSessionId, - }; + const metaData = { + pageUrl: window.location.href, + pageTitle: document.title, + pageReferrer: document.referrer, + browser: detectBrowser(), + clientTime: Date.now(), + scrnRes: getSreenRes(), + logType: "custom", + userAction: userAction, + details: details, + userId: config.userId, + toolVersion: config.version, + toolName: config.toolName, + useraleVersion: config.useraleVersion, + sessionID: config.sessionID, + httpSessionId: config.httpSessionId, + browserSessionId: config.browserSessionId, + }; - let log = Object.assign(metaData, customLog); + let log = Object.assign(metaData, customLog); - if ((typeof filterHandler === 'function') && !filterHandler(log)) { - return false; - } + if (typeof filterHandler === "function" && !filterHandler(log)) { + return false; + } - if (typeof mapHandler === 'function') { - log = mapHandler(log); - } + if (typeof mapHandler === "function") { + log = mapHandler(log); + } - for (const func of Object.values(cbHandlers)) { - if (typeof func === 'function') { - log = func(log, null); - if(!log) { - return false; - } + for (const func of Object.values(cbHandlers)) { + if (typeof func === "function") { + log = func(log, null); + if (!log) { + return false; } } + } - logs.push(log); + logs.push(log); - return true; + return true; } /** @@ -248,82 +252,84 @@ export function extractTimeFields(timeStamp) { * @return boolean */ export function packageIntervalLog(e) { - const target = getSelector(e.target); - const path = buildPath(e); - const type = e.type; - const timestamp = Math.floor((e.timeStamp && e.timeStamp > 0) ? config.time(e.timeStamp) : Date.now()); - - // Init - this should only happen once on initialization - if (intervalID == null) { - intervalID = target; - intervalType = type; - intervalPath = path; - intervalTimer = timestamp; - intervalCounter = 0; - } - - if (intervalID !== target || intervalType !== type) { - // When to create log? On transition end - // @todo Possible for intervalLog to not be pushed in the event the interval never ends... - - intervalLog = { - 'target': intervalID, - 'path': intervalPath, - 'pageUrl': window.location.href, - 'pageTitle': document.title, - 'pageReferrer': document.referrer, - 'browser': detectBrowser(), - 'count': intervalCounter, - 'duration': timestamp - intervalTimer, // microseconds - 'startTime': intervalTimer, - 'endTime': timestamp, - 'type': intervalType, - 'logType': 'interval', - 'targetChange': intervalID !== target, - 'typeChange': intervalType !== type, - 'userAction': false, - 'userId': config.userId, - 'toolVersion': config.version, - 'toolName': config.toolName, - 'useraleVersion': config.useraleVersion, - 'sessionID': config.sessionID, - 'httpSessionId': config.httpSessionId, - 'browserSessionId': config.browserSessionId, - }; - - if (typeof filterHandler === 'function' && !filterHandler(intervalLog)) { - return false; - } + const target = getSelector(e.target); + const path = buildPath(e); + const type = e.type; + const timestamp = Math.floor( + e.timeStamp && e.timeStamp > 0 ? config.time(e.timeStamp) : Date.now(), + ); - if (typeof mapHandler === 'function') { - intervalLog = mapHandler(intervalLog, e); - } + // Init - this should only happen once on initialization + if (intervalID == null) { + intervalID = target; + intervalType = type; + intervalPath = path; + intervalTimer = timestamp; + intervalCounter = 0; + } - for (const func of Object.values(cbHandlers)) { - if (typeof func === 'function') { - intervalLog = func(intervalLog, null); - if(!intervalLog) { - return false; - } - } - } + if (intervalID !== target || intervalType !== type) { + // When to create log? On transition end + // @todo Possible for intervalLog to not be pushed in the event the interval never ends... + + intervalLog = { + target: intervalID, + path: intervalPath, + pageUrl: window.location.href, + pageTitle: document.title, + pageReferrer: document.referrer, + browser: detectBrowser(), + count: intervalCounter, + duration: timestamp - intervalTimer, // microseconds + startTime: intervalTimer, + endTime: timestamp, + type: intervalType, + logType: "interval", + targetChange: intervalID !== target, + typeChange: intervalType !== type, + userAction: false, + userId: config.userId, + toolVersion: config.version, + toolName: config.toolName, + useraleVersion: config.useraleVersion, + sessionID: config.sessionID, + httpSessionId: config.httpSessionId, + browserSessionId: config.browserSessionId, + }; - logs.push(intervalLog); + if (typeof filterHandler === "function" && !filterHandler(intervalLog)) { + return false; + } - // Reset - intervalID = target; - intervalType = type; - intervalPath = path; - intervalTimer = timestamp; - intervalCounter = 0; + if (typeof mapHandler === "function") { + intervalLog = mapHandler(intervalLog, e); } - // Interval is still occuring, just update counter - if (intervalID == target && intervalType == type) { - intervalCounter = intervalCounter + 1; + for (const func of Object.values(cbHandlers)) { + if (typeof func === "function") { + intervalLog = func(intervalLog, null); + if (!intervalLog) { + return false; + } + } } - return true; + logs.push(intervalLog); + + // Reset + intervalID = target; + intervalType = type; + intervalPath = path; + intervalTimer = timestamp; + intervalCounter = 0; + } + + // Interval is still occuring, just update counter + if (intervalID == target && intervalType == type) { + intervalCounter = intervalCounter + 1; + } + + return true; } /** @@ -334,11 +340,14 @@ export function packageIntervalLog(e) { */ export function getLocation(e) { if (e.pageX != null) { - return { 'x' : e.pageX, 'y' : e.pageY }; + return { x: e.pageX, y: e.pageY }; } else if (e.clientX != null) { - return { 'x' : document.documentElement.scrollLeft + e.clientX, 'y' : document.documentElement.scrollTop + e.clientY }; + return { + x: document.documentElement.scrollLeft + e.clientX, + y: document.documentElement.scrollTop + e.clientY, + }; } else { - return { 'x' : null, 'y' : null }; + return { x: null, y: null }; } } @@ -347,7 +356,7 @@ export function getLocation(e) { * @return {Object} An object containing the innerWidth and InnerHeight */ export function getSreenRes() { - return { 'width': window.innerWidth, 'height': window.innerHeight}; + return { width: window.innerWidth, height: window.innerHeight }; } /** @@ -357,10 +366,24 @@ export function getSreenRes() { */ export function getSelector(ele) { if (ele.localName) { - return ele.localName + (ele.id ? ('#' + ele.id) : '') + (ele.className ? ('.' + ele.className) : ''); + return ( + ele.localName + + (ele.id ? "#" + ele.id : "") + + (ele.className ? "." + ele.className : "") + ); } else if (ele.nodeName) { - return ele.nodeName + (ele.id ? ('#' + ele.id) : '') + (ele.className ? ('.' + ele.className) : ''); - } else if (ele && ele.document && ele.location && ele.alert && ele.setInterval) { + return ( + ele.nodeName + + (ele.id ? "#" + ele.id : "") + + (ele.className ? "." + ele.className : "") + ); + } else if ( + ele && + ele.document && + ele.location && + ele.alert && + ele.setInterval + ) { return "Window"; } else { return "Unknown"; @@ -373,10 +396,10 @@ export function getSelector(ele) { * @return {HTMLElement[]} Array of elements, starting at the event target, ending at the root element. */ export function buildPath(e) { - if (e instanceof window.Event) { - const path = e.composedPath(); - return selectorizePath(path); - } + if (e instanceof window.Event) { + const path = e.composedPath(); + return selectorizePath(path); + } } /** @@ -388,16 +411,19 @@ export function selectorizePath(path) { let i = 0; let pathEle; const pathSelectors = []; - while (pathEle = path[i]) { + + pathEle = path[i]; + while (pathEle) { pathSelectors.push(getSelector(pathEle)); ++i; + pathEle = path[i]; } return pathSelectors; } export function detectBrowser() { - return { - 'browser': browserInfo ? browserInfo.name : '', - 'version': browserInfo ? browserInfo.version : '' - }; -} \ No newline at end of file + return { + browser: browserInfo ? browserInfo.name : "", + version: browserInfo ? browserInfo.version : "", + }; +} diff --git a/src/sendLogs.js b/src/sendLogs.js index 5985644d..dc0060c7 100644 --- a/src/sendLogs.js +++ b/src/sendLogs.js @@ -62,12 +62,20 @@ export function sendOnInterval(logs, config) { export function sendOnClose(logs, config) { window.addEventListener("pagehide", function () { if (config.on && logs.length > 0) { - // NOTE: sendBeacon does not support auth headers, - // so this will fail if auth is required. - // The alternative is to use fetch() with keepalive: true - // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description - // https://stackoverflow.com/a/73062712/9263449 - navigator.sendBeacon(config.url, JSON.stringify(logs)); + let header_options = config.authHeader + ? { + "Content-Type": "application/json;charset=UTF-8", + Authorization: config.authHeader, + } + : { "content-type": "application/json;charset=UTF-8" }; + + fetch(config.url, { + keepalive: true, + method: "POST", + headers: header_options, + body: JSON.stringify(logs), + }); + logs.splice(0); // clear log queue } }); diff --git a/src/utils/auth/index.js b/src/utils/auth/index.js index cb8099a4..a512b570 100644 --- a/src/utils/auth/index.js +++ b/src/utils/auth/index.js @@ -27,12 +27,12 @@ export let authCallback = null; export function updateAuthHeader(config) { if (authCallback) { try { - config.authHeader = authCallback(); + config.authHeader = authCallback(); } catch (e) { - // We should emit the error, but otherwise continue as this could be a temporary issue - // due to network connectivity or some logic inside the authCallback which is the user's - // responsibility. - console.error(`Error encountered while setting the auth header: ${e}`); + // We should emit the error, but otherwise continue as this could be a temporary issue + // due to network connectivity or some logic inside the authCallback which is the user's + // responsibility. + console.error(`Error encountered while setting the auth header: ${e}`); } } } @@ -75,4 +75,4 @@ export function verifyCallback(callback) { */ export function resetAuthCallback() { authCallback = null; -} \ No newline at end of file +} diff --git a/src/utils/headers/index.js b/src/utils/headers/index.js index e0c599fb..d96cfa9b 100644 --- a/src/utils/headers/index.js +++ b/src/utils/headers/index.js @@ -27,12 +27,12 @@ export let headersCallback = null; export function updateCustomHeaders(config) { if (headersCallback) { try { - config.headers = headersCallback(); + config.headers = headersCallback(); } catch (e) { - // We should emit the error, but otherwise continue as this could be a temporary issue - // due to network connectivity or some logic inside the headersCallback which is the user's - // responsibility. - console.error(`Error encountered while setting the headers: ${e}`); + // We should emit the error, but otherwise continue as this could be a temporary issue + // due to network connectivity or some logic inside the headersCallback which is the user's + // responsibility. + console.error(`Error encountered while setting the headers: ${e}`); } } } @@ -68,7 +68,9 @@ export function verifyCallback(callback) { } for (const [key, value] of Object.entries(result)) { if (typeof key !== "string" || typeof value !== "string") { - throw new Error("Userale header callback must return an object with string keys and values"); + throw new Error( + "Userale header callback must return an object with string keys and values", + ); } } } @@ -80,4 +82,4 @@ export function verifyCallback(callback) { */ export function resetHeadersCallback() { headersCallback = null; -} \ No newline at end of file +} diff --git a/src/utils/index.js b/src/utils/index.js index 243cbbe6..f4726406 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -15,16 +15,16 @@ * limitations under the License. */ export { - authCallback, - updateAuthHeader, - registerAuthCallback, - resetAuthCallback, - verifyCallback as verifyAuthCallback + authCallback, + updateAuthHeader, + registerAuthCallback, + resetAuthCallback, + verifyCallback as verifyAuthCallback, } from "./auth"; -export { - headersCallback, - updateCustomHeaders, - registerHeadersCallback, - resetHeadersCallback, - verifyCallback as verifyHeadersCallback -} from "./headers"; \ No newline at end of file +export { + headersCallback, + updateCustomHeaders, + registerHeadersCallback, + resetHeadersCallback, + verifyCallback as verifyHeadersCallback, +} from "./headers"; diff --git a/test/attachHandlers_spec.js b/test/attachHandlers_spec.js index e63a097c..3dcca372 100644 --- a/test/attachHandlers_spec.js +++ b/test/attachHandlers_spec.js @@ -5,39 +5,66 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import { expect } from 'chai'; -import { attachHandlers } from '../src/attachHandlers'; -import * as packager from '../src/packageLogs'; +import { expect } from "chai"; +import { attachHandlers } from "../src/attachHandlers"; +import * as packager from "../src/packageLogs"; -describe('attachHandlers', () => { - it('attaches all the event handlers without duplicates', (done) => { +describe("attachHandlers", () => { + it("attaches all the event handlers without duplicates", (done) => { let duplicateEvents = 0; const initialDocument = global.document; const initialWindow = global.window; // List of supported document events const missingDocumentEvents = [ - 'click', 'dblclick', 'mousedown', 'mouseup', 'focus', 'blur', - 'input', 'change', 'dragstart', 'dragend', 'drag', 'drop', - 'keydown', 'mouseover', 'submit' + "click", + "dblclick", + "mousedown", + "mouseup", + "focus", + "blur", + "input", + "change", + "dragstart", + "dragend", + "drag", + "drop", + "keydown", + "mouseover", + "submit", ]; // List of supported window events - const missingWindowEvents = ['wheel', 'scroll', 'resize', 'load', 'blur', 'focus']; + const missingWindowEvents = [ + "wheel", + "scroll", + "resize", + "load", + "blur", + "focus", + ]; // List of supported interval events - const missingIntervalEvents = ['click', 'focus', 'blur', 'input', 'change', 'mouseover', 'submit']; + const missingIntervalEvents = [ + "click", + "focus", + "blur", + "input", + "change", + "mouseover", + "submit", + ]; // Acts as a kind of Proxy for addEventListener. Keeps track of added listeners. - const listenerHook = eventList => (ev) => { + const listenerHook = (eventList) => (ev) => { const evIndex = eventList.indexOf(ev); if (evIndex !== -1) { eventList.splice(evIndex, 1); @@ -46,10 +73,14 @@ describe('attachHandlers', () => { } }; - const missingDocumentAndIntervalEvents = missingDocumentEvents.concat(missingIntervalEvents); + const missingDocumentAndIntervalEvents = missingDocumentEvents.concat( + missingIntervalEvents, + ); // MOCK - global.document = { addEventListener: listenerHook(missingDocumentAndIntervalEvents) }; + global.document = { + addEventListener: listenerHook(missingDocumentAndIntervalEvents), + }; global.window = { addEventListener: listenerHook(missingWindowEvents) }; attachHandlers({ logDetails: true }); expect(duplicateEvents).to.equal(0); @@ -61,15 +92,17 @@ describe('attachHandlers', () => { global.window = initialWindow; done(); }); - it('debounces bufferedEvents', (done) => { + it("debounces bufferedEvents", (done) => { let callCount = 0; let testingEvent = false; - const bufferedEvents = ['wheel', 'scroll', 'resize']; + const bufferedEvents = ["wheel", "scroll", "resize"]; const initialWindow = global.window; const initialDocument = global.document; const rate = 500; const initialPackage = packager.packageLog; - packager.packageLog = () => { callCount++; }; + packager.packageLog = () => { + callCount++; + }; global.document = { addEventListener: () => {} }; // Tries to call an event 3 times. Twice in quick succession, then once after the set delay. // Number of actual calls to packageLog are recorded in callCount. Should amount to exactly 2 calls. @@ -88,13 +121,13 @@ describe('attachHandlers', () => { done(); }, rate + 1); } - } + }, }; attachHandlers({ resolution: rate }); }); - describe('defineDetails', () => { + describe("defineDetails", () => { // TODO: clarify what constitutes "high detail events" and what is "correct" - it('configures high detail events correctly'); + it("configures high detail events correctly"); }); }); diff --git a/test/auth_spec.js b/test/auth_spec.js index db9b7c72..9fe1555d 100644 --- a/test/auth_spec.js +++ b/test/auth_spec.js @@ -5,130 +5,136 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import {expect} from 'chai'; -import sinon from 'sinon'; +import { expect } from "chai"; +import sinon from "sinon"; import { - authCallback, - registerAuthCallback, - resetAuthCallback, - updateAuthHeader, - verifyAuthCallback -} from '../src/utils'; - -describe('verifyCallback', () => { - it('should not throw error for valid callback', () => { - const validCallback = sinon.stub().returns('someString'); - expect(() => verifyAuthCallback(validCallback)).to.not.throw(); - }); - - it('should throw error for non-function callback', () => { - const nonFunctionCallback = 'notAFunction'; - expect(() => verifyAuthCallback(nonFunctionCallback)).to.throw('Userale auth callback must be a function'); - }); - - it('should throw error for non-string callback return', () => { - const invalidReturnCallback = sinon.stub().returns(123); - expect(() => verifyAuthCallback(invalidReturnCallback)).to.throw('Userale auth callback must return a string'); - }); - - it('should not throw error for valid callback with empty string return', () => { - const validCallback = sinon.stub().returns(''); - expect(() => verifyAuthCallback(validCallback)).to.not.throw(); - }); + authCallback, + registerAuthCallback, + resetAuthCallback, + updateAuthHeader, + verifyAuthCallback, +} from "../src/utils"; + +describe("verifyCallback", () => { + it("should not throw error for valid callback", () => { + const validCallback = sinon.stub().returns("someString"); + expect(() => verifyAuthCallback(validCallback)).to.not.throw(); + }); + + it("should throw error for non-function callback", () => { + const nonFunctionCallback = "notAFunction"; + expect(() => verifyAuthCallback(nonFunctionCallback)).to.throw( + "Userale auth callback must be a function", + ); + }); + + it("should throw error for non-string callback return", () => { + const invalidReturnCallback = sinon.stub().returns(123); + expect(() => verifyAuthCallback(invalidReturnCallback)).to.throw( + "Userale auth callback must return a string", + ); + }); + + it("should not throw error for valid callback with empty string return", () => { + const validCallback = sinon.stub().returns(""); + expect(() => verifyAuthCallback(validCallback)).to.not.throw(); + }); }); -describe('registerAuthCallback', () => { - afterEach(() => { - resetAuthCallback(); - }); - - it('should register a valid callback', () => { - const validCallback = sinon.stub().returns('someString'); - expect(registerAuthCallback(validCallback)).to.be.true; - expect(authCallback).to.equal(validCallback); - }); - - it('should not register a non-function callback', () => { - const nonFunctionCallback = 'notAFunction'; - expect(registerAuthCallback(nonFunctionCallback)).to.be.false; - expect(authCallback).to.be.null; - }); - - it('should not register a callback with invalid return type', () => { - const invalidReturnCallback = sinon.stub().returns(123); - expect(registerAuthCallback(invalidReturnCallback)).to.be.false; - expect(authCallback).to.be.null; - }); - - it('should register a callback with empty string return', () => { - const validCallback = sinon.stub().returns(''); - expect(registerAuthCallback(validCallback)).to.be.true; - expect(authCallback).to.equal(validCallback); - }); +describe("registerAuthCallback", () => { + afterEach(() => { + resetAuthCallback(); + }); + + it("should register a valid callback", () => { + const validCallback = sinon.stub().returns("someString"); + expect(registerAuthCallback(validCallback)).to.be.true; + expect(authCallback).to.equal(validCallback); + }); + + it("should not register a non-function callback", () => { + const nonFunctionCallback = "notAFunction"; + expect(registerAuthCallback(nonFunctionCallback)).to.be.false; + expect(authCallback).to.be.null; + }); + + it("should not register a callback with invalid return type", () => { + const invalidReturnCallback = sinon.stub().returns(123); + expect(registerAuthCallback(invalidReturnCallback)).to.be.false; + expect(authCallback).to.be.null; + }); + + it("should register a callback with empty string return", () => { + const validCallback = sinon.stub().returns(""); + expect(registerAuthCallback(validCallback)).to.be.true; + expect(authCallback).to.equal(validCallback); + }); }); -describe('updateAuthHeader', () => { - let config; - - beforeEach(() => { - // Initialize config object before each test - config = { authHeader: null }; - }); - - afterEach(() => { - resetAuthCallback(); - }); - - it('should update auth header when authCallback is provided', () => { - const validCallback = sinon.stub().returns('someString'); - registerAuthCallback(validCallback); - updateAuthHeader(config, authCallback); - expect(config.authHeader).to.equal('someString'); - }); - - it('should not update auth header when authCallback is not provided', () => { - updateAuthHeader(config, authCallback); - expect(config.authHeader).to.be.null; - }); - - it('should not update auth header when authCallback returns non-string', () => { - const invalidReturnCallback = sinon.stub().returns(123); - registerAuthCallback(invalidReturnCallback); - updateAuthHeader(config, authCallback); - expect(config.authHeader).to.be.null; - }); - - it('should update auth header with empty string return from authCallback', () => { - const validCallback = sinon.stub().returns(''); - registerAuthCallback(validCallback); - updateAuthHeader(config, authCallback); - expect(config.authHeader).to.equal(''); - }); - - it('should handle errors thrown during authCallback execution', () => { - const errorThrowingCallback = sinon.stub().throws(new Error('Callback execution failed')); - registerAuthCallback(errorThrowingCallback); - updateAuthHeader(config, authCallback); - expect(config.authHeader).to.be.null; - }); - - it('should not update auth header after unregistering authCallback', () => { - const validCallback = sinon.stub().returns('someString'); - registerAuthCallback(validCallback); - updateAuthHeader(config, authCallback); - expect(config.authHeader).to.equal('someString'); - - // Unregister authCallback - updateAuthHeader(config, null); - expect(config.authHeader).to.equal('someString'); - }); - }); \ No newline at end of file +describe("updateAuthHeader", () => { + let config; + + beforeEach(() => { + // Initialize config object before each test + config = { authHeader: null }; + }); + + afterEach(() => { + resetAuthCallback(); + }); + + it("should update auth header when authCallback is provided", () => { + const validCallback = sinon.stub().returns("someString"); + registerAuthCallback(validCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.equal("someString"); + }); + + it("should not update auth header when authCallback is not provided", () => { + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.be.null; + }); + + it("should not update auth header when authCallback returns non-string", () => { + const invalidReturnCallback = sinon.stub().returns(123); + registerAuthCallback(invalidReturnCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.be.null; + }); + + it("should update auth header with empty string return from authCallback", () => { + const validCallback = sinon.stub().returns(""); + registerAuthCallback(validCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.equal(""); + }); + + it("should handle errors thrown during authCallback execution", () => { + const errorThrowingCallback = sinon + .stub() + .throws(new Error("Callback execution failed")); + registerAuthCallback(errorThrowingCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.be.null; + }); + + it("should not update auth header after unregistering authCallback", () => { + const validCallback = sinon.stub().returns("someString"); + registerAuthCallback(validCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.equal("someString"); + + // Unregister authCallback + updateAuthHeader(config, null); + expect(config.authHeader).to.equal("someString"); + }); +}); diff --git a/test/configure_spec.js b/test/configure_spec.js index c9086c13..f20cb38a 100644 --- a/test/configure_spec.js +++ b/test/configure_spec.js @@ -5,68 +5,72 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import { expect } from 'chai'; +import { expect } from "chai"; -import { getUserIdFromParams, configure } from '../src/configure'; +import { getUserIdFromParams, configure } from "../src/configure"; -describe('configure', () => { - it('merges new configs into main config object', (done) => { +describe("configure", () => { + it("merges new configs into main config object", (done) => { const config = {}; - const newConfig = { foo: 'bar' }; + const newConfig = { foo: "bar" }; configure(config, newConfig); - expect(config).to.deep.equal({ foo: 'bar' }); + expect(config).to.deep.equal({ foo: "bar" }); done(); }); - it('Config autostart false makes autostart false', (done) => { - const config = {autostart: false}; + it("Config autostart false makes autostart false", (done) => { + const config = { autostart: false }; const newConfig = { autostart: true }; configure(config, newConfig); expect(config).to.deep.equal({ autostart: false }); done(); }); - it('neither autostart false makes autostart true', (done) => { - const config = {autostart: undefined}; + it("neither autostart false makes autostart true", (done) => { + const config = { autostart: undefined }; const newConfig = { autostart: true }; configure(config, newConfig); expect(config).to.deep.equal({ autostart: true }); done(); }); - it('includes a userid if present in the window.location', (done) => { + it("includes a userid if present in the window.location", (done) => { const config = {}; - const newConfig = { foo: 'bar', userFromParams: 'user', }; + const newConfig = { foo: "bar", userFromParams: "user" }; const initialWindow = global.window; - global.window = { location: { href: '?user=test&'} }; + global.window = { location: { href: "?user=test&" } }; configure(config, newConfig); global.window = initialWindow; - expect(config).to.deep.equal({ foo: 'bar', userFromParams: 'user', userId: 'test' }); + expect(config).to.deep.equal({ + foo: "bar", + userFromParams: "user", + userId: "test", + }); done(); }); - describe('getUserIdFromParams', () => { - it('fetches userId from URL params', (done) => { + describe("getUserIdFromParams", () => { + it("fetches userId from URL params", (done) => { const initialWindow = global.window; - global.window = { location: { href: '?user=foo&'} }; - const userId = getUserIdFromParams('user'); + global.window = { location: { href: "?user=foo&" } }; + const userId = getUserIdFromParams("user"); global.window = initialWindow; - expect(userId).to.equal('foo'); + expect(userId).to.equal("foo"); done(); }); - it('returns null if no matching param', (done) => { + it("returns null if no matching param", (done) => { const initialWindow = global.window; - global.window = { location: { href: '?user=foo&'} }; - const userId = getUserIdFromParams('bar'); + global.window = { location: { href: "?user=foo&" } }; + const userId = getUserIdFromParams("bar"); global.window = initialWindow; expect(userId).to.equal(null); done(); diff --git a/test/getInitialSettings_fetchAll.html b/test/getInitialSettings_fetchAll.html index bf343438..d77a60f2 100644 --- a/test/getInitialSettings_fetchAll.html +++ b/test/getInitialSettings_fetchAll.html @@ -14,22 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - - - + + + - -
- + +
+ diff --git a/test/getInitialSettings_spec.js b/test/getInitialSettings_spec.js index ec128077..83e190a9 100644 --- a/test/getInitialSettings_spec.js +++ b/test/getInitialSettings_spec.js @@ -5,83 +5,85 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import {expect} from 'chai'; -import 'global-jsdom/register' +import { expect } from "chai"; +import "global-jsdom/register"; -import {createEnvFromFile, sleep} from './testUtils'; -import {timeStampScale} from '../src/getInitialSettings.js'; +import { createEnvFromFile, sleep } from "./testUtils"; +import { timeStampScale } from "../src/getInitialSettings.js"; -describe('getInitialSettings', () => { - describe('timeStampScale', () => { - it('no event.timestamp', () => { - const e = {}; - const ts = timeStampScale(e); - expect(ts(e.timeStamp)).to.be.closeTo(Date.now(), 50); - }); +describe("getInitialSettings", () => { + describe("timeStampScale", () => { + it("no event.timestamp", () => { + const e = {}; + const ts = timeStampScale(e); + expect(ts(e.timeStamp)).to.be.closeTo(Date.now(), 50); + }); - it('zero', () => { - const e = {timeStamp: 0}; - const ts = timeStampScale(e); - expect(ts(e.timeStamp)).to.be.closeTo(Date.now(), 50); - }); + it("zero", () => { + const e = { timeStamp: 0 }; + const ts = timeStampScale(e); + expect(ts(e.timeStamp)).to.be.closeTo(Date.now(), 50); + }); - it('epoch milliseconds', () => { - const e = {timeStamp: 1451606400000}; - const ts = timeStampScale(e); - expect(ts(e.timeStamp)).to.equal(1451606400000); - }); + it("epoch milliseconds", () => { + const e = { timeStamp: 1451606400000 }; + const ts = timeStampScale(e); + expect(ts(e.timeStamp)).to.equal(1451606400000); + }); - it('epoch microseconds', () => { - const e = {timeStamp: 1451606400000000}; - const ts = timeStampScale(e); - expect(ts(e.timeStamp)).to.equal(1451606400000); - }); + it("epoch microseconds", () => { + const e = { timeStamp: 1451606400000000 }; + const ts = timeStampScale(e); + expect(ts(e.timeStamp)).to.equal(1451606400000); + }); - // Currently unsupported in jsdom - // Chrome specific -- manual testing is clear; - it('performance navigation time', () => { - const terriblePolyfill = {timing: {navigationStart: Date.now()}}; - const originalPerformance = global.performance; - global.performance = terriblePolyfill - const e = {timeStamp: 1}; - const ts = timeStampScale(e); - expect(ts(e.timeStamp)).to.equal(performance.timing.navigationStart + e.timeStamp); - global.performance = originalPerformance; - }); + // Currently unsupported in jsdom + // Chrome specific -- manual testing is clear; + it("performance navigation time", () => { + const terriblePolyfill = { timing: { navigationStart: Date.now() } }; + const originalPerformance = global.performance; + global.performance = terriblePolyfill; + const e = { timeStamp: 1 }; + const ts = timeStampScale(e); + expect(ts(e.timeStamp)).to.equal( + performance.timing.navigationStart + e.timeStamp, + ); + global.performance = originalPerformance; }); + }); - describe('getInitialSettings', () => { - it('fetches all settings from a script tag', async () => { - const dom = await createEnvFromFile('getInitialSettings_fetchAll.html') - const config = dom.window.userale.options(); - expect(config).to.have.property('autostart', true); - expect(config).to.have.property('url', 'http://test.com'); - expect(config).to.have.property('transmitInterval', 100); - expect(config).to.have.property('logCountThreshold', 10); - expect(config).to.have.property('userId', 'testuser'); - expect(config).to.have.property('version', '1.0.0'); - expect(config).to.have.property('logDetails', false); - expect(config).to.have.property('resolution', 100); - expect(config).to.have.property('toolName', 'testtool'); - dom.window.close() - }); + describe("getInitialSettings", () => { + it("fetches all settings from a script tag", async () => { + const dom = await createEnvFromFile("getInitialSettings_fetchAll.html"); + const config = dom.window.userale.options(); + expect(config).to.have.property("autostart", true); + expect(config).to.have.property("url", "http://test.com"); + expect(config).to.have.property("transmitInterval", 100); + expect(config).to.have.property("logCountThreshold", 10); + expect(config).to.have.property("userId", "testuser"); + expect(config).to.have.property("version", "1.0.0"); + expect(config).to.have.property("logDetails", false); + expect(config).to.have.property("resolution", 100); + expect(config).to.have.property("toolName", "testtool"); + dom.window.close(); + }); - it('grabs user id from params', async () => { - const dom = await createEnvFromFile('getInitialSettings_userParam.html', { - url: 'file://' + __dirname + '../' + '?user=fakeuser' - }); - const config = dom.window.userale.options(); - expect(config.userId).to.equal('fakeuser'); - dom.window.close() - }); + it("grabs user id from params", async () => { + const dom = await createEnvFromFile("getInitialSettings_userParam.html", { + url: "file://" + __dirname + "../" + "?user=fakeuser", + }); + const config = dom.window.userale.options(); + expect(config.userId).to.equal("fakeuser"); + dom.window.close(); }); + }); }); diff --git a/test/getInitialSettings_userParam.html b/test/getInitialSettings_userParam.html index f3314086..e25aaace 100644 --- a/test/getInitialSettings_userParam.html +++ b/test/getInitialSettings_userParam.html @@ -14,15 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - - - + + + - -
- + +
+ diff --git a/test/headers_spec.js b/test/headers_spec.js index a8bba1d9..3cd57d93 100644 --- a/test/headers_spec.js +++ b/test/headers_spec.js @@ -5,137 +5,157 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import {expect} from 'chai'; -import sinon from 'sinon'; +import { expect } from "chai"; +import sinon from "sinon"; import { - headersCallback, - registerHeadersCallback, - resetHeadersCallback, - updateCustomHeaders, - verifyHeadersCallback -} from '../src/utils'; - -describe('verifyCallback', () => { - it('should not throw error for valid callback', () => { - const validCallback = sinon.stub().returns({'x-api-token': 'someString', 'x-abc-def': 'someOtherString'}); - expect(() => verifyHeadersCallback(validCallback)).to.not.throw(); - }); - - it('should throw error for non-function callback', () => { - const nonFunctionCallback = 'notAFunction'; - expect(() => verifyHeadersCallback(nonFunctionCallback)).to.throw('Userale headers callback must be a function'); - }); - - it('should throw error for non-object callback return', () => { - const invalidReturnCallback = sinon.stub().returns(123); - expect(() => verifyHeadersCallback(invalidReturnCallback)).to.throw('Userale headers callback must return an object'); - }); - - it('should throw error for incorrect headers object return', () => { - const invalidReturnCallback = sinon.stub().returns({'x-not-a-proper-value': 123}); - expect(() => verifyHeadersCallback(invalidReturnCallback)).to.throw('Userale header callback must return an object with string keys and values'); - }); - - it('should not throw error for valid callback with empty object return', () => { - const validCallback = sinon.stub().returns({}); - expect(() => verifyHeadersCallback(validCallback)).to.not.throw(); - }); + headersCallback, + registerHeadersCallback, + resetHeadersCallback, + updateCustomHeaders, + verifyHeadersCallback, +} from "../src/utils"; + +describe("verifyCallback", () => { + it("should not throw error for valid callback", () => { + const validCallback = sinon + .stub() + .returns({ "x-api-token": "someString", "x-abc-def": "someOtherString" }); + expect(() => verifyHeadersCallback(validCallback)).to.not.throw(); + }); + + it("should throw error for non-function callback", () => { + const nonFunctionCallback = "notAFunction"; + expect(() => verifyHeadersCallback(nonFunctionCallback)).to.throw( + "Userale headers callback must be a function", + ); + }); + + it("should throw error for non-object callback return", () => { + const invalidReturnCallback = sinon.stub().returns(123); + expect(() => verifyHeadersCallback(invalidReturnCallback)).to.throw( + "Userale headers callback must return an object", + ); + }); + + it("should throw error for incorrect headers object return", () => { + const invalidReturnCallback = sinon + .stub() + .returns({ "x-not-a-proper-value": 123 }); + expect(() => verifyHeadersCallback(invalidReturnCallback)).to.throw( + "Userale header callback must return an object with string keys and values", + ); + }); + + it("should not throw error for valid callback with empty object return", () => { + const validCallback = sinon.stub().returns({}); + expect(() => verifyHeadersCallback(validCallback)).to.not.throw(); + }); }); -describe('registerHeadersCallback', () => { - afterEach(() => { - resetHeadersCallback(); - }); - - it('should register a valid callback', () => { - const validCallback = sinon.stub().returns({'x-api-token': 'someString', 'x-abc-def': 'someOtherString'}); - expect(registerHeadersCallback(validCallback)).to.be.true; - expect(headersCallback).to.equal(validCallback); - }); - - it('should not register a non-function callback', () => { - const nonFunctionCallback = 'notAFunction'; - expect(registerHeadersCallback(nonFunctionCallback)).to.be.false; - expect(headersCallback).to.be.null; - }); - - it('should not register a callback with invalid return type', () => { - const invalidReturnCallback = sinon.stub().returns(123); - expect(registerHeadersCallback(invalidReturnCallback)).to.be.false; - expect(headersCallback).to.be.null; - }); - - it('should register a callback with empty object return', () => { - const validCallback = sinon.stub().returns({}); - expect(registerHeadersCallback(validCallback)).to.be.true; - expect(headersCallback).to.equal(validCallback); - }); +describe("registerHeadersCallback", () => { + afterEach(() => { + resetHeadersCallback(); + }); + + it("should register a valid callback", () => { + const validCallback = sinon + .stub() + .returns({ "x-api-token": "someString", "x-abc-def": "someOtherString" }); + expect(registerHeadersCallback(validCallback)).to.be.true; + expect(headersCallback).to.equal(validCallback); + }); + + it("should not register a non-function callback", () => { + const nonFunctionCallback = "notAFunction"; + expect(registerHeadersCallback(nonFunctionCallback)).to.be.false; + expect(headersCallback).to.be.null; + }); + + it("should not register a callback with invalid return type", () => { + const invalidReturnCallback = sinon.stub().returns(123); + expect(registerHeadersCallback(invalidReturnCallback)).to.be.false; + expect(headersCallback).to.be.null; + }); + + it("should register a callback with empty object return", () => { + const validCallback = sinon.stub().returns({}); + expect(registerHeadersCallback(validCallback)).to.be.true; + expect(headersCallback).to.equal(validCallback); + }); }); -describe('updateCustomHeader', () => { - let config; - - beforeEach(() => { - // Initialize config object before each test - config = { headers: null }; - }); - - afterEach(() => { - resetHeadersCallback(); - }); - - it('should update custom headers when headersCallback is provided', () => { - const customHeaders = {'x-api-token': 'someString', 'x-abc-def': 'someOtherString'} - const validCallback = sinon.stub().returns(customHeaders); - registerHeadersCallback(validCallback); - updateCustomHeaders(config, headersCallback); - expect(config.headers).to.equal(customHeaders); - }); - - it('should not update custom headers when headersCallback is not provided', () => { - updateCustomHeaders(config, headersCallback); - expect(config.headers).to.be.null; - }); - - it('should not update custom headers when headersCallback returns non-object', () => { - const invalidReturnCallback = sinon.stub().returns(123); - registerHeadersCallback(invalidReturnCallback); - updateCustomHeaders(config, headersCallback); - expect(config.headers).to.be.null; - }); - - it('should update custom headers with empty string return from headersCallback', () => { - const validCallback = sinon.stub().returns({}); - registerHeadersCallback(validCallback); - updateCustomHeaders(config, headersCallback); - expect(config.headers).to.deep.equal({}); - }); - - it('should handle errors thrown during headersCallback execution', () => { - const errorThrowingCallback = sinon.stub().throws(new Error('Callback execution failed')); - registerHeadersCallback(errorThrowingCallback); - updateCustomHeaders(config, headersCallback); - expect(config.headers).to.be.null; - }); - - it('should not update custom headers after unregistering headersCallback', () => { - const customHeaders = {'x-api-token': 'someString', 'x-abc-def': 'someOtherString'} - const validCallback = sinon.stub().returns(customHeaders); - registerHeadersCallback(validCallback); - updateCustomHeaders(config, headersCallback); - expect(config.headers).to.equal(customHeaders); - - // Unregister headersCallback - updateCustomHeaders(config, null); - expect(config.headers).to.equal(customHeaders); - }); - }); \ No newline at end of file +describe("updateCustomHeader", () => { + let config; + + beforeEach(() => { + // Initialize config object before each test + config = { headers: null }; + }); + + afterEach(() => { + resetHeadersCallback(); + }); + + it("should update custom headers when headersCallback is provided", () => { + const customHeaders = { + "x-api-token": "someString", + "x-abc-def": "someOtherString", + }; + const validCallback = sinon.stub().returns(customHeaders); + registerHeadersCallback(validCallback); + updateCustomHeaders(config, headersCallback); + expect(config.headers).to.equal(customHeaders); + }); + + it("should not update custom headers when headersCallback is not provided", () => { + updateCustomHeaders(config, headersCallback); + expect(config.headers).to.be.null; + }); + + it("should not update custom headers when headersCallback returns non-object", () => { + const invalidReturnCallback = sinon.stub().returns(123); + registerHeadersCallback(invalidReturnCallback); + updateCustomHeaders(config, headersCallback); + expect(config.headers).to.be.null; + }); + + it("should update custom headers with empty string return from headersCallback", () => { + const validCallback = sinon.stub().returns({}); + registerHeadersCallback(validCallback); + updateCustomHeaders(config, headersCallback); + expect(config.headers).to.deep.equal({}); + }); + + it("should handle errors thrown during headersCallback execution", () => { + const errorThrowingCallback = sinon + .stub() + .throws(new Error("Callback execution failed")); + registerHeadersCallback(errorThrowingCallback); + updateCustomHeaders(config, headersCallback); + expect(config.headers).to.be.null; + }); + + it("should not update custom headers after unregistering headersCallback", () => { + const customHeaders = { + "x-api-token": "someString", + "x-abc-def": "someOtherString", + }; + const validCallback = sinon.stub().returns(customHeaders); + registerHeadersCallback(validCallback); + updateCustomHeaders(config, headersCallback); + expect(config.headers).to.equal(customHeaders); + + // Unregister headersCallback + updateCustomHeaders(config, null); + expect(config.headers).to.equal(customHeaders); + }); +}); diff --git a/test/main.html b/test/main.html index 539c3203..5a1f2aea 100644 --- a/test/main.html +++ b/test/main.html @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - - - + + + - -
- + +
+ diff --git a/test/main_spec.js b/test/main_spec.js index 084561a5..ae66e50d 100644 --- a/test/main_spec.js +++ b/test/main_spec.js @@ -5,96 +5,96 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import {expect} from 'chai'; -import {createEnvFromFile} from './testUtils'; +import { expect } from "chai"; +import { createEnvFromFile } from "./testUtils"; -describe('Userale API', () => { - const htmlFileName = 'main.html' - it('provides configs', async () => { - const dom = await createEnvFromFile(htmlFileName) - const config = dom.window.userale.options(); - expect(config).to.be.an('object'); - expect(config).to.have.all.keys([ - 'on', - 'useraleVersion', - 'autostart', - 'url', - 'transmitInterval', - 'logCountThreshold', - 'userId', - 'sessionID', - 'httpSessionId', - 'browserSessionId', - 'version', - 'logDetails', - 'resolution', - 'toolName', - 'userFromParams', - 'time', - 'authHeader', - 'headers', - 'custIndex' - ]); - dom.window.close(); - }); - - it('edits configs', async () => { - const dom = await createEnvFromFile(htmlFileName) - const config = dom.window.userale.options(); - const interval = config.transmitInterval; - dom.window.userale.options({ - transmitInterval: interval + 10 - }); - const newConfig = dom.window.userale.options(); +describe("Userale API", () => { + const htmlFileName = "main.html"; + it("provides configs", async () => { + const dom = await createEnvFromFile(htmlFileName); + const config = dom.window.userale.options(); + expect(config).to.be.an("object"); + expect(config).to.have.all.keys([ + "on", + "useraleVersion", + "autostart", + "url", + "transmitInterval", + "logCountThreshold", + "userId", + "sessionID", + "httpSessionId", + "browserSessionId", + "version", + "logDetails", + "resolution", + "toolName", + "userFromParams", + "time", + "authHeader", + "headers", + "custIndex", + ]); + dom.window.close(); + }); - expect(newConfig.transmitInterval).to.equal(interval + 10); - dom.window.close(); + it("edits configs", async () => { + const dom = await createEnvFromFile(htmlFileName); + const config = dom.window.userale.options(); + const interval = config.transmitInterval; + dom.window.userale.options({ + transmitInterval: interval + 10, }); + const newConfig = dom.window.userale.options(); - it('disables autostart', async () => { - const dom = await createEnvFromFile(htmlFileName) - dom.window.userale.options({ - autostart: false - }); - const newConfig = dom.window.userale.options(); + expect(newConfig.transmitInterval).to.equal(interval + 10); + dom.window.close(); + }); - expect(newConfig.autostart).to.equal(false); - dom.window.close(); + it("disables autostart", async () => { + const dom = await createEnvFromFile(htmlFileName); + dom.window.userale.options({ + autostart: false, }); + const newConfig = dom.window.userale.options(); - it('starts + stops', async () => { - const dom = await createEnvFromFile(htmlFileName) - setTimeout(() => { - const {userale} = dom.window; - expect(userale.options().on).to.equal(true); + expect(newConfig.autostart).to.equal(false); + dom.window.close(); + }); - userale.stop(); - expect(userale.options().on).to.equal(false); + it("starts + stops", async () => { + const dom = await createEnvFromFile(htmlFileName); + setTimeout(() => { + const { userale } = dom.window; + expect(userale.options().on).to.equal(true); - userale.start(); - expect(userale.options().on).to.equal(true); + userale.stop(); + expect(userale.options().on).to.equal(false); - dom.window.close(); - }, 200); - }); + userale.start(); + expect(userale.options().on).to.equal(true); - it('sends custom logs', async () => { - const dom = await createEnvFromFile(htmlFileName) - const {userale} = dom.window; + dom.window.close(); + }, 200); + }); - expect(userale.log({})).to.equal(true); - expect(userale.log()).to.equal(false); - expect(userale.log(null)).to.equal(false); + it("sends custom logs", async () => { + const dom = await createEnvFromFile(htmlFileName); + const { userale } = dom.window; - dom.window.close(); - }); + expect(userale.log({})).to.equal(true); + expect(userale.log()).to.equal(false); + expect(userale.log(null)).to.equal(false); + + dom.window.close(); + }); }); diff --git a/test/packageLogs_spec.js b/test/packageLogs_spec.js index 26680fef..39f46e93 100644 --- a/test/packageLogs_spec.js +++ b/test/packageLogs_spec.js @@ -5,357 +5,374 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import {expect} from 'chai'; -import {JSDOM} from 'jsdom'; -import 'global-jsdom/register' +import { expect } from "chai"; +import { JSDOM } from "jsdom"; +import "global-jsdom/register"; import { - addCallbacks, - buildPath, - cbHandlers, - extractTimeFields, - filterHandler, - getLocation, - getSelector, - initPackager, - logs, - mapHandler, - packageLog, - removeCallbacks, - selectorizePath, - setLogFilter, - setLogMapper, -} from '../src/packageLogs'; - -describe('packageLogs', () => { - describe('setLogFilter', () => { - it('assigns the handler to the provided value', () => { - const func = x => true; - setLogFilter(func); - expect(filterHandler).to.equal(func); - }); - it('allows the handler to be nulled', () => { - setLogFilter(x => true); - setLogFilter(null); - expect(filterHandler).to.equal(null); - }); + addCallbacks, + buildPath, + cbHandlers, + extractTimeFields, + filterHandler, + getLocation, + getSelector, + initPackager, + logs, + mapHandler, + packageLog, + removeCallbacks, + selectorizePath, + setLogFilter, + setLogMapper, +} from "../src/packageLogs"; + +describe("packageLogs", () => { + describe("setLogFilter", () => { + it("assigns the handler to the provided value", () => { + const func = (x) => true; + setLogFilter(func); + expect(filterHandler).to.equal(func); }); + it("allows the handler to be nulled", () => { + setLogFilter((x) => true); + setLogFilter(null); + expect(filterHandler).to.equal(null); + }); + }); + + describe("setLogMapper", () => { + it("assigns the handler to the provided value", () => { + const func = (x) => true; + setLogMapper(func); + expect(mapHandler).to.equal(func); + }); + it("allows the handler to be nulled", () => { + setLogMapper((x) => true); + setLogMapper(null); + expect(mapHandler).to.equal(null); + }); + }); + + describe("addCallbacks", () => { + it("adds a single callback", () => { + initPackager([], { on: false }); + const fn = { + func1() { + return true; + }, + }; + addCallbacks(fn); + expect(Object.keys(cbHandlers)).to.deep.equal(Object.keys(fn)); + }); + it("adds a list of callbacks", () => { + initPackager([], { on: false }); + + const fns = { + func1() { + return true; + }, + func2() { + return false; + }, + }; + + addCallbacks(fns); + expect(Object.keys(cbHandlers)).to.deep.equal(Object.keys(fns)); + }); + }); + + describe("removeCallbacks", () => { + it("removes a single callback", () => { + initPackager([], { on: false }); + const fn = { + func() { + return true; + }, + }; + addCallbacks(fn); + removeCallbacks(Object.keys(fn)); + expect(cbHandlers).to.be.empty; + }); + it("removes a list of callbacks", () => { + initPackager([], { on: false }); + const fns = { + func1() { + return true; + }, + func2() { + return false; + }, + }; + addCallbacks(fns); + removeCallbacks(Object.keys(fns)); + expect(cbHandlers).to.be.empty; + }); + }); - describe('setLogMapper', () => { - it('assigns the handler to the provided value', () => { - const func = x => true; - setLogMapper(func); - expect(mapHandler).to.equal(func); - }); - it('allows the handler to be nulled', () => { - setLogMapper(x => true); - setLogMapper(null); - expect(mapHandler).to.equal(null); - }); + describe("packageLog", () => { + it("only executes if on", () => { + initPackager([], { on: true }); + const evt = { target: {}, type: "test" }; + expect(packageLog(evt)).to.equal(true); + + initPackager([], { on: false }); + expect(packageLog({})).to.equal(false); + }); + it("calls detailFcn with the event as an argument if provided", () => { + initPackager([], { on: true }); + let called = false; + const evt = { target: {}, type: "test" }; + const detailFcn = (e) => { + called = true; + expect(e).to.equal(evt); + }; + packageLog(evt, detailFcn); + expect(called).to.equal(true); }); - - describe('addCallbacks', () => { - it('adds a single callback', () => { - initPackager([], {on: false}); - const fn = { - func1() {return true} - }; - addCallbacks(fn); - expect(Object.keys(cbHandlers)).to.deep.equal(Object.keys(fn)); - }); - it('adds a list of callbacks', () => { - initPackager([], {on: false}); - - const fns = { - func1() {return true}, - func2() {return false} - }; - - addCallbacks(fns); - expect(Object.keys(cbHandlers)).to.deep.equal(Object.keys(fns)); - }); + it("packages logs", () => { + initPackager([], { on: true }); + const evt = { + target: {}, + type: "test", + }; + expect(packageLog(evt)).to.equal(true); + }); + + it("filters logs when a handler is assigned and returns false", () => { + let filterCalled = false; + const filter = { + filterAll() { + filterCalled = true; + return false; + }, + }; + + const evt = { + target: {}, + type: "test", + }; + + initPackager([], { on: true }); + packageLog(evt); + + expect(logs.length).to.equal(1); + + addCallbacks(filter); + packageLog(evt); + + expect(filterCalled).to.equal(true); + expect(logs.length).to.equal(1); + }); + + it("assigns logs to the callback's return value if a handler is assigned", () => { + let mapperCalled = false; + + const mappedLog = { type: "foo" }; + const mapper = { + mapper() { + mapperCalled = true; + return mappedLog; + }, + }; + + const evt = { + target: {}, + type: "test", + }; + + initPackager([], { on: true }); + + addCallbacks(mapper); + packageLog(evt); + + expect(mapperCalled).to.equal(true); + expect(logs.indexOf(mappedLog)).to.equal(0); }); - - describe('removeCallbacks', () => { - it('removes a single callback', () => { - initPackager([], {on: false}); - const fn = {func() {return true}}; - addCallbacks(fn); - removeCallbacks(Object.keys(fn)); - expect(cbHandlers).to.be.empty; - }); - it('removes a list of callbacks', () => { - initPackager([], {on: false}); - const fns = { - func1() {return true}, - func2() {return false} - }; - addCallbacks(fns); - removeCallbacks(Object.keys(fns)); - expect(cbHandlers).to.be.empty; - }); + + it("does not call a subsequent handler if the log is filtered out", () => { + let mapperCalled = false; + const filter = () => false; + const mapper = (log) => { + mapperCalled = true; + return log; + }; + + const evt = { + target: {}, + type: "test", + }; + + initPackager([], { on: true }); + addCallbacks(filter); + addCallbacks(mapper); + + packageLog(evt); + + expect(mapperCalled).to.equal(false); }); - describe('packageLog', () => { - it('only executes if on', () => { - initPackager([], {on: true}); - const evt = {target: {}, type: 'test'}; - expect(packageLog(evt)).to.equal(true); - - initPackager([], {on: false}); - expect(packageLog({})).to.equal(false); - }); - it('calls detailFcn with the event as an argument if provided', () => { - initPackager([], {on: true}); - let called = false; - const evt = {target: {}, type: 'test'}; - const detailFcn = (e) => { - called = true; - expect(e).to.equal(evt); - }; - packageLog(evt, detailFcn); - expect(called).to.equal(true); - }); - it('packages logs', () => { - initPackager([], {on: true}); - const evt = { - target: {}, - type: 'test' - }; - expect(packageLog(evt)).to.equal(true); - }); - - it('filters logs when a handler is assigned and returns false', () => { - let filterCalled = false; - const filter = { - filterAll() { - filterCalled = true; - return false; - } - }; - - const evt = { - target: {}, - type: 'test', - }; - - initPackager([], {on: true}); - packageLog(evt); - - expect(logs.length).to.equal(1); - - addCallbacks(filter); - packageLog(evt); - - expect(filterCalled).to.equal(true); - expect(logs.length).to.equal(1); - }); - - it('assigns logs to the callback\'s return value if a handler is assigned', () => { - let mapperCalled = false; - - const mappedLog = {type: 'foo'}; - const mapper = { - mapper() { - mapperCalled = true; - return mappedLog; - } - }; - - const evt = { - target: {}, - type: 'test', - }; - - initPackager([], {on: true}); - - addCallbacks(mapper); - packageLog(evt); - - expect(mapperCalled).to.equal(true); - expect(logs.indexOf(mappedLog)).to.equal(0); - }); - - it('does not call a subsequent handler if the log is filtered out', () => { - let mapperCalled = false; - const filter = () => false; - const mapper = (log) => { - mapperCalled = true; - return log; - }; - - const evt = { - target: {}, - type: 'test', - }; - - initPackager([], {on: true}); - addCallbacks(filter); - addCallbacks(mapper); - - packageLog(evt); - - expect(mapperCalled).to.equal(false); - }); - - it('does not attempt to call a non-function filter/mapper', () => { - const evt = { - target: {}, - type: 'test', - }; - - initPackager([], {on: true}); - packageLog(evt); - addCallbacks('foo'); - packageLog(evt); - - expect(logs.length).to.equal(2); - }); + it("does not attempt to call a non-function filter/mapper", () => { + const evt = { + target: {}, + type: "test", + }; + + initPackager([], { on: true }); + packageLog(evt); + addCallbacks("foo"); + packageLog(evt); + + expect(logs.length).to.equal(2); }); + }); + + describe("extractTimeFields", () => { + it("returns the millisecond and microsecond portions of a timestamp", () => { + const timeStamp = 123.456; + const fields = { milli: 123, micro: 0.456 }; + const ret = extractTimeFields(timeStamp); - describe('extractTimeFields', () => { - it('returns the millisecond and microsecond portions of a timestamp', () => { - const timeStamp = 123.456; - const fields = {milli: 123, micro: 0.456}; - const ret = extractTimeFields(timeStamp); - - expect(ret.milli).to.equal(fields.milli); - expect(ret.micro).to.equal(fields.micro); - }); - it('sets micro to 0 when no decimal is present', () => { - const timeStamp = 123; - const fields = {milli: 123, micro: 0}; - const ret = extractTimeFields(timeStamp); - - expect(ret.milli).to.equal(fields.milli); - expect(ret.micro).to.equal(fields.micro); - }); - it('always returns an object', () => { - const stampVariants = [ - null, - 'foobar', - {foo: 'bar'}, - undefined, - ['foo', 'bar'], - 123, - ]; - - stampVariants.forEach((variant) => { - const ret = extractTimeFields(variant); - expect(!!ret).to.equal(true); - expect(typeof ret).to.equal('object'); - }); - }); + expect(ret.milli).to.equal(fields.milli); + expect(ret.micro).to.equal(fields.micro); }); + it("sets micro to 0 when no decimal is present", () => { + const timeStamp = 123; + const fields = { milli: 123, micro: 0 }; + const ret = extractTimeFields(timeStamp); - describe('getLocation', () => { - it('returns event page location', () => { - new JSDOM(``); - const document = window.document; - const ele = document.createElement('div'); - // Create a click in the top left corner of the viewport - const evt = new window.MouseEvent('click', { - 'view': window, - 'bubbles': true, - 'cancelable': true, - 'clientX': 0, - 'clientY': 0, - }); - document.body.appendChild(ele); - ele.addEventListener('click', (e) => { - // Expect the click location to be the top left corner of the viewport - let expectedLocation = {'x': window.scrollX, 'y': window.scrollY}; - expect(getLocation(e)).to.deep.equal(expectedLocation); - }); - ele.dispatchEvent(evt); - }); - - it('calculates page location if unavailable', () => { - new JSDOM(``) - const document = window.document; - const ele = document.createElement('div'); - const evt = new window.MouseEvent('click', { - 'view': window, - 'bubbles': true, - 'cancelable': true - }); - document.body.appendChild(ele); - ele.addEventListener('click', (e) => { - document.documentElement.scrollLeft = 0; - document.documentElement.scrollTop = 0; - const originalDocument = global.document; - global.document = document; - expect(getLocation(e)).to.deep.equal({x: 0, y: 0}); - global.document = originalDocument; - }); - ele.dispatchEvent(evt); - }); - - it('fails to null', () => { - let hadError = false; - try { - getLocation(null); - } catch (e) { - hadError = true; - } - expect(hadError).to.equal(true); - }); + expect(ret.milli).to.equal(fields.milli); + expect(ret.micro).to.equal(fields.micro); + }); + it("always returns an object", () => { + const stampVariants = [ + null, + "foobar", + { foo: "bar" }, + undefined, + ["foo", "bar"], + 123, + ]; + + stampVariants.forEach((variant) => { + const ret = extractTimeFields(variant); + expect(!!ret).to.equal(true); + expect(typeof ret).to.equal("object"); + }); + }); + }); + + describe("getLocation", () => { + it("returns event page location", () => { + new JSDOM(``); + const document = window.document; + const ele = document.createElement("div"); + // Create a click in the top left corner of the viewport + const evt = new window.MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + clientX: 0, + clientY: 0, + }); + document.body.appendChild(ele); + ele.addEventListener("click", (e) => { + // Expect the click location to be the top left corner of the viewport + let expectedLocation = { x: window.scrollX, y: window.scrollY }; + expect(getLocation(e)).to.deep.equal(expectedLocation); + }); + ele.dispatchEvent(evt); }); - describe('selectorizePath', () => { - it('returns a new array of the same length provided', () => { - const arr = [{}, {}]; - const ret = selectorizePath(arr); - expect(ret).to.be.instanceof(Array); - expect(ret).to.not.equal(arr); - expect(ret.length).to.equal(arr.length); - }); + it("calculates page location if unavailable", () => { + new JSDOM(``); + const document = window.document; + const ele = document.createElement("div"); + const evt = new window.MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + }); + document.body.appendChild(ele); + ele.addEventListener("click", (e) => { + document.documentElement.scrollLeft = 0; + document.documentElement.scrollTop = 0; + const originalDocument = global.document; + global.document = document; + expect(getLocation(e)).to.deep.equal({ x: 0, y: 0 }); + global.document = originalDocument; + }); + ele.dispatchEvent(evt); }); - describe('getSelector', () => { - it('builds a selector', () => { - new JSDOM(``) - const document = window.document; - const element = document.createElement('div'); - expect(getSelector(element)).to.equal('div'); - element.id = 'bar'; - expect(getSelector(element)).to.equal('div#bar'); - element.removeAttribute('id'); - element.classList.add('baz'); - expect(getSelector(element)).to.equal('div.baz'); - element.id = 'bar'; - expect(getSelector(element)).to.equal('div#bar.baz'); - }); - it('identifies window', () => { - new JSDOM(``) - expect(getSelector(window)).to.equal('Window'); - }); - - it('handles a non-null unknown value', () => { - expect(getSelector('foo')).to.equal('Unknown'); - }); + it("fails to null", () => { + let hadError = false; + try { + getLocation(null); + } catch (e) { + hadError = true; + } + expect(hadError).to.equal(true); + }); + }); + + describe("selectorizePath", () => { + it("returns a new array of the same length provided", () => { + const arr = [{}, {}]; + const ret = selectorizePath(arr); + expect(ret).to.be.instanceof(Array); + expect(ret).to.not.equal(arr); + expect(ret.length).to.equal(arr.length); + }); + }); + + describe("getSelector", () => { + it("builds a selector", () => { + new JSDOM(``); + const document = window.document; + const element = document.createElement("div"); + expect(getSelector(element)).to.equal("div"); + element.id = "bar"; + expect(getSelector(element)).to.equal("div#bar"); + element.removeAttribute("id"); + element.classList.add("baz"); + expect(getSelector(element)).to.equal("div.baz"); + element.id = "bar"; + expect(getSelector(element)).to.equal("div#bar.baz"); + }); + it("identifies window", () => { + new JSDOM(``); + expect(getSelector(window)).to.equal("Window"); }); - describe('buildPath', () => { - it('builds a path', () => { - new JSDOM(``) - let actualPath - const document = window.document; - const ele = document.createElement('div'); - const evt = new window.Event('CustomEvent', {bubbles: true, cancelable: true}) - document.body.appendChild(ele); - ele.addEventListener('CustomEvent', e => actualPath = buildPath(e)) - ele.dispatchEvent(evt); - const expectedPath = ['div', 'body', 'html', "#document", "Window"] - expect(actualPath).to.deep.equal(expectedPath); - }); + it("handles a non-null unknown value", () => { + expect(getSelector("foo")).to.equal("Unknown"); + }); + }); + + describe("buildPath", () => { + it("builds a path", () => { + new JSDOM(``); + let actualPath; + const document = window.document; + const ele = document.createElement("div"); + const evt = new window.Event("CustomEvent", { + bubbles: true, + cancelable: true, + }); + document.body.appendChild(ele); + ele.addEventListener("CustomEvent", (e) => (actualPath = buildPath(e))); + ele.dispatchEvent(evt); + const expectedPath = ["div", "body", "html", "#document", "Window"]; + expect(actualPath).to.deep.equal(expectedPath); }); + }); }); diff --git a/test/sendLogs_spec.js b/test/sendLogs_spec.js index 7b508029..72f94dbb 100644 --- a/test/sendLogs_spec.js +++ b/test/sendLogs_spec.js @@ -5,190 +5,211 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import chai, {expect} from 'chai'; -import chaiSubset from 'chai-subset'; -import {JSDOM} from 'jsdom'; -import sinon from 'sinon'; -import {initSender, sendOnInterval, sendOnClose} from '../src/sendLogs'; -import {registerAuthCallback, registerHeadersCallback} from '../src/utils'; -import 'global-jsdom/register' +import chai, { expect } from "chai"; +import chaiSubset from "chai-subset"; +import { JSDOM } from "jsdom"; +import sinon from "sinon"; +import { initSender, sendOnInterval, sendOnClose } from "../src/sendLogs"; +import { registerAuthCallback, registerHeadersCallback } from "../src/utils"; +import "global-jsdom/register"; chai.use(chaiSubset); -describe('sendLogs', () => { - it('sends logs on an interval', (done) => { - let requests = 0; - const originalXMLHttpRequest = global.XMLHttpRequest; - const conf = {on: true, transmitInterval: 500, url: 'test', logCountThreshold: 2}; - const logs = []; - const clock = sinon.useFakeTimers(); - const xhr = sinon.useFakeXMLHttpRequest(); - global.XMLHttpRequest = xhr; - xhr.onCreate = () => { - requests++; - }; - - sendOnInterval(logs, conf); - - clock.tick(conf.transmitInterval * 2); - // Make sure it doesn't make requests for no raisin - expect(requests).to.equal(0); - - // Make sure it respects the logCountThreshold - logs.push({foo: 'bar1'}); - clock.tick(conf.transmitInterval); - expect(logs.length).to.equal(1); - - // Make sure it sends the logs and clears the array - logs.push({foo: 'bar2'}); - clock.tick(conf.transmitInterval); - expect(logs.length).to.equal(0); - expect(requests).to.equal(1); - - xhr.restore(); - clock.restore(); - global.XMLHttpRequest = originalXMLHttpRequest; - done(); - }); - - it('does not send logs if the config is off', (done) => { - let requests = 0; - const originalXMLHttpRequest = global.XMLHttpRequest; - const conf = {on: true, transmitInterval: 500, url: 'test', logCountThreshold: 1}; - const logs = []; - const clock = sinon.useFakeTimers(); - const xhr = sinon.useFakeXMLHttpRequest(); - global.XMLHttpRequest = xhr; - xhr.onCreate = () => { - requests++; - }; - - sendOnInterval(logs, conf); - - // Make sure it respects the logCountThreshold - logs.push({foo: 'bar1'}); - clock.tick(conf.transmitInterval); - - expect(logs.length).to.equal(0); - expect(requests).to.equal(1); - - conf.on = false; - - logs.push({foo: 'bar2'}); - clock.tick(conf.transmitInterval); - expect(logs.length).to.equal(1); - expect(requests).to.equal(1); - - xhr.restore(); - clock.restore(); - global.XMLHttpRequest = originalXMLHttpRequest; - done(); - }); - - it('sends logs on page exit with navigator', () => { - const sendBeaconSpy = sinon.spy() - global.navigator = { - sendBeacon: sendBeaconSpy - }; - sendOnClose([], {on: true, url: 'test'}) - sendOnClose([{foo: 'bar'}], {on: true, url: 'test'}); - global.window.dispatchEvent(new window.CustomEvent('pagehide')) - sinon.assert.calledOnce(sendBeaconSpy) - }); - - it('does not send logs on page exit when config is off', () => { - const sendBeaconSpy = sinon.spy() - global.navigator = { - sendBeacon: sendBeaconSpy - }; - sendOnClose([{foo: 'bar'}], {on: false, url: 'test'}); - global.window.dispatchEvent(new window.CustomEvent('pagehide')) - sinon.assert.notCalled(sendBeaconSpy) - }); - - it('sends logs with proper auth header when using registerAuthCallback', (done) => { - let requests = [] - const originalXMLHttpRequest = global.XMLHttpRequest; - const conf = { on: true, transmitInterval: 500, url: 'test', logCountThreshold: 1 }; - const logs = []; - const clock = sinon.useFakeTimers(); - const xhr = sinon.useFakeXMLHttpRequest(); - global.XMLHttpRequest = xhr; - xhr.onCreate = (xhr) => { - requests.push(xhr); - }; - - // Mock the authCallback function - const authCallback = sinon.stub().returns('fakeAuthToken'); - - // Register the authCallback - registerAuthCallback(authCallback); - - // Initialize sender with logs and config - initSender(logs, conf); - - // Simulate log entry - logs.push({ foo: 'bar' }); - - // Trigger interval to send logs - clock.tick(conf.transmitInterval); - - // Verify that the request has the proper auth header - expect(requests.length).to.equal(1); - expect(requests[0].requestHeaders.Authorization).to.equal('fakeAuthToken'); - - // Restore XMLHttpRequest and clock - xhr.restore(); - clock.restore(); - global.XMLHttpRequest = originalXMLHttpRequest; - done() - }); - - it('sends logs with proper custom headers when using registerHeadersCallback', (done) => { - let requests = [] - const originalXMLHttpRequest = global.XMLHttpRequest; - const conf = { on: true, transmitInterval: 500, url: 'test', logCountThreshold: 1 }; - const logs = []; - const clock = sinon.useFakeTimers(); - const xhr = sinon.useFakeXMLHttpRequest(); - global.XMLHttpRequest = xhr; - xhr.onCreate = (xhr) => { - requests.push(xhr); - }; - - // Mock the authCallback function - const customHeaders = {'x-api-token': 'someString', 'x-abc-def': 'someOtherString'} - const headersCallback = sinon.stub().returns(customHeaders); - - // Register the authCallback - registerHeadersCallback(headersCallback); - - // Initialize sender with logs and config - initSender(logs, conf); - - // Simulate log entry - logs.push({ foo: 'bar' }); - - // Trigger interval to send logs - clock.tick(conf.transmitInterval); - - // Verify that the request has the proper auth header - expect(requests.length).to.equal(1); - expect(requests[0].requestHeaders).to.containSubset(customHeaders); - - // Restore XMLHttpRequest and clock - xhr.restore(); - clock.restore(); - global.XMLHttpRequest = originalXMLHttpRequest; - done() - }); +describe("sendLogs", () => { + it("sends logs on an interval", (done) => { + let requests = 0; + const originalXMLHttpRequest = global.XMLHttpRequest; + const conf = { + on: true, + transmitInterval: 500, + url: "test", + logCountThreshold: 2, + }; + const logs = []; + const clock = sinon.useFakeTimers(); + const xhr = sinon.useFakeXMLHttpRequest(); + global.XMLHttpRequest = xhr; + xhr.onCreate = () => { + requests++; + }; + + sendOnInterval(logs, conf); + + clock.tick(conf.transmitInterval * 2); + // Make sure it doesn't make requests for no raisin + expect(requests).to.equal(0); + + // Make sure it respects the logCountThreshold + logs.push({ foo: "bar1" }); + clock.tick(conf.transmitInterval); + expect(logs.length).to.equal(1); + + // Make sure it sends the logs and clears the array + logs.push({ foo: "bar2" }); + clock.tick(conf.transmitInterval); + expect(logs.length).to.equal(0); + expect(requests).to.equal(1); + + xhr.restore(); + clock.restore(); + global.XMLHttpRequest = originalXMLHttpRequest; + done(); + }); + + it("does not send logs if the config is off", (done) => { + let requests = 0; + const originalXMLHttpRequest = global.XMLHttpRequest; + const conf = { + on: true, + transmitInterval: 500, + url: "test", + logCountThreshold: 1, + }; + const logs = []; + const clock = sinon.useFakeTimers(); + const xhr = sinon.useFakeXMLHttpRequest(); + global.XMLHttpRequest = xhr; + xhr.onCreate = () => { + requests++; + }; + + sendOnInterval(logs, conf); + + // Make sure it respects the logCountThreshold + logs.push({ foo: "bar1" }); + clock.tick(conf.transmitInterval); + + expect(logs.length).to.equal(0); + expect(requests).to.equal(1); + + conf.on = false; + + logs.push({ foo: "bar2" }); + clock.tick(conf.transmitInterval); + expect(logs.length).to.equal(1); + expect(requests).to.equal(1); + + xhr.restore(); + clock.restore(); + global.XMLHttpRequest = originalXMLHttpRequest; + done(); + }); + + it("sends logs on page exit with fetch", () => { + const fetchSpy = sinon.spy(); + global.fetch = fetchSpy; + + sendOnClose([], { on: true, url: "test" }); + sendOnClose([{ foo: "bar" }], { on: true, url: "test" }); + global.window.dispatchEvent(new window.CustomEvent("pagehide")); + sinon.assert.calledOnce(fetchSpy); + }); + + it("does not send logs on page exit when config is off", () => { + const fetchSpy = sinon.spy(); + global.fetch = fetchSpy; + + sendOnClose([{ foo: "bar" }], { on: false, url: "test" }); + global.window.dispatchEvent(new window.CustomEvent("pagehide")); + sinon.assert.notCalled(fetchSpy); + }); + + it("sends logs with proper auth header when using registerAuthCallback", (done) => { + let requests = []; + const originalXMLHttpRequest = global.XMLHttpRequest; + const conf = { + on: true, + transmitInterval: 500, + url: "test", + logCountThreshold: 1, + }; + const logs = []; + const clock = sinon.useFakeTimers(); + const xhr = sinon.useFakeXMLHttpRequest(); + global.XMLHttpRequest = xhr; + xhr.onCreate = (xhr) => { + requests.push(xhr); + }; + + // Mock the authCallback function + const authCallback = sinon.stub().returns("fakeAuthToken"); + + // Register the authCallback + registerAuthCallback(authCallback); + + // Initialize sender with logs and config + initSender(logs, conf); + + // Simulate log entry + logs.push({ foo: "bar" }); + + // Trigger interval to send logs + clock.tick(conf.transmitInterval); + + // Verify that the request has the proper auth header + expect(requests.length).to.equal(1); + expect(requests[0].requestHeaders.Authorization).to.equal("fakeAuthToken"); + + // Restore XMLHttpRequest and clock + xhr.restore(); + clock.restore(); + global.XMLHttpRequest = originalXMLHttpRequest; + done(); + }); + + it("sends logs with proper custom headers when using registerHeadersCallback", (done) => { + let requests = []; + const originalXMLHttpRequest = global.XMLHttpRequest; + const conf = { + on: true, + transmitInterval: 500, + url: "test", + logCountThreshold: 1, + }; + const logs = []; + const clock = sinon.useFakeTimers(); + const xhr = sinon.useFakeXMLHttpRequest(); + global.XMLHttpRequest = xhr; + xhr.onCreate = (xhr) => { + requests.push(xhr); + }; + + // Mock the authCallback function + const customHeaders = { + "x-api-token": "someString", + "x-abc-def": "someOtherString", + }; + const headersCallback = sinon.stub().returns(customHeaders); + + // Register the authCallback + registerHeadersCallback(headersCallback); + + // Initialize sender with logs and config + initSender(logs, conf); + + // Simulate log entry + logs.push({ foo: "bar" }); + + // Trigger interval to send logs + clock.tick(conf.transmitInterval); + + // Verify that the request has the proper auth header + expect(requests.length).to.equal(1); + expect(requests[0].requestHeaders).to.containSubset(customHeaders); + + // Restore XMLHttpRequest and clock + xhr.restore(); + clock.restore(); + global.XMLHttpRequest = originalXMLHttpRequest; + done(); + }); }); diff --git a/test/testUtils.js b/test/testUtils.js index 0c84126d..18f42483 100644 --- a/test/testUtils.js +++ b/test/testUtils.js @@ -5,32 +5,36 @@ * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -import {JSDOM} from 'jsdom'; -import Storage from 'dom-storage'; +import { JSDOM } from "jsdom"; +import Storage from "dom-storage"; export function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } export async function createEnvFromFile(fileName, extraConfig = {}) { - const dom = await JSDOM.fromFile(__dirname + '/' + fileName, { - runScripts: 'dangerously', - resources: 'usable', - beforeParse: (window) => { - Object.defineProperty(window, 'localStorage', {value: new Storage(null, {strict: true})}) - Object.defineProperty(window, 'sessionStorage', { value: new Storage(null, {strict: true})}) - }, - ...extraConfig - }) - await sleep(100) // wait for external scripts to load - return dom; + const dom = await JSDOM.fromFile(__dirname + "/" + fileName, { + runScripts: "dangerously", + resources: "usable", + beforeParse: (window) => { + Object.defineProperty(window, "localStorage", { + value: new Storage(null, { strict: true }), + }); + Object.defineProperty(window, "sessionStorage", { + value: new Storage(null, { strict: true }), + }); + }, + ...extraConfig, + }); + await sleep(100); // wait for external scripts to load + return dom; }