1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 /** 6 * Implements the NavigationCollector object that powers the extension. 7 * 8 * @author mkwst (a] google.com (Mike West) 9 */ 10 11 /** 12 * Collects navigation events, and provides a list of successful requests 13 * that you can do interesting things with. Calling the constructor will 14 * automatically bind handlers to the relevant webnavigation API events, 15 * and to a `getMostRequestedUrls` extension message for internal 16 * communication between background pages and popups. 17 * 18 * @constructor 19 */ 20 function NavigationCollector() { 21 /** 22 * A list of currently pending requests, implemented as a hash of each 23 * request's tab ID, frame ID, and URL in order to ensure uniqueness. 24 * 25 * @type {Object.<string, {start: number}>} 26 * @private 27 */ 28 this.pending_ = {}; 29 30 /** 31 * A list of completed requests, implemented as a hash of each 32 * request's tab ID, frame ID, and URL in order to ensure uniqueness. 33 * 34 * @type {Object.<string, Array.<NavigationCollector.Request>>} 35 * @private 36 */ 37 this.completed_ = {}; 38 39 /** 40 * A list of requests that errored off, implemented as a hash of each 41 * request's tab ID, frame ID, and URL in order to ensure uniqueness. 42 * 43 * @type {Object.<string, Array.<NavigationCollector.Request>>} 44 * @private 45 */ 46 this.errored_ = {}; 47 48 // Bind handlers to the 'webNavigation' events that we're interested 49 // in handling in order to build up a complete picture of the whole 50 // navigation event. 51 chrome.experimental.webNavigation.onBeforeRetarget.addListener( 52 this.onBeforeRetargetListener_.bind(this)); 53 chrome.experimental.webNavigation.onBeforeNavigate.addListener( 54 this.onBeforeNavigateListener_.bind(this)); 55 chrome.experimental.webNavigation.onCompleted.addListener( 56 this.onCompletedListener_.bind(this)); 57 chrome.experimental.webNavigation.onCommitted.addListener( 58 this.onCommittedListener_.bind(this)); 59 chrome.experimental.webNavigation.onErrorOccurred.addListener( 60 this.onErrorOccurredListener_.bind(this)); 61 62 // Bind handler to extension messages for communication from popup. 63 chrome.extension.onRequest.addListener(this.onRequestListener_.bind(this)); 64 } 65 66 /////////////////////////////////////////////////////////////////////////////// 67 68 /** 69 * The possible transition types that explain how the navigation event 70 * was generated (i.e. "The user clicked on a link." or "The user submitted 71 * a form"). 72 * 73 * @see http://code.google.com/chrome/extensions/trunk/history.html 74 * @enum {string} 75 */ 76 NavigationCollector.NavigationType = { 77 AUTO_BOOKMARK: 'auto_bookmark', 78 AUTO_SUBFRAME: 'auto_subframe', 79 FORM_SUBMIT: 'form_submit', 80 GENERATED: 'generated', 81 KEYWORD: 'keyword', 82 KEYWORD_GENERATED: 'keyword_generated', 83 LINK: 'link', 84 MANUAL_SUBFRAME: 'manual_subframe', 85 RELOAD: 'reload', 86 START_PAGE: 'start_page', 87 TYPED: 'typed' 88 }; 89 90 /** 91 * The possible transition qualifiers: 92 * 93 * * CLIENT_REDIRECT: Redirects caused by JavaScript, or a refresh meta tag 94 * on a page. 95 * 96 * * SERVER_REDIRECT: Redirected by the server via a 301/302 response. 97 * 98 * * FORWARD_BACK: User used the forward or back buttons to navigate through 99 * her browsing history. 100 * 101 * @enum {string} 102 */ 103 NavigationCollector.NavigationQualifier = { 104 CLIENT_REDIRECT: 'client_redirect', 105 FORWARD_BACK: 'forward_back', 106 SERVER_REDIRECT: 'server_redirect' 107 }; 108 109 /** 110 * @typedef {{url: string, transitionType: NavigationCollector.NavigationType, 111 * transitionQualifier: Array.<NavigationCollector.NavigationQualifier>, 112 * openedInNewTab: boolean, sourceUrl: ?string, duration: number}} 113 */ 114 NavigationCollector.Request; 115 116 /////////////////////////////////////////////////////////////////////////////// 117 118 NavigationCollector.prototype = { 119 /** 120 * Returns a somewhat unique ID for a given WebNavigation request. 121 * 122 * @param {!{tabId: number, frameId: number, url: string}} data Information 123 * about the navigation event we'd like an ID for. 124 * @return {!string} ID created by combining the tab ID and frame ID (as the 125 * API ensures that these will be unique across a single navigation 126 * event) 127 * @private 128 */ 129 parseId_: function(data) { 130 return data.tabId + '-' + data.frameId; 131 }, 132 133 134 /** 135 * Creates an empty entry in the pending array, and prepopulates the 136 * errored and completed arrays for ease of insertion later. 137 * 138 * @param {!string} id The request's ID, as produced by parseId_. 139 * @param {!string} url The request's URL. 140 */ 141 prepareDataStorage_: function(id, url) { 142 this.pending_[id] = this.pending_[id] || { 143 openedInNewTab: false, 144 sourceUrl: null, 145 start: null, 146 transitionQualifiers: [], 147 transitionType: null 148 }; 149 this.completed_[url] = this.completed_[url] || []; 150 this.errored_[url] = this.errored_[url] || []; 151 }, 152 153 154 /** 155 * Handler for the 'onBeforeRetarget' event. Updates the pending request 156 * with a sourceUrl, and notes that it was opened in a new tab. 157 * 158 * Pushes the request onto the 159 * 'pending_' object, and stores it for later use. 160 * 161 * @param {!Object} data The event data generated for this request. 162 * @private 163 */ 164 onBeforeRetargetListener_: function(data) { 165 var id = this.parseId_(data); 166 this.prepareDataStorage_(id, data.url); 167 this.pending_[id].openedInNewTab = true; 168 this.pending_[id].sourceUrl = data.sourceUrl; 169 this.pending_[id].start = data.timeStamp; 170 }, 171 172 173 /** 174 * Handler for the 'onBeforeNavigate' event. Pushes the request onto the 175 * 'pending_' object, and stores it for later use. 176 * 177 * @param {!Object} data The event data generated for this request. 178 * @private 179 */ 180 onBeforeNavigateListener_: function(data) { 181 var id = this.parseId_(data); 182 this.prepareDataStorage_(id, data.url); 183 this.pending_[id].start = this.pending_[id].start || data.timeStamp; 184 }, 185 186 187 /** 188 * Handler for the 'onCommitted' event. Updates the pending request with 189 * transition information. 190 * 191 * Pushes the request onto the 192 * 'pending_' object, and stores it for later use. 193 * 194 * @param {!Object} data The event data generated for this request. 195 * @private 196 */ 197 onCommittedListener_: function(data) { 198 var id = this.parseId_(data); 199 if (!this.pending_[id]) { 200 console.warn( 201 chrome.i18n.getMessage('errorCommittedWithoutPending'), 202 data.url, 203 data); 204 } else { 205 this.pending_[id].transitionType = data.transitionType; 206 this.pending_[id].transitionQualifiers = 207 data.transitionQualifiers; 208 } 209 }, 210 211 212 /** 213 * Handler for the 'onCompleted` event. Pulls the request's data from the 214 * 'pending_' object, combines it with the completed event's data, and pushes 215 * a new NavigationCollector.Request object onto 'completed_'. 216 * 217 * @param {!Object} data The event data generated for this request. 218 * @private 219 */ 220 onCompletedListener_: function(data) { 221 var id = this.parseId_(data); 222 if (!this.pending_[id]) { 223 console.warn( 224 chrome.i18n.getMessage('errorCompletedWithoutPending'), 225 data.url, 226 data); 227 } else { 228 this.completed_[data.url].push({ 229 duration: (data.timeStamp - this.pending_[id].start), 230 openedInNewWindow: this.pending_[id].openedInNewWindow, 231 sourceUrl: this.pending_[id].sourceUrl, 232 transitionQualifiers: this.pending_[id].transitionQualifiers, 233 transitionType: this.pending_[id].transitionType, 234 url: data.url 235 }); 236 delete this.pending_[id]; 237 } 238 }, 239 240 241 /** 242 * Handler for the 'onErrorOccurred` event. Pulls the request's data from the 243 * 'pending_' object, combines it with the completed event's data, and pushes 244 * a new NavigationCollector.Request object onto 'errored_'. 245 * 246 * @param {!Object} data The event data generated for this request. 247 * @private 248 */ 249 onErrorOccurredListener_: function(data) { 250 var id = this.parseId_(data); 251 if (!this.pending_[id]) { 252 console.error( 253 chrome.i18n.getMessage('errorErrorOccurredWithoutPending'), 254 data.url, 255 data); 256 } else { 257 this.errored_[data.url].push({ 258 duration: (data.timeStamp - this.pending_[id].start), 259 openedInNewWindow: this.pending_[id].openedInNewWindow, 260 sourceUrl: this.pending_[id].sourceUrl, 261 transitionQualifiers: this.pending_[id].transitionQualifiers, 262 transitionType: this.pending_[id].transitionType, 263 url: data.url 264 }); 265 delete this.pending_[id]; 266 } 267 }, 268 269 /** 270 * Handle request messages from the popup. 271 * 272 * @param {!{type:string}} request The external request to answer. 273 * @param {!MessageSender} sender Info about the script context that sent 274 * the request. 275 * @param {!function} sendResponse Function to call to send a response. 276 * @private 277 */ 278 onRequestListener_: function(request, sender, sendResponse) { 279 if (request.type === 'getMostRequestedUrls') 280 sendResponse({result: this.getMostRequestedUrls(request.num)}); 281 else 282 sendResponse({}); 283 }, 284 285 /////////////////////////////////////////////////////////////////////////////// 286 287 /** 288 * @return {Object.<string, NavigationCollector.Request>} The complete list of 289 * successful navigation requests. 290 */ 291 get completed() { 292 return this.completed_; 293 }, 294 295 296 /** 297 * @return {Object.<string, Navigationcollector.Request>} The complete list of 298 * unsuccessful navigation requests. 299 */ 300 get errored() { 301 return this.errored_; 302 }, 303 304 305 /** 306 * Get a list of the X most requested URLs. 307 * 308 * @param {number=} num The number of successful navigation requests to 309 * return. If 0 is passed in, or the argument left off entirely, all 310 * successful requests are returned. 311 * @return {Object.<string, NavigationCollector.Request>} The list of 312 * successful navigation requests, sorted in decending order of frequency. 313 */ 314 getMostRequestedUrls: function(num) { 315 return this.getMostFrequentUrls_(this.completed, num); 316 }, 317 318 319 /** 320 * Get a list of the X most errored URLs. 321 * 322 * @param {number=} num The number of unsuccessful navigation requests to 323 * return. If 0 is passed in, or the argument left off entirely, all 324 * successful requests are returned. 325 * @return {Object.<string, NavigationCollector.Request>} The list of 326 * unsuccessful navigation requests, sorted in decending order 327 * of frequency. 328 */ 329 getMostErroredUrls: function(num) { 330 return this.getMostErroredUrls_(this.errored, num); 331 }, 332 333 334 /** 335 * Get a list of the most frequent URLs in a list. 336 * 337 * @param {NavigationCollector.Request} list A list of URLs to parse. 338 * @param {number=} num The number of navigation requests to return. If 339 * 0 is passed in, or the argument left off entirely, all requests 340 * are returned. 341 * @return {Object.<string, NavigationCollector.Request>} The list of 342 * navigation requests, sorted in decending order of frequency. 343 * @private 344 */ 345 getMostFrequentUrls_: function(list, num) { 346 var result = []; 347 var avg; 348 // Convert the 'completed_' object to an array. 349 for (var x in list) { 350 avg = 0; 351 if (list.hasOwnProperty(x)) { 352 list[x].forEach(function(o) { 353 avg += o.duration; 354 }); 355 avg = avg / list[x].length; 356 result.push({ 357 url: x, 358 numRequests: list[x].length, 359 requestList: list[x], 360 average: avg 361 }); 362 } 363 } 364 // Sort the array. 365 result.sort(function(a, b) { 366 return b.numRequests - a.numRequests; 367 }); 368 // Return the requested number of results. 369 return num ? result.slice(0, num) : result; 370 } 371 }; 372