har-automation.js (6457B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 HarCollector, 9 } = require("resource://devtools/client/netmonitor/src/har/har-collector.js"); 10 const { 11 HarExporter, 12 } = require("resource://devtools/client/netmonitor/src/har/har-exporter.js"); 13 const { 14 HarUtils, 15 } = require("resource://devtools/client/netmonitor/src/har/har-utils.js"); 16 const { 17 getLongStringFullText, 18 } = require("resource://devtools/client/shared/string-utils.js"); 19 20 const prefDomain = "devtools.netmonitor.har."; 21 22 // Helper tracer. Should be generic sharable by other modules (bug 1171927) 23 const trace = { 24 log() {}, 25 }; 26 27 /** 28 * This object is responsible for automated HAR export. It listens 29 * for Network activity, collects all HTTP data and triggers HAR 30 * export when the page is loaded. 31 * 32 * The user needs to enable the following preference to make the 33 * auto-export work: devtools.netmonitor.har.enableAutoExportToFile 34 * 35 * HAR files are stored within directory that is specified in this 36 * preference: devtools.netmonitor.har.defaultLogDir 37 * 38 * If the default log directory preference isn't set the following 39 * directory is used by default: <profile>/har/logs 40 */ 41 class HarAutomation { 42 // Initialization 43 44 async initialize(toolbox) { 45 this.toolbox = toolbox; 46 this.commands = toolbox.commands; 47 48 await this.startMonitoring(); 49 } 50 51 destroy() { 52 if (this.collector) { 53 this.collector.stop(); 54 } 55 56 if (this.tabWatcher) { 57 this.tabWatcher.disconnect(); 58 } 59 } 60 61 // Automation 62 63 async startMonitoring() { 64 await this.commands.resourceCommand.watchResources( 65 [this.commands.resourceCommand.TYPES.DOCUMENT_EVENT], 66 { 67 onAvailable: resources => { 68 // Only consider top level document, and ignore remote iframes top document 69 if ( 70 resources.find( 71 r => r.name == "will-navigate" && r.targetFront.isTopLevel 72 ) 73 ) { 74 this.pageLoadBegin(); 75 } 76 if ( 77 resources.find( 78 r => r.name == "dom-complete" && r.targetFront.isTopLevel 79 ) 80 ) { 81 this.pageLoadDone(); 82 } 83 }, 84 ignoreExistingResources: true, 85 } 86 ); 87 } 88 89 pageLoadBegin() { 90 this.resetCollector(); 91 } 92 93 resetCollector() { 94 if (this.collector) { 95 this.collector.stop(); 96 } 97 98 // A page is about to be loaded, start collecting HTTP 99 // data from events sent from the backend. 100 this.collector = new HarCollector({ 101 commands: this.commands, 102 }); 103 104 this.collector.start(); 105 } 106 107 /** 108 * A page is done loading, export collected data. Note that 109 * some requests for additional page resources might be pending, 110 * so export all after all has been properly received from the backend. 111 * 112 * This collector still works and collects any consequent HTTP 113 * traffic (e.g. XHRs) happening after the page is loaded and 114 * The additional traffic can be exported by executing 115 * triggerExport on this object. 116 */ 117 pageLoadDone(response) { 118 trace.log("HarAutomation.pageLoadDone; ", response); 119 120 if (this.collector) { 121 this.collector.waitForHarLoad().then(() => { 122 return this.autoExport(); 123 }); 124 } 125 } 126 127 autoExport() { 128 const autoExport = Services.prefs.getBoolPref( 129 prefDomain + "enableAutoExportToFile" 130 ); 131 132 if (!autoExport) { 133 return Promise.resolve(); 134 } 135 136 // Auto export to file is enabled, so save collected data 137 // into a file and use all the default options. 138 const data = { 139 fileName: Services.prefs.getCharPref(prefDomain + "defaultFileName"), 140 }; 141 142 return this.executeExport(data); 143 } 144 145 // Public API 146 147 /** 148 * Export all what is currently collected. 149 */ 150 triggerExport(data) { 151 if (!data.fileName) { 152 data.fileName = Services.prefs.getCharPref( 153 prefDomain + "defaultFileName" 154 ); 155 } 156 157 return this.executeExport(data); 158 } 159 160 /** 161 * Clear currently collected data. 162 */ 163 clear() { 164 this.resetCollector(); 165 } 166 167 // HAR Export 168 169 /** 170 * Execute HAR export. This method fetches all data from the 171 * Network panel (asynchronously) and saves it into a file. 172 */ 173 async executeExport(data) { 174 const items = this.collector.getItems(); 175 const { title } = this.commands.targetCommand.targetFront; 176 177 const netMonitor = await this.toolbox.getNetMonitorAPI(); 178 const connector = await netMonitor.getHarExportConnector(); 179 180 const options = { 181 connector, 182 requestData: null, 183 getTimingMarker: null, 184 getString: this.getString.bind(this), 185 view: this, 186 items, 187 }; 188 189 options.defaultFileName = data.fileName; 190 options.compress = data.compress; 191 options.title = data.title || title; 192 options.id = data.id; 193 options.jsonp = data.jsonp; 194 options.includeResponseBodies = data.includeResponseBodies; 195 options.jsonpCallback = data.jsonpCallback; 196 options.forceExport = data.forceExport; 197 198 trace.log("HarAutomation.executeExport; " + data.fileName, options); 199 200 const jsonString = await HarExporter.fetchHarData(options); 201 202 // Save the HAR file if the file name is provided. 203 if (jsonString && options.defaultFileName) { 204 const file = getDefaultTargetFile(options); 205 if (file) { 206 HarUtils.saveToFile(file, jsonString, options.compress); 207 } 208 } 209 210 return jsonString; 211 } 212 213 /** 214 * Fetches the full text of a string. 215 */ 216 async getString(stringGrip) { 217 const fullText = await getLongStringFullText( 218 this.commands.client, 219 stringGrip 220 ); 221 return fullText; 222 } 223 } 224 225 // Protocol Helpers 226 227 /** 228 * Returns target file for exported HAR data. 229 */ 230 function getDefaultTargetFile(options) { 231 const path = 232 options.defaultLogDir || 233 Services.prefs.getCharPref("devtools.netmonitor.har.defaultLogDir"); 234 const folder = HarUtils.getLocalDirectory(path); 235 236 const host = new URL(options.connector.currentTarget.url); 237 const fileName = HarUtils.getHarFileName( 238 options.defaultFileName, 239 options.jsonp, 240 options.compress, 241 host.hostname 242 ); 243 244 folder.append(fileName); 245 folder.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8)); 246 247 return folder; 248 } 249 250 // Exports from this module 251 exports.HarAutomation = HarAutomation;