Home | History | Annotate | Download | only in basic
      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