├── .editorconfig ├── .eslintrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── node.js.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.json] 12 | insert_final_newline = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 2017, 5 | "sourceType": "module" 6 | }, 7 | 8 | "env": { 9 | "node": true, 10 | "es6": true 11 | }, 12 | 13 | "rules": { 14 | "array-bracket-spacing": ["error", "never"], 15 | "brace-style": ["error", "stroustrup", { 16 | "allowSingleLine": true 17 | }], 18 | "camelcase": ["error", { 19 | "properties": "never" 20 | }], 21 | "comma-spacing": ["error", { 22 | "before": false, 23 | "after": true 24 | }], 25 | "comma-style": ["error", "last"], 26 | "comma-dangle": ["error", "always-multiline"], 27 | "complexity": ["warn", 8], 28 | "computed-property-spacing": ["error", "never"], 29 | "consistent-return": "warn", 30 | "curly": ["error", "all"], 31 | "default-case": "error", 32 | "dot-notation": ["warn", { 33 | "allowKeywords": true 34 | }], 35 | "dot-location": ["error", "property"], 36 | "eol-last": "error", 37 | "eqeqeq": "error", 38 | "func-style": "off", 39 | "guard-for-in": "off", 40 | "handle-callback-err": ["error", "^(e|er|err|error)[0-9]{1,2}?$"], 41 | "indent": ["error", 2, { 42 | "SwitchCase": 1 43 | }], 44 | "keyword-spacing": "error", 45 | "key-spacing": ["error", { 46 | "beforeColon": false, 47 | "afterColon": true 48 | }], 49 | "lines-around-comment": ["error", { 50 | "beforeBlockComment": true, 51 | "afterBlockComment": true, 52 | "beforeLineComment": true, 53 | "afterLineComment": false, 54 | "allowBlockStart": true, 55 | "allowBlockEnd": false 56 | }], 57 | "linebreak-style": ["error", "unix"], 58 | "max-nested-callbacks": ["warn", 3], 59 | "new-cap": "off", 60 | "newline-after-var": ["error", "always"], 61 | "no-alert": "error", 62 | "no-caller": "error", 63 | "no-catch-shadow": "error", 64 | "no-delete-var": "error", 65 | "no-div-regex": "error", 66 | "no-duplicate-case": "error", 67 | "no-else-return": "error", 68 | "no-empty": "error", 69 | "no-empty-character-class": "error", 70 | "no-eval": "error", 71 | "no-extend-native": "error", 72 | "no-extra-semi": "error", 73 | "no-fallthrough": "error", 74 | "no-floating-decimal": "error", 75 | "no-func-assign": "error", 76 | "no-implied-eval": "error", 77 | "no-inline-comments": "error", 78 | "no-invalid-regexp": "error", 79 | "no-label-var": "error", 80 | "no-labels": "error", 81 | "no-lone-blocks": "error", 82 | "no-lonely-if": "error", 83 | "no-mixed-requires": "off", 84 | "no-mixed-spaces-and-tabs": "error", 85 | "no-multi-spaces": "error", 86 | "no-multi-str": "error", 87 | "no-multiple-empty-lines": ["error", { 88 | "max": 2 89 | }], 90 | "no-native-reassign": "error", 91 | "no-nested-ternary": "error", 92 | "no-new-func": "error", 93 | "no-new-object": "error", 94 | "no-new-wrappers": "error", 95 | "no-octal-escape": "error", 96 | "no-octal": "error", 97 | "no-path-concat": "error", 98 | "no-param-reassign": "off", 99 | "no-process-env": "off", 100 | "no-proto": "error", 101 | "no-redeclare": "error", 102 | "no-reserved-keys": "off", 103 | "no-return-assign": ["error", "always"], 104 | "no-self-compare": "error", 105 | "no-sequences": "error", 106 | "no-shadow": "error", 107 | "no-shadow-restricted-names": "error", 108 | "no-spaced-func": "off", 109 | "no-sparse-arrays": "warn", 110 | "no-sync": "warn", 111 | "no-ternary": "off", 112 | "no-throw-literal": "error", 113 | "no-trailing-spaces": "error", 114 | "no-undef": "error", 115 | "no-undef-init": "error", 116 | "no-undefined": "error", 117 | "no-underscore-dangle": "error", 118 | "no-unexpected-multiline": "error", 119 | "no-unneeded-ternary": "error", 120 | "no-unreachable": "error", 121 | "no-unused-vars": "warn", 122 | "no-use-before-define": "error", 123 | "no-useless-concat": "error", 124 | "no-warning-comments": "warn", 125 | "no-with": "error", 126 | "no-wrap-func": "off", 127 | "object-curly-spacing": ["error", "always", { 128 | "objectsInObjects": false, 129 | "arraysInObjects": false 130 | }], 131 | "one-var": ["error", "never"], 132 | "operator-assignment": ["error", "always"], 133 | "operator-linebreak": ["error", "before"], 134 | "padded-blocks": "off", 135 | "quote-props": ["error", "consistent"], 136 | "quotes": ["error", "single", "avoid-escape"], 137 | "radix": "error", 138 | "semi": "error", 139 | "semi-spacing": ["error", { 140 | "before": false, 141 | "after": true 142 | }], 143 | "space-before-blocks": ["error", "always"], 144 | "space-before-function-paren": ["error", "always"], 145 | "space-in-parens": ["error", "never"], 146 | "space-infix-ops": "error", 147 | "space-unary-ops": ["error", { 148 | "words": true, 149 | "nonwords": false 150 | }], 151 | "spaced-comment": ["error", "always"], 152 | "use-isnan": "error", 153 | "valid-typeof": "error", 154 | "vars-on-top": "error", 155 | "wrap-regex": "off", 156 | "yoda": ["error", "never"] 157 | } 158 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: "https://fvdm.com/donating" 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | assignees: 9 | - "fvdm" 10 | commit-message: 11 | prefix: "Chore(github)" 12 | 13 | - package-ecosystem: "npm" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | assignees: 18 | - "fvdm" 19 | commit-message: 20 | prefix: "Chore(deps)" 21 | prefix-development: "Chore(devdeps)" 22 | allow: 23 | dependency-type: "production" 24 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ develop, master ] 6 | pull_request: 7 | branches: [ develop ] 8 | schedule: 9 | - cron: '34 18 * * 6' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | language: [ 'javascript' ] 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v1 27 | with: 28 | languages: ${{ matrix.language }} 29 | 30 | - name: Perform CodeQL Analysis 31 | uses: github/codeql-action/analyze@v1 32 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: "Node.js" 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | 10 | jobs: 11 | lts_versions: 12 | name: "Get versions" 13 | runs-on: ubuntu-latest 14 | steps: 15 | - id: set-matrix 16 | run: echo "::set-output name=matrix::$(curl -s https://raw.githubusercontent.com/fvdm/nodejs-versions/main/lts.json)" 17 | outputs: 18 | matrix: ${{ steps.set-matrix.outputs.matrix }} 19 | 20 | build: 21 | name: "Node" 22 | needs: lts_versions 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | node-version: ${{ fromJson(needs.lts_versions.outputs.matrix) }} 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Test on Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v2 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | - run: git fetch --prune --unshallow 34 | - run: npm install 35 | - run: npm test 36 | env: 37 | GTMETRIX_EMAIL: ${{ secrets.GTMETRIX_EMAIL }} 38 | GTMETRIX_APIKEY: ${{ secrets.GTMETRIX_APIKEY }} 39 | GTMETRIX_TIMEOUT: ${{ secrets.GTMETRIX_TIMEOUT }} 40 | GTMETRIX_LOCATION: ${{ secrets.GTMETRIX_LOCATION }} 41 | GTMETRIX_BROWSER: ${{ secrets.GTMETRIX_BROWSER }} 42 | - run: npm audit --production 43 | - name: Coveralls Parallel 44 | uses: coverallsapp/github-action@master 45 | with: 46 | github-token: ${{ secrets.GITHUB_TOKEN }} 47 | flag-name: node-${{ matrix.node-version }} 48 | parallel: true 49 | 50 | finish: 51 | name: "Finish" 52 | needs: build 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Coveralls Finished 56 | uses: coverallsapp/github-action@master 57 | with: 58 | github-token: ${{ secrets.GITHUB_TOKEN }} 59 | parallel-finished: true 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | .*_history 4 | *.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | .package.json 30 | package-lock.json 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.3.0 (2018-03-06) 2 | 3 | ##### Documentation Changes 4 | 5 | * **readme:** 6 | * Add output examples ([a6f65647](https://github.com/fvdm/nodejs-gtmetrix/commit/a6f656472070c3dc21d340d7c2f3478b27cd9bf1)) 7 | * Callback arg is no longer required ([b20aa309](https://github.com/fvdm/nodejs-gtmetrix/commit/b20aa30925fa0ec0c8f6c9cdb4e94a73f32516bb)) 8 | * Minor edits ([0a1af5f0](https://github.com/fvdm/nodejs-gtmetrix/commit/0a1af5f0586398a39c800602f2f7567e77ae4a0c)) 9 | * Better parsable syntax ([3ace947d](https://github.com/fvdm/nodejs-gtmetrix/commit/3ace947d10aead442b25c09da0f37ef01fa98248)) 10 | * Removed result from example ([6e858c13](https://github.com/fvdm/nodejs-gtmetrix/commit/6e858c13ea5939d540bd3a0b7462e2b003d23f3f)) 11 | * Full code example ([545b7021](https://github.com/fvdm/nodejs-gtmetrix/commit/545b7021f86c7f9173a75af77414e252b6339b6d)) 12 | 13 | ##### New Features 14 | 15 | * **interface:** Promisified all methods ([54a74b59](https://github.com/fvdm/nodejs-gtmetrix/commit/54a74b59052c514dbf4b008f1eefae2f2ac8e1f3)) 16 | 17 | ##### Refactors 18 | 19 | * **interface:** Switched to es6-promisify ([4dfae869](https://github.com/fvdm/nodejs-gtmetrix/commit/4dfae869d147977959cbd3690529fa3ba6c58b1c)) 20 | * **package:** Minimum node version 8.0.0 ([ad64bb9a](https://github.com/fvdm/nodejs-gtmetrix/commit/ad64bb9a6744d9b4447096678628cf2a7ac2881d)) 21 | 22 | ##### Code Style Changes 23 | 24 | * **syntax:** Only set doRequest of httpreq ([f24606f6](https://github.com/fvdm/nodejs-gtmetrix/commit/f24606f64ba1d954ca6115e66a463394d130e4b3)) 25 | 26 | #### 1.2.7 (2018-03-02) 27 | 28 | ##### Bug Fixes 29 | 30 | * **config:** parseInt timeout and defaults ([db336a14](https://github.com/fvdm/nodejs-gtmetrix/commit/db336a142f4b582da931f45fb5f96ab6346138b0)) 31 | 32 | #### 1.2.6 (2018-03-01) 33 | 34 | ##### Documentation Changes 35 | 36 | * **badges:** Moved Greenkeeper to the badges ([e07b9f99](https://github.com/fvdm/nodejs-gtmetrix/commit/e07b9f99dfae648f47e1d10337e104572b88de4d)) 37 | 38 | ##### Bug Fixes 39 | 40 | * **account:** Account.status typo ([30619c8a](https://github.com/fvdm/nodejs-gtmetrix/commit/30619c8ac3e48b232fa4971c76c05e5c16704bfb)) 41 | * **response:** Missing references ([0930b8a8](https://github.com/fvdm/nodejs-gtmetrix/commit/0930b8a8aeb739bdd9e3bc7d961b62906904c046)) 42 | * **pollingCallback:** 43 | * Stop polling when error ([af3ccb0d](https://github.com/fvdm/nodejs-gtmetrix/commit/af3ccb0d12a01d063a2633ad6acfc8149ecbcc37)) 44 | * Syntax typo ([bc1434c6](https://github.com/fvdm/nodejs-gtmetrix/commit/bc1434c627b5a971ba7fc034db29679c6b2f987c)) 45 | * Enforce type string on error ([#14](https://github.com/fvdm/nodejs-gtmetrix/pull/14)) ([678bd227](https://github.com/fvdm/nodejs-gtmetrix/commit/678bd227ae7aefa82ac76bc86f27b12098417083)) 46 | * **polling:** Check err.error before match ([#14](https://github.com/fvdm/nodejs-gtmetrix/pull/14)) ([6fe16d0c](https://github.com/fvdm/nodejs-gtmetrix/commit/6fe16d0ca13b6f110a98862361f32b1d66f5eb59)) 47 | 48 | ##### Refactors 49 | 50 | * **main:** Cleaner interface statements ([91e06140](https://github.com/fvdm/nodejs-gtmetrix/commit/91e06140373d590524c15db0425a3f093055afdb)) 51 | * **testGet:** Reduced complexity ([6e1b2738](https://github.com/fvdm/nodejs-gtmetrix/commit/6e1b2738a268ba94d4788cfbb3c4a8f4b262de84)) 52 | * **pollingCallback:** 53 | * Reduced complexity ([c4bf94b2](https://github.com/fvdm/nodejs-gtmetrix/commit/c4bf94b22ab8b7408e6216c2264d341863d10a70)) 54 | * Rewrite response handling ([ad2bc570](https://github.com/fvdm/nodejs-gtmetrix/commit/ad2bc570297387a91f5dcb0dc049641483fca979)) 55 | * Clean up redundancy ([9de29973](https://github.com/fvdm/nodejs-gtmetrix/commit/9de29973915b9e1fb30ca76ad335f1022628115e)) 56 | 57 | ##### Tests 58 | 59 | * **main:** 60 | * Fixed bad ref ([bafaf5c2](https://github.com/fvdm/nodejs-gtmetrix/commit/bafaf5c250a1c1c47df630628096dc449d589e65)) 61 | * Fixed bad test var ([00059abe](https://github.com/fvdm/nodejs-gtmetrix/commit/00059abe91be356b0089035d06c2ada31dc57214)) 62 | * Add Interface test ([34209410](https://github.com/fvdm/nodejs-gtmetrix/commit/342094104466db7984343f3a2aef724c3ff141a2)) 63 | * **package:** Change dev deps to dotest ([cd347553](https://github.com/fvdm/nodejs-gtmetrix/commit/cd347553e8ef185c66b0c8a71423973a06f66790)) 64 | * **config:** Update Travis CI node versions ([32073c21](https://github.com/fvdm/nodejs-gtmetrix/commit/32073c21eb1b3e10f735e471f025c7711a63a8ad)) 65 | 66 | #### 1.2.5 (2017-09-09) 67 | 68 | ##### Chores 69 | 70 | * **repo:** Removed package-lock.json ([fef5aa2a](https://github.com/fvdm/nodejs-gtmetrix/commit/fef5aa2ac202e32e2948ed562365a4f5cdd1c271)) 71 | 72 | #### 1.2.4 (2017-7-8) 73 | 74 | ##### Chores 75 | 76 | * **repo:** Added package-lock.json ([194a2651](https://github.com/fvdm/nodejs-gtmetrix/commit/194a2651bf8972cbc85f2fc0cab54d998a160f20)) 77 | * **package:** 78 | * Clean up description and keywords ([7bdcf6da](https://github.com/fvdm/nodejs-gtmetrix/commit/7bdcf6dadf49867e46f1e5ec80cd6f76b136dcfe)) 79 | * Updated dependencies ([c9539360](https://github.com/fvdm/nodejs-gtmetrix/commit/c95393604fe0a5e4b855b7ec7ce18158e1d0d59e)) 80 | * update eslint to version 3.19.0 (#13) ([840d95d9](https://github.com/fvdm/nodejs-gtmetrix/commit/840d95d9968e8f6841c9fce8d5f0ef3dc173f9c6)) 81 | * update coveralls to version 2.13.0 (#12) ([e58acb3a](https://github.com/fvdm/nodejs-gtmetrix/commit/e58acb3a275750c1f39e9aaae4c93fd2a01ac0df)) 82 | * Update dotest dev dep ([ebf48a19](https://github.com/fvdm/nodejs-gtmetrix/commit/ebf48a19d26892103ba105f6d28ec2c228c7dd2d)) 83 | * Update dev deps ([52a28531](https://github.com/fvdm/nodejs-gtmetrix/commit/52a28531c801f64c2dc2b42188328da4bcf9d937)) 84 | 85 | ##### Documentation Changes 86 | 87 | * **readme:** 88 | * Minor clean up ([e78ae317](https://github.com/fvdm/nodejs-gtmetrix/commit/e78ae31716481a28911c86ba365775ed84c1d9fd)) 89 | * Add coffee button to Author ([e9c4194a](https://github.com/fvdm/nodejs-gtmetrix/commit/e9c4194ae4496809c8dc2cd4b65aa59ca89739df)) 90 | 91 | ##### Bug Fixes 92 | 93 | * **testGet:** Get resource with polling failed ([bb909295](https://github.com/fvdm/nodejs-gtmetrix/commit/bb909295afe122360992d5c7e1d1c9b4a0fc90af)) 94 | 95 | ##### Refactors 96 | 97 | * **testGet:** Prevent extra request on polling ([c5d9d5f3](https://github.com/fvdm/nodejs-gtmetrix/commit/c5d9d5f3c51b4b13f225a0e20ee47e4d513b5f91)) 98 | 99 | ##### Code Style Changes 100 | 101 | * **code:** Use ES6 arrow functions ([dc84c68b](https://github.com/fvdm/nodejs-gtmetrix/commit/dc84c68bdde80c01d4dacce88a8953020ff4236e)) 102 | * **comments:** Clean up JSDoc comments ([94badba7](https://github.com/fvdm/nodejs-gtmetrix/commit/94badba73973f1ca163075007d3d1fbb656cd386)) 103 | 104 | ##### Tests 105 | 106 | * **main:** Improved test coverage ([0270034e](https://github.com/fvdm/nodejs-gtmetrix/commit/0270034eb6624cbe3a3b73dd7c2af5fba98e9132)) 107 | * **config:** Add node v8 to Travis CI ([76489631](https://github.com/fvdm/nodejs-gtmetrix/commit/764896311816772fcd1ef79bf1f65a6413ac995b)) 108 | 109 | #### 1.2.3 (2017-3-23) 110 | 111 | ##### Chores 112 | 113 | * **package:** Update deps ([a86dd718](https://github.com/fvdm/nodejs-gtmetrix/commit/a86dd718547e97241bec05392838be52ef22ff52)) 114 | 115 | ##### Documentation Changes 116 | 117 | * **readme:** 118 | * Add resources table ([e4e013f4](https://github.com/fvdm/nodejs-gtmetrix/commit/e4e013f4fb0310b715690316b9e9be48a8d222ba)) 119 | * Removed encoding from resource code ([24a8826a](https://github.com/fvdm/nodejs-gtmetrix/commit/24a8826a340e19493d128b5a962c6e079a84deef)) 120 | 121 | ##### Bug Fixes 122 | 123 | * **testGet:** Improved resource handling ([380cc790](https://github.com/fvdm/nodejs-gtmetrix/commit/380cc7907473218f5c2f3bd98ed2d715718d217b)) 124 | 125 | #### 1.2.2 (2017-2-16) 126 | 127 | ##### Tests 128 | 129 | * **fix:** Fix test.get data.state check ([49b20bfd](https://github.com/fvdm/nodejs-gtmetrix/commit/49b20bfdd6ca9cd80034300e2528999c6d929a2c)) 130 | 131 | #### 1.2.1 (2017-2-16) 132 | 133 | ##### Chores 134 | 135 | * **develop:** Add .editorconfig file ([b9f433b7](https://github.com/fvdm/nodejs-gtmetrix/commit/b9f433b7cac2b55e274a1adb02fb13e6f1975e8e)) 136 | * **package:** Update dev dep ([6015cbed](https://github.com/fvdm/nodejs-gtmetrix/commit/6015cbed15bafba6abb17900ecb5a6485632c5af)) 137 | 138 | ##### Bug Fixes 139 | 140 | * **test.get:** Fix binary-mode on more resources ([e3817e23](https://github.com/fvdm/nodejs-gtmetrix/commit/e3817e23de41d0cce7db5ba868cb0ff510a69d06)) 141 | 142 | ##### Refactors 143 | 144 | * **test.get:** Cleaner loop condition ([3291bf6c](https://github.com/fvdm/nodejs-gtmetrix/commit/3291bf6c8588cd32563723a0e85a184af9d86cd9)) 145 | 146 | ##### Tests 147 | 148 | * **config:** 149 | * Update eslint config to ES6 ([b1506a97](https://github.com/fvdm/nodejs-gtmetrix/commit/b1506a974bd87ffd45a174d48f716c5820debb44)) 150 | * bitHound max 500 lines ([8ddd3b7b](https://github.com/fvdm/nodejs-gtmetrix/commit/8ddd3b7b19213ee479a6cf36792cdc10dbc9e6a5)) 151 | * **main:** Add API error test with polling ([3af45111](https://github.com/fvdm/nodejs-gtmetrix/commit/3af45111eba00829cd6e8ef2c8355aee68d2fdc1)) 152 | * **fix:** Don’t overwrite cache with data ([3460c768](https://github.com/fvdm/nodejs-gtmetrix/commit/3460c768a756db76deefb5c25bc57322ce4bebcc)) 153 | 154 | ### 1.2.0 (2017-2-16) 155 | 156 | ##### Documentation Changes 157 | 158 | * **readme:** Add test.get polling argument #9 ([52887ff6](https://github.com/fvdm/nodejs-gtmetrix/commit/52887ff62e37271249bded78447cdff8d1c848ca)) 159 | 160 | ##### New Features 161 | 162 | * **test.get:** Add auto polling argument ([8eed2378](https://github.com/fvdm/nodejs-gtmetrix/commit/8eed23787a6e4597d98ac6981f24e26f4844f32c)) 163 | 164 | ##### Tests 165 | 166 | * **style:** Use dotest test() function ([634beb54](https://github.com/fvdm/nodejs-gtmetrix/commit/634beb54ee693bd2a954a64eab6db4f73c416272)) 167 | * **fix:** Fixed browser.get data.id type ([9d163261](https://github.com/fvdm/nodejs-gtmetrix/commit/9d1632610d69d506ba11e02b2b99a4c13f640b95)) 168 | * **main:** Add polling test ([8af63f88](https://github.com/fvdm/nodejs-gtmetrix/commit/8af63f88f25f07577d7fca7635deafb0bbab5955)) 169 | 170 | #### 1.1.5 (2017-2-16) 171 | 172 | ##### Documentation Changes 173 | 174 | * **badges:** Fix typo in repo name ([aea0385b](https://github.com/fvdm/nodejs-gtmetrix/commit/aea0385b4d86c5eb960dee7140c42a6c20aad0ac)) 175 | 176 | #### 1.1.4 (2017-2-16) 177 | 178 | ##### Documentation Changes 179 | 180 | * **badges:** 181 | * Fix wrong copy/paste ([47b9c630](https://github.com/fvdm/nodejs-gtmetrix/commit/47b9c6302fcfa3e5e7255cdf2cbbd75b999dac16)) 182 | * Fix coverage status branch ([f6ef03f1](https://github.com/fvdm/nodejs-gtmetrix/commit/f6ef03f1083c56c30bc4211dbfa0941269385f71)) 183 | 184 | #### 1.1.3 (2017-2-16) 185 | 186 | ##### Refactors 187 | 188 | * **apiRequest:** Timeout must not be replaced ([06e0ed79](https://github.com/fvdm/nodejs-gtmetrix/commit/06e0ed79dd2e11f3b90122feff6594fadbc223f7)) 189 | * **apiResponse:** No default for props.method ([051fa477](https://github.com/fvdm/nodejs-gtmetrix/commit/051fa4772e44561387f22f245e85eca97425532a)) 190 | * **errors:** Moved cb errors to doError() ([72c26638](https://github.com/fvdm/nodejs-gtmetrix/commit/72c26638ac29be52777bb7ab10c4cac8fd51c6fc)) 191 | 192 | ##### Tests 193 | 194 | * **main:** Added test Error request failed ([74cfa98a](https://github.com/fvdm/nodejs-gtmetrix/commit/74cfa98ab0c898c3218ab58a6857c089ee2fa063)) 195 | 196 | #### 1.1.2 (2017-2-16) 197 | 198 | ##### Chores 199 | 200 | * **develop:** 201 | * Added bitHound config ([07b12fdf](https://github.com/fvdm/nodejs-gtmetrix/commit/07b12fdfd3876ae5634b63604f63852519e5a321)) 202 | * ESLint config for ES6, minor edit ([befbb042](https://github.com/fvdm/nodejs-gtmetrix/commit/befbb042a3049035b45729bd41e790212287dabc)) 203 | * Added _history files to gitignore ([3d336191](https://github.com/fvdm/nodejs-gtmetrix/commit/3d336191a33546ab5d48264a88cc71ed7fa87775)) 204 | * **package:** 205 | * Minor clean up ([c9fece0d](https://github.com/fvdm/nodejs-gtmetrix/commit/c9fece0de8a434fa1d1b4e96904fa1182c9df261)) 206 | * Update httpreq dep ([63c0bc58](https://github.com/fvdm/nodejs-gtmetrix/commit/63c0bc58368ddd01068a24399435b0940c9990af)) 207 | * Replaced test runner and dev deps by dotest ([bf02723b](https://github.com/fvdm/nodejs-gtmetrix/commit/bf02723bfc523ab6e00d95ccbe2af28064b64610)) 208 | * update eslint to version 3.0.0 ([72cddc5d](https://github.com/fvdm/nodejs-gtmetrix/commit/72cddc5de3153aa7d4d0138944db593ead7341b5)) 209 | * update eslint to version 2.5.0 ([014b20ac](https://github.com/fvdm/nodejs-gtmetrix/commit/014b20acf34a72ebe314e8260db2e6263590a837)) 210 | 211 | ##### Documentation Changes 212 | 213 | * **badges:** 214 | * Added code quality status ([4ade2453](https://github.com/fvdm/nodejs-gtmetrix/commit/4ade24539766396aee900f2a1efefbceb6dd22ae)) 215 | * Added coverage status ([4beec872](https://github.com/fvdm/nodejs-gtmetrix/commit/4beec872415b8ba0cca8ea9a118fca54b776e651)) 216 | * Replaced Gemnasium with bitHound ([a0d88bc2](https://github.com/fvdm/nodejs-gtmetrix/commit/a0d88bc295311c438012b89a6f16fefe85589b32)) 217 | * Add Gemnasium dependencies status ([48fd8ee0](https://github.com/fvdm/nodejs-gtmetrix/commit/48fd8ee0462661bce296e35574319908a4ae6844)) 218 | * Add npm version for changelog ([54e5449a](https://github.com/fvdm/nodejs-gtmetrix/commit/54e5449a314f6a09e630215a43664e4538f62d37)) 219 | * **readme:** Reduced author footnote ([2cb1bc44](https://github.com/fvdm/nodejs-gtmetrix/commit/2cb1bc443f6f968ccdf5e68ecdf859913ef322a6)) 220 | 221 | ##### Other Changes 222 | 223 | * **undefined:** 224 | * always run both test commands ([befba658](https://github.com/fvdm/nodejs-gtmetrix/commit/befba6586ddfd5e899772b26ed8fdd0e611ddabb)) 225 | * minimum dotest v1.5.0 ([d30bbbbd](https://github.com/fvdm/nodejs-gtmetrix/commit/d30bbbbdc41a842821f751e57b291498bd1058e8)) 226 | * dev dep eslint 2.5.0 is broken ([78e87ac5](https://github.com/fvdm/nodejs-gtmetrix/commit/78e87ac55681b862afc2f8d7221fc0ba1b298822)) 227 | 228 | ##### Refactors 229 | 230 | * **main:** Rewrite to cleaner code style ([2494004b](https://github.com/fvdm/nodejs-gtmetrix/commit/2494004b2c340719e57c1700f7b497a2c84d6ea8)) 231 | * **package:** Minimum supported node v4.0 ([665aeb7b](https://github.com/fvdm/nodejs-gtmetrix/commit/665aeb7b2f192d6731dd525096ac16ecb1e51985)) 232 | 233 | ##### Tests 234 | 235 | * **config:** 236 | * Update Travis node versions ([c467c098](https://github.com/fvdm/nodejs-gtmetrix/commit/c467c098214c34be1e7e86805fae0a6003155aa6)) 237 | * Use dynamic node versions on Travis CI ([53ada208](https://github.com/fvdm/nodejs-gtmetrix/commit/53ada2089b2e4f9f6404ad01d3a477833559e1cd)) 238 | * **lint:** Update eslint to ES6 ([902b1878](https://github.com/fvdm/nodejs-gtmetrix/commit/902b1878b26522551924aa2395b9be4ade64f4f0)) 239 | * **undefined:** 240 | * add node v6 to Travis config ([7b36f34c](https://github.com/fvdm/nodejs-gtmetrix/commit/7b36f34c986c3f18a7d9a9655d23bd5f056d7a62)) 241 | * wait one second between requests ([ae9c9043](https://github.com/fvdm/nodejs-gtmetrix/commit/ae9c9043551383e4d7c532a351c0f6383eb452b5)) 242 | * don't fail on incomplete response data ([d712e048](https://github.com/fvdm/nodejs-gtmetrix/commit/d712e048d8696b391f3d139c345cfecccc248b20)) 243 | 244 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gtmetrix 2 | 3 | Node.js module for the GTmetrix API to run and access tests. 4 | 5 | [![npm](https://img.shields.io/npm/v/gtmetrix.svg?maxAge=3600)](https://github.com/fvdm/nodejs-gtmetrix/blob/master/CHANGELOG.md) 6 | [![Build Status](https://github.com/fvdm/nodejs-gtmetrix/actions/workflows/node.js.yml/badge.svg?branch=master)](https://github.com/fvdm/nodejs-gtmetrix/actions/workflows/node.js.yml) 7 | [![Coverage Status](https://coveralls.io/repos/github/fvdm/nodejs-gtmetrix/badge.svg?branch=master)](https://coveralls.io/github/fvdm/nodejs-gtmetrix?branch=master) 8 | 9 | * [Node.js](https://nodejs.org) 10 | * [GTMetrix](https://gtmetrix.com) 11 | * [API documentation](https://gtmetrix.com/api/) 12 | 13 | 14 | ## Example 15 | 16 | ```js 17 | const gtmetrix = require ('gtmetrix') ({ 18 | email: 'your@mail.tld', 19 | apikey: 'abc123' 20 | }); 21 | 22 | // Run test from London with Google Chrome 23 | const testDetails = { 24 | url: 'http://example.net/', 25 | location: 2, 26 | browser: 3 27 | }; 28 | 29 | // Poll test every 5 seconds for completion, then log the result 30 | gtmetrix.test.create (testDetails).then (data => 31 | gtmetrix.test.get (data.test_id, 5000).then (console.log)); 32 | ``` 33 | 34 | _(For readability I left out the error handling)_ 35 | 36 | 37 | ## Installation 38 | 39 | `npm i gtmetrix` 40 | 41 | You need an account at GTmetrix to get an API key. 42 | The API key can be found [here](https://gtmetrix.com/api/#api-details) when you 43 | are logged in. 44 | 45 | 46 | ## Configuration 47 | 48 | The setup function takes an _object_ with these settings. 49 | 50 | name | type | required | default | description 51 | :-------|:--------|:---------|:--------|:-------------------- 52 | email | string | yes | | Your account email 53 | apikey | string | yes | | Your account API key 54 | timeout | int | no | 5000 | Wait timeout in ms 55 | 56 | 57 | #### Example 58 | 59 | ```js 60 | const gtmetrix = require ('gtmetrix') ({ 61 | email: 'your@mail.tld', 62 | apikey: 'abc123', 63 | timeout: 10000 64 | }); 65 | ``` 66 | 67 | 68 | ## Methods 69 | 70 | All methods return promises, but you can also provide a callback function 71 | instead which gets the standard `err` and `data` arguments. 72 | 73 | In the examples below I use a mix of callbacks and promises, but each method 74 | can do both. I also left out the error handling for better readability. 75 | 76 | 77 | #### Errors 78 | 79 | message | description | properties 80 | :----------------|:-----------------------|:---------------------------------- 81 | request failed | Request cannot be made | `error` 82 | invalid response | Can't process response | `error` `statusCode` `contentType` 83 | API error | API returned an error | `error` `statusCode` `contentType` 84 | 85 | 86 | ### test.create 87 | **( params, [callback] )** 88 | 89 | Run a test. 90 | 91 | argument | type | required | description 92 | :--------|:---------|:---------|:----------------- 93 | params | object | yes | Test settings 94 | callback | function | no | `(err, data)` or promise 95 | 96 | [API documentation](https://gtmetrix.com/api/#api-test-start) 97 | 98 | 99 | ```js 100 | // Run test from London with Google Chrome 101 | const test = { 102 | url: 'http://example.net/', 103 | location: 2, 104 | browser: 3 105 | }; 106 | 107 | gtmetrix.test.create (test, console.log); 108 | ``` 109 | 110 | ```js 111 | { credits_left: 68, 112 | test_id: 'Ao0AYQbz', 113 | poll_state_url: 'https://gtmetrix.com/api/0.1/test/Ao0AYQbz' } 114 | ``` 115 | 116 | 117 | ### test.get 118 | **( testId, [resource], [polling], [callback] )** 119 | 120 | Get details about a test or one of its resources. 121 | 122 | When you specify a binary resource, i.e. `screenshot`, 123 | the callback `data` will be a _Buffer_ instance, so you can 124 | post-process the binary data however you like. See example below. 125 | 126 | argument | type | required | description 127 | :--------|:---------|:---------|:----------------------------- 128 | testId | string | yes | Test `id` to look up 129 | resource | string | no | Retrieve a test resource 130 | polling | int | no | Retry until completion, in ms 131 | callback | function | no | `(err, data)` or promise 132 | 133 | [API documentation](https://gtmetrix.com/api/#api-test-state) 134 | 135 | 136 | #### Test details 137 | 138 | Get what is currently available, without waiting for completion. 139 | 140 | ```js 141 | gtmetrix.test.get ('Ao0AYQbz', console.log); 142 | ``` 143 | 144 | ```js 145 | { resources: {}, error: '', results: {}, state: 'started' } 146 | ``` 147 | 148 | 149 | Wait for completion and then get the details. 150 | 151 | ```js 152 | // Retry every 5 seconds (5000 ms) 153 | gtmetrix.test.get ('Ao0AYQbz', 5000, console.log); 154 | ``` 155 | 156 | ```js 157 | { resources: 158 | { report_pdf: 'https://gtmetrix.com/api/0.1/test/Ao0AYQbz/report-pdf', 159 | pagespeed: 'https://gtmetrix.com/api/0.1/test/Ao0AYQbz/pagespeed', 160 | har: 'https://gtmetrix.com/api/0.1/test/Ao0AYQbz/har', 161 | pagespeed_files: 'https://gtmetrix.com/api/0.1/test/Ao0AYQbz/pagespeed-files', 162 | report_pdf_full: 'https://gtmetrix.com/api/0.1/test/Ao0AYQbz/report-pdf?full=1', 163 | yslow: 'https://gtmetrix.com/api/0.1/test/Ao0AYQbz/yslow', 164 | screenshot: 'https://gtmetrix.com/api/0.1/test/Ao0AYQbz/screenshot' }, 165 | error: '', 166 | results: 167 | { onload_time: 185, 168 | first_contentful_paint_time: 221, 169 | page_elements: 2, 170 | report_url: 'https://gtmetrix.com/reports/example.net/Ao0AYQbz', 171 | redirect_duration: 0, 172 | first_paint_time: 221, 173 | dom_content_loaded_duration: null, 174 | dom_content_loaded_time: 184, 175 | dom_interactive_time: 183, 176 | page_bytes: 1911, 177 | page_load_time: 185, 178 | html_bytes: 277, 179 | fully_loaded_time: 307, 180 | html_load_time: 145, 181 | rum_speed_index: 221, 182 | yslow_score: 99, 183 | pagespeed_score: 99, 184 | backend_duration: 68, 185 | onload_duration: 0, 186 | connect_duration: 77 }, 187 | state: 'completed' } 188 | ``` 189 | 190 | 191 | #### Retrieve screenshot 192 | 193 | Retry every 5000 ms until it's ready. 194 | 195 | ```js 196 | const fs = require ('fs'); 197 | 198 | gtmetrix.test.get ('Ao0AYQbz', 'screenshot', 5000).then (data => 199 | fs.writeFile (__dirname + '/screenshot.jpg', data, console.log)); 200 | ``` 201 | 202 | 203 | ##### Resources 204 | 205 | resource | binary | content | description 206 | :---------------|:-------|:----------|:--------------------------------------- 207 | filmstrip | yes | JPEG | Page loading filmstrip (requires video) 208 | har | no | JS object | HTTP Archive 209 | pagespeed | no | JS object | Pagespeed report 210 | pagespeed-files | yes | ZIP | Pagespeed optimized files 211 | report-pdf | yes | PDF | Test summary 212 | report-pdf-full | yes | PDF | Full test report 213 | screenshot | yes | JPEG | Screenshot image 214 | video | yes | MP4 | Page loading video 215 | yslow | no | JS object | YSlow report 216 | 217 | 218 | ### locations.list 219 | **( [callback] )** 220 | 221 | Get a list of available test locations. 222 | 223 | argument | type | required | description 224 | :--------|:---------|:---------|:------------------------ 225 | callback | function | no | `(err, data)` or promise 226 | 227 | [API documentation](https://gtmetrix.com/api/#api-locations) 228 | 229 | 230 | ```js 231 | gtmetrix.locations.list (console.log); 232 | ``` 233 | 234 | ```js 235 | [ { name: 'Vancouver, Canada', 236 | default: true, 237 | id: '1', 238 | browsers: [ 1, 3 ] } ] 239 | ``` 240 | 241 | 242 | ### browsers.list 243 | **( [callback] )** 244 | 245 | Get a list of available test browsers. 246 | 247 | argument | type | required | description 248 | :--------|:---------|:---------|:------------------------ 249 | callback | function | no | `(err, data)` or promise 250 | 251 | [API documentation](https://gtmetrix.com/api/#api-browsers) 252 | 253 | 254 | ```js 255 | gtmetrix.browsers.list (console.log); 256 | ``` 257 | 258 | ```js 259 | [ { features: 260 | { dns: true, 261 | cookies: true, 262 | adblock: true, 263 | http_auth: true, 264 | video: true, 265 | user_agent: true, 266 | throttle: true, 267 | filtering: true, 268 | resolution: true }, 269 | browser: 'firefox', 270 | name: 'Firefox (Desktop)', 271 | platform: 'desktop', 272 | id: 1, 273 | device: '' } ] 274 | ``` 275 | 276 | 277 | ### browsers.get 278 | **( browserId, [callback] )** 279 | 280 | Get details about a test browser. 281 | 282 | argument | type | required | description 283 | :---------|:---------|:---------|:----------------------- 284 | browserId | int | yes | Browser to look up 285 | callback | function | no | `(err, data)` or promise 286 | 287 | [API documentation](https://gtmetrix.com/api/#api-browsers-details) 288 | 289 | 290 | ```js 291 | gtmetrix.browsers.get (3, console.log); 292 | ``` 293 | 294 | ```js 295 | { features: 296 | { dns: true, 297 | cookies: true, 298 | adblock: true, 299 | http_auth: true, 300 | video: true, 301 | user_agent: true, 302 | throttle: true, 303 | filtering: true, 304 | resolution: true }, 305 | browser: 'chrome', 306 | name: 'Chrome (Desktop)', 307 | platform: 'desktop', 308 | id: 3, 309 | device: '' } 310 | ``` 311 | 312 | 313 | ### account.status 314 | **( [callback] )** 315 | 316 | Information about your account. 317 | 318 | argument | type | description 319 | :----------|:---------|:----------- 320 | [callback] | function | `(err, data)` or promise 321 | 322 | [API documentation](https://gtmetrix.com/api/#api-status) 323 | 324 | 325 | ```js 326 | gtmetrix.account.status (console.log); 327 | ``` 328 | 329 | ```js 330 | { api_refill: 1234567890, api_credits: 68 } 331 | ``` 332 | 333 | 334 | ## Unlicense 335 | 336 | This is free and unencumbered software released into the public domain. 337 | 338 | Anyone is free to copy, modify, publish, use, compile, sell, or 339 | distribute this software, either in source code form or as a compiled 340 | binary, for any purpose, commercial or non-commercial, and by any 341 | means. 342 | 343 | In jurisdictions that recognize copyright laws, the author or authors 344 | of this software dedicate any and all copyright interest in the 345 | software to the public domain. We make this dedication for the benefit 346 | of the public at large and to the detriment of our heirs and 347 | successors. We intend this dedication to be an overt act of 348 | relinquishment in perpetuity of all present and future rights to this 349 | software under copyright law. 350 | 351 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 352 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 353 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 354 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 355 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 356 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 357 | OTHER DEALINGS IN THE SOFTWARE. 358 | 359 | For more information, please refer to 360 | 361 | 362 | ## Author 363 | 364 | [Franklin](https://fvdm.com) 365 | | [Buy me a coffee](https://fvdm.com/donating) 366 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { doRequest } = require ('httpreq'); 2 | const { promisify } = require ('es6-promisify'); 3 | 4 | 5 | // Default config 6 | const defaults = { 7 | email: null, 8 | apikey: null, 9 | timeout: 5000, 10 | }; 11 | 12 | let pkg = { 13 | test: { 14 | get: null, 15 | }, 16 | locations: {}, 17 | browsers: {}, 18 | account: {}, 19 | }; 20 | 21 | let config = {}; 22 | 23 | 24 | /** 25 | * Get info about resource type 26 | * 27 | * @param {string} name The resource name 28 | * @return {object} Resource info object 29 | */ 30 | 31 | function resourceType (name) { 32 | let info = { 33 | binary: false, 34 | path: name.replace ('_', '-'), 35 | }; 36 | 37 | if (info.path.match (/^(filmstrip|pagespeed-files|report-pdf(-full)?|screenshot|video)$/)) { 38 | info.binary = true; 39 | } 40 | 41 | if (info.path === 'report-pdf-full') { 42 | info.path = 'report-pdf?full=1'; 43 | } 44 | 45 | return info; 46 | } 47 | 48 | 49 | /** 50 | * Make an error 51 | * 52 | * @return {Error} 53 | * 54 | * @param {string} msg Error.message 55 | * @param {mixed} err Error.error 56 | * @param {number|null} code Error.statusCode 57 | * @param {string|null} type Error.contentType 58 | */ 59 | 60 | function doError (msg, err, code, type) { 61 | let error = new Error (msg); 62 | 63 | error.error = err; 64 | error.statusCode = code; 65 | error.contentType = type; 66 | return error; 67 | } 68 | 69 | 70 | /** 71 | * Process API response 72 | * 73 | * @callback callback 74 | * @return {void} 75 | * 76 | * @param {object} options httpreq options 77 | * @param {Error|null} err httpreq Error 78 | * @param {object} res httpreq response 79 | * @param {function} callback `(err, data)` 80 | */ 81 | 82 | function apiResponse (options, err, res, callback) { 83 | let type; 84 | let size; 85 | let code; 86 | let data; 87 | let error = null; 88 | 89 | if (err) { 90 | error = doError ('request failed', err, null, null); 91 | callback (error); 92 | return; 93 | } 94 | 95 | type = String (res.headers ['content-type']) .split (';') [0]; 96 | size = String (res.headers ['content-length']); 97 | code = res.statusCode; 98 | data = res.body; 99 | 100 | // Received data, expecting binary 101 | if (size && options.binary && type.match (/\/(pdf|jpeg|tar)$/)) { 102 | callback (null, data); 103 | return; 104 | } 105 | 106 | // Received something else 107 | try { 108 | data = JSON.parse (data); 109 | 110 | if (data.error) { 111 | error = doError ('API error', data.error, code, type); 112 | callback (error); 113 | return; 114 | } 115 | } 116 | catch (e) { 117 | error = doError ('invalid response', e, code, type); 118 | callback (error); 119 | return; 120 | } 121 | 122 | // It's real data 123 | callback (null, data); 124 | } 125 | 126 | 127 | /** 128 | * Send API request 129 | * 130 | * @callback callback 131 | * @return {void} 132 | * 133 | * @param {object} props 134 | * @param {boolean} [props.binary=false] Expect binary response 135 | * @param {string} props.method HTTP method 136 | * @param {object} [props.params] Method parameters 137 | * @param {string} props.path Method path 138 | * @param {function} callback `(err, data)` 139 | */ 140 | 141 | function apiRequest (props, callback) { 142 | const options = { 143 | url: 'https://gtmetrix.com/api/0.1/' + props.path, 144 | parameters: props.params || null, 145 | method: props.method, 146 | headers: { 147 | 'User-Agent': 'gtmetrix.js (https://www.npmjs.com/package/gtmetrix)', 148 | }, 149 | timeout: parseInt (config.timeout, 10) || defaults.timeout, 150 | auth: config.email + ':' + config.apikey, 151 | binary: props.binary || false, 152 | }; 153 | 154 | doRequest (options, (err, res) => { 155 | apiResponse (options, err, res, callback); 156 | }); 157 | } 158 | 159 | 160 | /** 161 | * Create test 162 | * 163 | * @callback callback 164 | * @return {Promise} 165 | * 166 | * @param {object} params 167 | * @param {function} [callback] `(err, data)` 168 | */ 169 | 170 | pkg.test.create = promisify ((params, callback) => { 171 | const props = { 172 | method: 'POST', 173 | path: 'test', 174 | params: params, 175 | }; 176 | 177 | apiRequest (props, callback); 178 | }); 179 | 180 | 181 | /** 182 | * Process callback when polling 183 | * 184 | * @callback callback 185 | * @return {boolean} Stop polling? true = yes 186 | * 187 | * @param {object} props Request properties 188 | * @param {Error|null} err Response error 189 | * @param {object} data Response data 190 | * @param {function} callback `(err, data)` 191 | */ 192 | 193 | function pollingCallback (props, err, data, callback) { 194 | // API error saying we need to wait 195 | if (err && String (err.error).match (/Data not yet available/)) { 196 | return false; 197 | } 198 | 199 | // Another API error 200 | if (err) { 201 | callback (err); 202 | return true; 203 | } 204 | 205 | // No API error, binary expected = ok complete 206 | if (props.binary) { 207 | callback (null, data); 208 | return true; 209 | } 210 | 211 | // No error, non-binary, not running = ok complete 212 | if (!String (data.state).match (/^(started|queued)$/)) { 213 | callback (null, data); 214 | return true; 215 | } 216 | 217 | // else keep polling 218 | return false; 219 | } 220 | 221 | 222 | /** 223 | * Process test response 224 | * 225 | * @callback callback 226 | * @return {void} 227 | * 228 | * @param {object} params 229 | * @param {bool} params.polling Keep polling for updates 230 | * @param {string} params.testId Test ID 231 | * @param {string} params.resource Test resource 232 | * @param {object} params.props Request params 233 | * @param {Error|null} err Response error 234 | * @param {mixed} data Response data 235 | * @param {function} callback `(err, data)` 236 | */ 237 | 238 | function testResponse (params, err, data, callback) { 239 | let retryInterval; 240 | let complete; 241 | 242 | if (err && !params.polling) { 243 | callback (err); 244 | return; 245 | } 246 | 247 | if (!params.polling) { 248 | callback (null, data); 249 | return; 250 | } 251 | 252 | if (params.polling === true) { 253 | params.polling = 5000; 254 | } 255 | 256 | if (typeof params.polling === 'number') { 257 | complete = pollingCallback (params.props, err, data, callback); 258 | 259 | if (complete) { 260 | return; 261 | } 262 | 263 | // Test is still running 264 | retryInterval = setInterval (() => { 265 | pkg.test.get (params.testId, params.resource, (pErr, pData) => { 266 | const pComplete = pollingCallback (params.props, pErr, pData, callback); 267 | 268 | if (pComplete) { 269 | clearInterval (retryInterval); 270 | } 271 | }); 272 | }, params.polling); 273 | } 274 | } 275 | 276 | 277 | /** 278 | * Get test result 279 | * 280 | * @callback callback 281 | * @return {Promise} 282 | * 283 | * @param {string} testId Test ID 284 | * @param {string} [resource] Resource to get, i.e. `screenshot` 285 | * @param {number} [polling] Poll state until completion, in ms 286 | * @param {function} [callback] `(err, data)` 287 | */ 288 | 289 | pkg.test.get = promisify ((testId, resource, polling, callback) => { 290 | let resourceInfo = {}; 291 | let params = { 292 | testId, 293 | resource, 294 | polling, 295 | props: { 296 | method: 'GET', 297 | path: 'test/' + testId, 298 | }, 299 | }; 300 | 301 | if (typeof polling === 'function') { 302 | callback = polling; 303 | params.polling = null; 304 | } 305 | 306 | switch (typeof resource) { 307 | case 'function': 308 | callback = resource; 309 | params.resource = null; 310 | params.polling = null; 311 | break; 312 | 313 | case 'number': 314 | params.polling = resource; 315 | params.resource = null; 316 | break; 317 | 318 | case 'string': 319 | resourceInfo = resourceType (resource); 320 | params.props.path += '/' + resourceInfo.path; 321 | params.props.binary = resourceInfo.binary; 322 | break; 323 | 324 | default: 325 | break; 326 | } 327 | 328 | apiRequest (params.props, (err, data) => { 329 | testResponse (params, err, data, callback); 330 | }); 331 | }); 332 | 333 | 334 | /** 335 | * List locations 336 | * 337 | * @callback callback 338 | * @return {Promise} 339 | * 340 | * @param {function} [callback] `(err, data)` 341 | */ 342 | 343 | pkg.locations.list = promisify ((callback) => { 344 | const props = { 345 | method: 'GET', 346 | path: 'locations', 347 | }; 348 | 349 | apiRequest (props, callback); 350 | }); 351 | 352 | 353 | /** 354 | * List browsers 355 | * 356 | * @callback callback 357 | * @return {Promise} 358 | * 359 | * @param {function} [callback] `(err, data)` 360 | */ 361 | 362 | pkg.browsers.list = promisify ((callback) => { 363 | const props = { 364 | method: 'GET', 365 | path: 'browsers', 366 | }; 367 | 368 | apiRequest (props, callback); 369 | }); 370 | 371 | 372 | /** 373 | * Get browser 374 | * 375 | * @callback callback 376 | * @return {Promise} 377 | * 378 | * @param {string} browserId 379 | * @param {function} [callback] `(err, data)` 380 | */ 381 | 382 | pkg.browsers.get = promisify ((browserId, callback) => { 383 | const props = { 384 | method: 'GET', 385 | path: 'browsers/' + browserId, 386 | }; 387 | 388 | apiRequest (props, callback); 389 | }); 390 | 391 | 392 | /** 393 | * Get account status 394 | * 395 | * @callback callback 396 | * @return {Promise} 397 | * 398 | * @param {function} [callback] `(err, data)` 399 | */ 400 | 401 | pkg.account.status = promisify ((callback) => { 402 | const props = { 403 | method: 'GET', 404 | path: 'status', 405 | }; 406 | 407 | apiRequest (props, callback); 408 | }); 409 | 410 | 411 | /** 412 | * Module interface 413 | * 414 | * @return {object} Methods 415 | * 416 | * @param {object} props 417 | * @param {string} props.email API email 418 | * @param {string} props.apikey API key 419 | * @param {number} [props.timeout=5000] Request timeut in ms 420 | */ 421 | 422 | module.exports = (props) => { 423 | let key; 424 | 425 | for (key in props) { 426 | config [key] = props [key]; 427 | } 428 | 429 | return pkg; 430 | }; 431 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Franklin", 4 | "email": "info@fvdm.com", 5 | "url": "https://fvdm.com" 6 | }, 7 | "name": "gtmetrix", 8 | "description": "Run and access GTmetrix tests to measure website performance (unofficial)", 9 | "version": "1.3.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/fvdm/nodejs-gtmetrix.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/fvdm/nodejs-gtmetrix/issues" 16 | }, 17 | "main": "index.js", 18 | "dependencies": { 19 | "es6-promisify": "^6.0.0", 20 | "httpreq": "^0.5.1" 21 | }, 22 | "devDependencies": { 23 | "dotest": "^2.3.0" 24 | }, 25 | "engines": { 26 | "node": ">=12" 27 | }, 28 | "keywords": [ 29 | "api", 30 | "gtmetrix", 31 | "meassure", 32 | "metrics", 33 | "pagespeed", 34 | "performance", 35 | "screenshot", 36 | "website", 37 | "yslow" 38 | ], 39 | "license": "Unlicense", 40 | "scripts": { 41 | "test": "dotest" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var path = require ('path'); 2 | var dotest = require ('dotest'); 3 | var app = require (path.join (__dirname, path.sep)); 4 | 5 | 6 | // Setup 7 | // $ GTMETRIX_EMAIL= GTMETRIX_APIKEY= npm test 8 | var email = process.env.GTMETRIX_EMAIL; 9 | var apikey = process.env.GTMETRIX_APIKEY; 10 | var timeout = String (process.env.GTMETRIX_TIMEOUT); 11 | var location = process.env.GTMETRIX_LOCATION || 2; 12 | var browser = process.env.GTMETRIX_BROWSER || 3; 13 | 14 | var gtmetrix = app ({ 15 | email: email, 16 | apikey: apikey, 17 | timeout: timeout, 18 | }); 19 | 20 | var cache = { 21 | url: 'http://example.net/', 22 | location, 23 | browser, 24 | }; 25 | 26 | 27 | dotest.add ('Interface', test => { 28 | const tst = gtmetrix && gtmetrix.test; 29 | const locations = gtmetrix && gtmetrix.locations; 30 | const browsers = gtmetrix && gtmetrix.browsers; 31 | const account = gtmetrix && gtmetrix.account; 32 | 33 | test() 34 | .isFunction ('fail', 'export', app) 35 | .isObject ('fail', 'module', gtmetrix) 36 | 37 | .isObject ('fail', '.test', tst) 38 | .isFunction ('fail', '.test.create', tst && tst.create) 39 | .isFunction ('fail', '.test.get', tst && tst.get) 40 | 41 | .isObject ('fail', '.locations', locations) 42 | .isFunction ('fail', '.locations.list', locations && locations.list) 43 | 44 | .isObject ('fail', '.browsers', browsers) 45 | .isFunction ('fail', '.browsers.list', browsers && browsers.list) 46 | .isFunction ('fail', '.browsers.get', browsers && browsers.get) 47 | 48 | .isObject ('fail', '.account', account) 49 | .isFunction ('fail', '.account.status', account && account.status) 50 | .done(); 51 | }); 52 | 53 | 54 | /** 55 | * Promise handling 56 | */ 57 | 58 | dotest.add ('Promise - account.status', test => { 59 | gtmetrix.account.status () 60 | .catch (err => test (err).done()) 61 | .then (data => test() 62 | .isObject ('fail', 'data', data) 63 | .done() 64 | ); 65 | }); 66 | 67 | 68 | dotest.add ('Promise - Error: API error - without polling', test => { 69 | gtmetrix.test.get ('0') 70 | .catch (err => { 71 | test() 72 | .isError ('fail', 'err', err) 73 | .isExactly ('fail', 'err.message', err && err.message, 'API error'); 74 | }) 75 | .then (data => { 76 | test() 77 | .isUndefined ('fail', 'data', data) 78 | .done(); 79 | }); 80 | }); 81 | 82 | 83 | /** 84 | * Callback handling 85 | */ 86 | 87 | dotest.add ('Callback - account.status', test => { 88 | gtmetrix.account.status ((err, data) => { 89 | test (err) 90 | .isObject ('fail', 'data', data) 91 | .done (); 92 | }); 93 | }); 94 | 95 | 96 | dotest.add ('Callback - browsers.list', test => { 97 | gtmetrix.browsers.list ((err, data) => { 98 | test (err) 99 | .isArray ('fail', 'data', data) 100 | .isNotEmpty ('fail', 'data', data) 101 | .isObject ('warn', 'data[0]', data && data [0]) 102 | .done (); 103 | }); 104 | }); 105 | 106 | 107 | dotest.add ('Callback - browsers.get', test => { 108 | gtmetrix.browsers.get (3, (err, data) => { 109 | test (err) 110 | .isObject ('fail', 'data', data) 111 | .isExactly ('warn', 'data.id', data && data.id, 3) 112 | .done (); 113 | }); 114 | }); 115 | 116 | 117 | dotest.add ('Callback - locations.list', test => { 118 | gtmetrix.locations.list ((err, data) => { 119 | test (err) 120 | .isArray ('fail', 'data', data) 121 | .isNotEmpty ('fail', 'data', data) 122 | .isObject ('warn', 'data[0]', data && data [0]) 123 | .done (); 124 | }); 125 | }); 126 | 127 | 128 | dotest.add ('Callback - test.create', test => { 129 | gtmetrix.test.create (cache, (err, data) => { 130 | cache.test = data; 131 | test (err) 132 | .isObject ('fail', 'data', data) 133 | .done (); 134 | }); 135 | }); 136 | 137 | 138 | dotest.add ('Callback - test.get - without polling', test => { 139 | gtmetrix.test.get (cache.test.test_id, (err, data) => { 140 | test (err) 141 | .isObject ('fail', 'data', data) 142 | .isString ('warn', 'data.state', data && data.state) 143 | .done (); 144 | }); 145 | }); 146 | 147 | 148 | dotest.add ('Callback - test.get - with polling', test => { 149 | gtmetrix.test.get (cache.test.test_id, 5000, (err, data) => { 150 | test (err) 151 | .isObject ('fail', 'data', data) 152 | .isExactly ('fail', 'data.state', data && data.state, 'completed') 153 | .done (); 154 | }); 155 | }); 156 | 157 | 158 | dotest.add ('Callback - test.get resource - binary with polling', test => { 159 | gtmetrix.test.get (cache.test.test_id, 'report-pdf-full', 5000, (err, data) => { 160 | test (err) 161 | .isObject ('fail', 'data', data) 162 | .done (); 163 | }); 164 | }); 165 | 166 | 167 | dotest.add ('Callback - test.get resource - non-binary with polling', test => { 168 | gtmetrix.test.get (cache.test.test_id, 'yslow', 5000, (err, data) => { 169 | test (err) 170 | .isObject ('fail', 'data', data) 171 | .isExactly ('fail', 'data.u', data && data.u, cache.url) 172 | .done (); 173 | }); 174 | }); 175 | 176 | 177 | dotest.add ('Callback - Error: API error - without polling', test => { 178 | gtmetrix.test.get ('0', (err, data) => { 179 | test () 180 | .isError ('fail', 'err', err) 181 | .isExactly ('fail', 'err.message', err && err.message, 'API error') 182 | .isUndefined ('fail', 'data', data) 183 | .done (); 184 | }); 185 | }); 186 | 187 | 188 | dotest.add ('Callback - Error: API error - resource with default polling', test => { 189 | gtmetrix.test.get ('0', 'yslow', true, (err, data) => { 190 | test () 191 | .isError ('fail', 'err', err) 192 | .isExactly ('fail', 'err.message', err && err.message, 'API error') 193 | .isUndefined ('fail', 'data', data) 194 | .done (); 195 | }); 196 | }); 197 | 198 | 199 | dotest.add ('Callback - Error: request failed (timeout)', test => { 200 | var tmp = app ({ 201 | email: email, 202 | apikey: apikey, 203 | timeout: 1, 204 | }); 205 | 206 | tmp.account.status ((err, data) => { 207 | var error = err && err.error; 208 | 209 | test () 210 | .isError ('fail', 'err', err) 211 | .isExactly ('fail', 'err.message', err && err.message, 'request failed') 212 | .isError ('fail', 'err.error', error) 213 | .isExactly ('warn', 'err.error.code', error && error.code, 'TIMEOUT') 214 | .isUndefined ('fail', 'data', data) 215 | .done (); 216 | }); 217 | }); 218 | 219 | 220 | // Start the tests 221 | dotest.run (1000); 222 | --------------------------------------------------------------------------------