├── index.js ├── .gitignore ├── package.json ├── LICENSE ├── README.md └── lib └── PuppeteerHar.js /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/PuppeteerHar'); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | .idea 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-har", 3 | "version": "1.1.2", 4 | "description": "Generate HAR file with Puppeteer", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Everettss/puppeteer-har" 12 | }, 13 | "keywords": [ 14 | "har", 15 | "puppeteer", 16 | "performance" 17 | ], 18 | "author": "Michał Janaszek", 19 | "license": "MIT", 20 | "dependencies": { 21 | "chrome-har": "^0.11.3" 22 | }, 23 | "peerDependencies": { 24 | "puppeteer": ">=1.0.0" 25 | }, 26 | "engines": { 27 | "node": ">=7.6.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michał Janaszek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # puppeteer-har 2 | [![npm version][1]][2] 3 | 4 | Generate HAR file with [puppeteer](https://github.com/GoogleChrome/puppeteer). 5 | 6 | ## Install 7 | 8 | ``` 9 | npm install puppeteer-har 10 | ``` 11 | 12 | ## Usage 13 | 14 | ```javascript 15 | const puppeteer = require('puppeteer'); 16 | const PuppeteerHar = require('puppeteer-har'); 17 | 18 | (async () => { 19 | const browser = await puppeteer.launch(); 20 | const page = await browser.newPage(); 21 | 22 | const har = new PuppeteerHar(page); 23 | await har.start({ path: 'results.har' }); 24 | 25 | await page.goto('http://example.com'); 26 | 27 | await har.stop(); 28 | await browser.close(); 29 | })(); 30 | ``` 31 | 32 | ### PuppeteerHar(page) 33 | - `page` <[Page]> 34 | 35 | ### har.start([options]) 36 | - `options` Optional 37 | - `path` <[string]> If set HAR file will be written at this path 38 | - returns: <[Promise]> 39 | 40 | ### har.stop() 41 | - returns: <[Promise]> If path is not set in `har.start` Promise will return object with HAR. 42 | 43 | [1]: https://img.shields.io/npm/v/puppeteer-har.svg?style=flat-square 44 | [2]: https://npmjs.org/package/puppeteer-har 45 | [Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object" 46 | [Page]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page 47 | [Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise" 48 | [string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String" 49 | -------------------------------------------------------------------------------- /lib/PuppeteerHar.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { promisify } = require('util'); 3 | const { harFromMessages } = require('chrome-har'); 4 | 5 | // event types to observe 6 | const page_observe = [ 7 | 'Page.loadEventFired', 8 | 'Page.domContentEventFired', 9 | 'Page.frameStartedLoading', 10 | 'Page.frameAttached', 11 | 'Page.frameScheduledNavigation', 12 | ]; 13 | 14 | const network_observe = [ 15 | 'Network.requestWillBeSent', 16 | 'Network.requestServedFromCache', 17 | 'Network.dataReceived', 18 | 'Network.responseReceived', 19 | 'Network.resourceChangedPriority', 20 | 'Network.loadingFinished', 21 | 'Network.loadingFailed', 22 | ]; 23 | 24 | class PuppeteerHar { 25 | 26 | /** 27 | * @param {object} page 28 | */ 29 | constructor(page) { 30 | this.page = page; 31 | this.mainFrame = this.page.mainFrame(); 32 | this.inProgress = false; 33 | this.cleanUp(); 34 | } 35 | 36 | /** 37 | * @returns {void} 38 | */ 39 | cleanUp() { 40 | this.network_events = []; 41 | this.page_events = []; 42 | this.response_body_promises = []; 43 | } 44 | 45 | /** 46 | * @param {{path: string}=} options 47 | * @return {Promise} 48 | */ 49 | async start({ path, saveResponse, captureMimeTypes } = {}) { 50 | this.inProgress = true; 51 | this.saveResponse = saveResponse || false; 52 | this.captureMimeTypes = captureMimeTypes || ['text/html', 'application/json']; 53 | this.path = path; 54 | this.client = await this.page.target().createCDPSession(); 55 | await this.client.send('Page.enable'); 56 | await this.client.send('Network.enable'); 57 | page_observe.forEach(method => { 58 | this.client.on(method, params => { 59 | if (!this.inProgress) { 60 | return; 61 | } 62 | this.page_events.push({ method, params }); 63 | }); 64 | }); 65 | network_observe.forEach(method => { 66 | this.client.on(method, params => { 67 | if (!this.inProgress) { 68 | return; 69 | } 70 | this.network_events.push({ method, params }); 71 | 72 | if (this.saveResponse && method == 'Network.responseReceived') { 73 | const response = params.response; 74 | const requestId = params.requestId; 75 | 76 | // Response body is unavailable for redirects, no-content, image, audio and video responses 77 | if (response.status !== 204 && 78 | response.headers.location == null && 79 | this.captureMimeTypes.includes(response.mimeType) 80 | ) { 81 | const promise = this.client.send( 82 | 'Network.getResponseBody', { requestId }, 83 | ).then((responseBody) => { 84 | // Set the response so `chrome-har` can add it to the HAR 85 | params.response.body = new Buffer.from( 86 | responseBody.body, 87 | responseBody.base64Encoded ? 'base64' : undefined, 88 | ).toString(); 89 | }, (reason) => { 90 | // Resources (i.e. response bodies) are flushed after page commits 91 | // navigation and we are no longer able to retrieve them. In this 92 | // case, fail soft so we still add the rest of the response to the 93 | // HAR. Possible option would be force wait before navigation... 94 | }); 95 | this.response_body_promises.push(promise); 96 | } 97 | } 98 | }); 99 | }); 100 | } 101 | 102 | /** 103 | * @returns {Promise} 104 | */ 105 | async stop() { 106 | this.inProgress = false; 107 | await Promise.all(this.response_body_promises); 108 | await this.client.detach(); 109 | const har = harFromMessages( 110 | this.page_events.concat(this.network_events), 111 | {includeTextFromResponseBody: this.saveResponse} 112 | ); 113 | this.cleanUp(); 114 | if (this.path) { 115 | await promisify(fs.writeFile)(this.path, JSON.stringify(har)); 116 | } else { 117 | return har; 118 | } 119 | } 120 | } 121 | 122 | module.exports = PuppeteerHar; 123 | --------------------------------------------------------------------------------