index.js (17025B)
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 ACTIVITY_TYPE, 9 EVENTS, 10 TEST_EVENTS, 11 } = require("resource://devtools/client/netmonitor/src/constants.js"); 12 const FirefoxDataProvider = require("resource://devtools/client/netmonitor/src/connector/firefox-data-provider.js"); 13 const { 14 getDisplayedTimingMarker, 15 } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); 16 17 const { 18 TYPES, 19 } = require("resource://devtools/shared/commands/resource/resource-command.js"); 20 21 // Network throttling 22 loader.lazyRequireGetter( 23 this, 24 "throttlingProfiles", 25 "resource://devtools/client/shared/components/throttling/profiles.js" 26 ); 27 28 loader.lazyRequireGetter( 29 this, 30 "HarMetadataCollector", 31 "resource://devtools/client/netmonitor/src/connector/har-metadata-collector.js", 32 true 33 ); 34 35 const DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF = "devtools.netmonitor.persistlog"; 36 37 /** 38 * Connector to Firefox backend. 39 */ 40 class Connector { 41 constructor() { 42 // Public methods 43 this.connect = this.connect.bind(this); 44 this.disconnect = this.disconnect.bind(this); 45 this.willNavigate = this.willNavigate.bind(this); 46 this.navigate = this.navigate.bind(this); 47 this.triggerActivity = this.triggerActivity.bind(this); 48 this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this); 49 this.requestData = this.requestData.bind(this); 50 this.getTimingMarker = this.getTimingMarker.bind(this); 51 this.updateNetworkThrottling = this.updateNetworkThrottling.bind(this); 52 53 // Internals 54 this.getLongString = this.getLongString.bind(this); 55 this.onResourceAvailable = this.onResourceAvailable.bind(this); 56 this.onResourceUpdated = this.onResourceUpdated.bind(this); 57 this.updatePersist = this.updatePersist.bind(this); 58 59 this.networkFront = null; 60 } 61 62 static NETWORK_RESOURCES = [ 63 TYPES.NETWORK_EVENT, 64 TYPES.NETWORK_EVENT_STACKTRACE, 65 TYPES.WEBSOCKET, 66 TYPES.SERVER_SENT_EVENT, 67 ]; 68 69 get networkResources() { 70 const networkResources = Array.from(Connector.NETWORK_RESOURCES); 71 if ( 72 Services.prefs.getBoolPref("devtools.netmonitor.features.webtransport") 73 ) { 74 networkResources.push(TYPES.WEBTRANSPORT); 75 } 76 return networkResources; 77 } 78 79 get currentTarget() { 80 return this.commands.targetCommand.targetFront; 81 } 82 83 /** 84 * Connect to the backend. 85 * 86 * @param {object} connection object with e.g. reference to the Toolbox. 87 * @param {object} actions (optional) is used to fire Redux actions to update store. 88 * @param {object} getState (optional) is used to get access to the state. 89 */ 90 async connect(connection, actions, getState) { 91 this.actions = actions; 92 this.getState = getState; 93 this.toolbox = connection.toolbox; 94 this.commands = this.toolbox.commands; 95 this.networkCommand = this.commands.networkCommand; 96 97 // The owner object (NetMonitorAPI) received all events. 98 this.owner = connection.owner; 99 100 this.networkFront = 101 await this.commands.watcherFront.getNetworkParentActor(); 102 103 this.dataProvider = new FirefoxDataProvider({ 104 commands: this.commands, 105 actions: this.actions, 106 owner: this.owner, 107 }); 108 109 this._harMetadataCollector = new HarMetadataCollector(this.commands); 110 await this._harMetadataCollector.connect(); 111 112 await this.commands.resourceCommand.watchResources([TYPES.DOCUMENT_EVENT], { 113 onAvailable: this.onResourceAvailable, 114 }); 115 116 await this.resume(false); 117 118 // Server side persistance of the data across reload is disabled by default. 119 // Ensure enabling it, if the related frontend pref is true. 120 if (Services.prefs.getBoolPref(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF)) { 121 await this.updatePersist(); 122 } 123 Services.prefs.addObserver( 124 DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF, 125 this.updatePersist 126 ); 127 } 128 129 disconnect() { 130 // As this function might be called twice, we need to guard if already called. 131 if (this._destroyed) { 132 return; 133 } 134 135 this._destroyed = true; 136 137 this.commands.resourceCommand.unwatchResources([TYPES.DOCUMENT_EVENT], { 138 onAvailable: this.onResourceAvailable, 139 }); 140 141 this.pause(); 142 143 Services.prefs.removeObserver( 144 DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF, 145 this.updatePersist 146 ); 147 148 if (this.actions) { 149 this.actions.batchReset(); 150 } 151 152 this.dataProvider.destroy(); 153 this.dataProvider = null; 154 this._harMetadataCollector.destroy(); 155 } 156 157 /** 158 * Clear network data from the connector. 159 * 160 * @param {object} options 161 * @param {boolean} options.isExplicitClear 162 * Set to true if the call to clear requests is explicitly requested by 163 * the user, to false if this is an automated clear, eg on navigation. 164 */ 165 clear({ isExplicitClear }) { 166 // Clear all the caches in the data provider 167 this.dataProvider.clear(); 168 169 this._harMetadataCollector.clear(); 170 171 if (isExplicitClear) { 172 // Only clear the resources if the clear was initiated explicitly by the 173 // UI, in other cases (eg navigation) the server handles the cleanup. 174 this.commands.resourceCommand.clearResources(this.networkResources); 175 this.emitForTests("clear-network-resources"); 176 } 177 178 // Disable the related network logs in the webconsole 179 this.toolbox.disableAllConsoleNetworkLogs(); 180 } 181 182 pause() { 183 return this.commands.resourceCommand.unwatchResources( 184 this.networkResources, 185 { 186 onAvailable: this.onResourceAvailable, 187 onUpdated: this.onResourceUpdated, 188 } 189 ); 190 } 191 192 resume(ignoreExistingResources = true) { 193 return this.commands.resourceCommand.watchResources(this.networkResources, { 194 onAvailable: this.onResourceAvailable, 195 onUpdated: this.onResourceUpdated, 196 ignoreExistingResources, 197 }); 198 } 199 200 async onResourceAvailable(resources, { areExistingResources }) { 201 for (const resource of resources) { 202 if (resource.resourceType === TYPES.DOCUMENT_EVENT) { 203 this.onDocEvent(resource, { areExistingResources }); 204 continue; 205 } 206 207 if (resource.resourceType === TYPES.NETWORK_EVENT) { 208 this.dataProvider.onNetworkResourceAvailable(resource); 209 continue; 210 } 211 212 if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) { 213 this.dataProvider.onStackTraceAvailable(resource); 214 continue; 215 } 216 217 if (resource.resourceType === TYPES.WEBSOCKET) { 218 const { wsMessageType } = resource; 219 220 switch (wsMessageType) { 221 case "webSocketOpened": { 222 this.dataProvider.onWebSocketOpened( 223 resource.httpChannelId, 224 resource.effectiveURI, 225 resource.protocols, 226 resource.extensions 227 ); 228 break; 229 } 230 case "webSocketClosed": { 231 this.dataProvider.onWebSocketClosed( 232 resource.httpChannelId, 233 resource.wasClean, 234 resource.code, 235 resource.reason 236 ); 237 break; 238 } 239 case "frameReceived": { 240 this.dataProvider.onFrameReceived( 241 resource.httpChannelId, 242 resource.data 243 ); 244 break; 245 } 246 case "frameSent": { 247 this.dataProvider.onFrameSent( 248 resource.httpChannelId, 249 resource.data 250 ); 251 break; 252 } 253 } 254 continue; 255 } 256 257 if (resource.resourceType === TYPES.SERVER_SENT_EVENT) { 258 const { messageType, httpChannelId, data } = resource; 259 switch (messageType) { 260 case "eventSourceConnectionClosed": { 261 this.dataProvider.onEventSourceConnectionClosed(httpChannelId); 262 break; 263 } 264 case "eventReceived": { 265 this.dataProvider.onEventReceived(httpChannelId, data); 266 break; 267 } 268 } 269 } 270 } 271 } 272 273 async onResourceUpdated(updates) { 274 for (const { resource, update } of updates) { 275 this.dataProvider.onNetworkResourceUpdated(resource, update); 276 } 277 } 278 279 enableActions(enable) { 280 this.dataProvider.enableActions(enable); 281 } 282 283 willNavigate() { 284 if (this.actions) { 285 if (!Services.prefs.getBoolPref(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF)) { 286 this.actions.batchReset(); 287 this.actions.clearRequests({ isExplicitClear: false }); 288 } else { 289 // If the log is persistent, just clear all accumulated timing markers. 290 this.actions.clearTimingMarkers(); 291 } 292 } 293 294 if (this.actions && this.getState) { 295 const state = this.getState(); 296 // Resume is done automatically on page reload/navigation. 297 if (!state.requests.recording) { 298 this.actions.toggleRecording(); 299 } 300 301 // Stop any ongoing search. 302 if (state.search.ongoingSearch) { 303 this.actions.stopOngoingSearch(); 304 } 305 } 306 } 307 308 navigate() { 309 if (!this.dataProvider.hasPendingRequests()) { 310 this.onReloaded(); 311 return; 312 } 313 const listener = () => { 314 if (this.dataProvider && this.dataProvider.hasPendingRequests()) { 315 return; 316 } 317 if (this.owner) { 318 this.owner.off(EVENTS.PAYLOAD_READY, listener); 319 } 320 // Netmonitor may already be destroyed, 321 // so do not try to notify the listeners 322 if (this.dataProvider) { 323 this.onReloaded(); 324 } 325 }; 326 if (this.owner) { 327 this.owner.on(EVENTS.PAYLOAD_READY, listener); 328 } 329 } 330 331 onReloaded() { 332 const panel = this.toolbox.getPanel("netmonitor"); 333 if (panel) { 334 panel.emit("reloaded"); 335 } 336 } 337 338 /** 339 * The "DOMContentLoaded" and "Load" events sent by the console actor. 340 * 341 * @param {object} resource The DOCUMENT_EVENT resource 342 */ 343 onDocEvent(resource, { areExistingResources }) { 344 if (!resource.targetFront.isTopLevel) { 345 // Only consider top level document, and ignore remote iframes top document 346 return; 347 } 348 349 // Netmonitor does not support dom-loading 350 if ( 351 resource.name != "dom-interactive" && 352 resource.name != "dom-complete" && 353 resource.name != "will-navigate" 354 ) { 355 return; 356 } 357 358 if (resource.name == "will-navigate") { 359 // When we open the netmonitor while the page already started loading, 360 // we don't want to clear it. So here, we ignore will-navigate events 361 // which were stored in the ResourceCommand cache and only consider 362 // the live one coming straight from the server. 363 if (!areExistingResources) { 364 this.willNavigate(); 365 } 366 return; 367 } 368 369 if (this.actions) { 370 this.actions.addTimingMarker(resource); 371 } 372 373 if (resource.name === "dom-complete") { 374 this.navigate(); 375 } 376 377 this.emitForTests(TEST_EVENTS.TIMELINE_EVENT, resource); 378 } 379 380 async updatePersist() { 381 const enabled = Services.prefs.getBoolPref( 382 DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF 383 ); 384 385 await this.networkFront.setPersist(enabled); 386 387 this.emitForTests(TEST_EVENTS.PERSIST_CHANGED, enabled); 388 } 389 390 /** 391 * Triggers a specific "activity" to be performed by the frontend. 392 * This can be, for example, triggering reloads or enabling/disabling cache. 393 * 394 * @param {number} type The activity type. See the ACTIVITY_TYPE const. 395 * @return {object} A promise resolved once the activity finishes and the frontend 396 * is back into "standby" mode. 397 */ 398 triggerActivity(type) { 399 // Puts the frontend into "standby" (when there's no particular activity). 400 const standBy = () => { 401 this.currentActivity = ACTIVITY_TYPE.NONE; 402 }; 403 404 // Reconfigures the tab, optionally triggering a reload. 405 const reconfigureTab = async options => { 406 await this.commands.targetConfigurationCommand.updateConfiguration( 407 options 408 ); 409 }; 410 411 // Reconfigures the tab and waits for the target to finish navigating. 412 const reconfigureTabAndReload = async options => { 413 await reconfigureTab(options); 414 await this.commands.targetCommand.reloadTopLevelTarget(); 415 }; 416 417 switch (type) { 418 case ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT: 419 return reconfigureTabAndReload({}).then(standBy); 420 case ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED: 421 this.currentActivity = ACTIVITY_TYPE.ENABLE_CACHE; 422 this.commands.resourceCommand 423 .waitForNextResource( 424 this.commands.resourceCommand.TYPES.DOCUMENT_EVENT, 425 { 426 ignoreExistingResources: true, 427 predicate(resource) { 428 return resource.name == "will-navigate"; 429 }, 430 } 431 ) 432 .then(() => { 433 this.currentActivity = type; 434 }); 435 return reconfigureTabAndReload({ 436 cacheDisabled: false, 437 }).then(standBy); 438 case ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED: 439 this.currentActivity = ACTIVITY_TYPE.DISABLE_CACHE; 440 this.commands.resourceCommand 441 .waitForNextResource( 442 this.commands.resourceCommand.TYPES.DOCUMENT_EVENT, 443 { 444 ignoreExistingResources: true, 445 predicate(resource) { 446 return resource.name == "will-navigate"; 447 }, 448 } 449 ) 450 .then(() => { 451 this.currentActivity = type; 452 }); 453 return reconfigureTabAndReload({ 454 cacheDisabled: true, 455 }).then(standBy); 456 case ACTIVITY_TYPE.ENABLE_CACHE: 457 this.currentActivity = type; 458 return reconfigureTab({ 459 cacheDisabled: false, 460 }).then(standBy); 461 case ACTIVITY_TYPE.DISABLE_CACHE: 462 this.currentActivity = type; 463 return reconfigureTab({ 464 cacheDisabled: true, 465 }).then(standBy); 466 } 467 this.currentActivity = ACTIVITY_TYPE.NONE; 468 return Promise.reject(new Error("Invalid activity type")); 469 } 470 471 /** 472 * Fetches the full text of a LongString. 473 * 474 * @param {object|string} stringGrip 475 * The long string grip containing the corresponding actor. 476 * If you pass in a plain string (by accident or because you're lazy), 477 * then a promise of the same string is simply returned. 478 * @return {object} 479 * A promise that is resolved when the full string contents 480 * are available, or rejected if something goes wrong. 481 */ 482 getLongString(stringGrip) { 483 return this.dataProvider.getLongString(stringGrip); 484 } 485 486 /** 487 * Used for HAR generation. 488 */ 489 getHarData() { 490 return this._harMetadataCollector.getHarData(); 491 } 492 493 /** 494 * Getter that returns the current toolbox instance. 495 * 496 * @return {Toolbox} toolbox instance 497 */ 498 getToolbox() { 499 return this.toolbox; 500 } 501 502 /** 503 * Open a given source in Debugger 504 * 505 * @param {string} sourceURL source url 506 * @param {number} sourceLine source line number 507 */ 508 viewSourceInDebugger(sourceURL, sourceLine, sourceColumn) { 509 if (this.toolbox) { 510 this.toolbox.viewSourceInDebugger(sourceURL, sourceLine, sourceColumn); 511 } 512 } 513 514 /** 515 * Fetch networkEventUpdate websocket message from back-end when 516 * data provider is connected. 517 * 518 * @param {object} request network request instance 519 * @param {string} type NetworkEventUpdate type 520 */ 521 requestData(request, type) { 522 return this.dataProvider.requestData(request, type); 523 } 524 525 getTimingMarker(name) { 526 if (!this.getState) { 527 return -1; 528 } 529 530 const state = this.getState(); 531 return getDisplayedTimingMarker(state, name); 532 } 533 534 async updateNetworkThrottling(enabled, profile) { 535 if (!enabled) { 536 this.networkFront.clearNetworkThrottling(); 537 await this.commands.targetConfigurationCommand.updateConfiguration({ 538 setTabOffline: false, 539 }); 540 } else { 541 // The profile can be either a profile id which is used to 542 // search the predefined throttle profiles or a profile object 543 // as defined in the trottle tests. 544 if (typeof profile === "string") { 545 profile = throttlingProfiles.profiles.find(({ id }) => id == profile); 546 } 547 const { download, upload, latency, id } = profile; 548 549 // The offline profile has download and upload set to false 550 await this.commands.targetConfigurationCommand.updateConfiguration({ 551 setTabOffline: id === throttlingProfiles.PROFILE_CONSTANTS.OFFLINE, 552 }); 553 554 await this.networkFront.setNetworkThrottling({ 555 downloadThroughput: download, 556 uploadThroughput: upload, 557 latency, 558 }); 559 } 560 561 this.emitForTests(TEST_EVENTS.THROTTLING_CHANGED, { profile }); 562 } 563 564 /** 565 * Fire events for the owner object. These events are only 566 * used in tests so, don't fire them in production release. 567 */ 568 emitForTests(type, data) { 569 if (this.owner) { 570 this.owner.emitForTests(type, data); 571 } 572 } 573 } 574 module.exports.Connector = Connector;