Home | History | Annotate | Download | only in basic
      1 // Copyright (c) 2012 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.webNavigation.onCreatedNavigationTarget.addListener(
     52       this.onCreatedNavigationTargetListener_.bind(this));
     53   chrome.webNavigation.onBeforeNavigate.addListener(
     54       this.onBeforeNavigateListener_.bind(this));
     55   chrome.webNavigation.onCompleted.addListener(
     56       this.onCompletedListener_.bind(this));
     57   chrome.webNavigation.onCommitted.addListener(
     58       this.onCommittedListener_.bind(this));
     59   chrome.webNavigation.onErrorOccurred.addListener(
     60       this.onErrorOccurredListener_.bind(this));
     61   chrome.webNavigation.onReferenceFragmentUpdated.addListener(
     62       this.onReferenceFragmentUpdatedListener_.bind(this));
     63   chrome.webNavigation.onHistoryStateUpdated.addListener(
     64       this.onHistoryStateUpdatedListener_.bind(this));
     65 
     66   // Bind handler to extension messages for communication from popup.
     67   chrome.extension.onRequest.addListener(this.onRequestListener_.bind(this));
     68 
     69   this.loadDataStorage_();
     70 }
     71 
     72 ///////////////////////////////////////////////////////////////////////////////
     73 
     74 /**
     75  * The possible transition types that explain how the navigation event
     76  * was generated (i.e. "The user clicked on a link." or "The user submitted
     77  * a form").
     78  *
     79  * @see http://code.google.com/chrome/extensions/trunk/history.html
     80  * @enum {string}
     81  */
     82 NavigationCollector.NavigationType = {
     83   AUTO_BOOKMARK: 'auto_bookmark',
     84   AUTO_SUBFRAME: 'auto_subframe',
     85   FORM_SUBMIT: 'form_submit',
     86   GENERATED: 'generated',
     87   KEYWORD: 'keyword',
     88   KEYWORD_GENERATED: 'keyword_generated',
     89   LINK: 'link',
     90   MANUAL_SUBFRAME: 'manual_subframe',
     91   RELOAD: 'reload',
     92   START_PAGE: 'start_page',
     93   TYPED: 'typed'
     94 };
     95 
     96 /**
     97  * The possible transition qualifiers:
     98  *
     99  * * CLIENT_REDIRECT: Redirects caused by JavaScript, or a refresh meta tag
    100  *   on a page.
    101  *
    102  * * SERVER_REDIRECT: Redirected by the server via a 301/302 response.
    103  *
    104  * * FORWARD_BACK: User used the forward or back buttons to navigate through
    105  *   her browsing history.
    106  *
    107  * @enum {string}
    108  */
    109 NavigationCollector.NavigationQualifier = {
    110   CLIENT_REDIRECT: 'client_redirect',
    111   FORWARD_BACK: 'forward_back',
    112   SERVER_REDIRECT: 'server_redirect'
    113 };
    114 
    115 /**
    116  * @typedef {{url: string, transitionType: NavigationCollector.NavigationType,
    117  *     transitionQualifier: Array.<NavigationCollector.NavigationQualifier>,
    118  *     openedInNewTab: boolean, source: {frameId: ?number, tabId: ?number},
    119  *     duration: number}}
    120  */
    121 NavigationCollector.Request;
    122 
    123 ///////////////////////////////////////////////////////////////////////////////
    124 
    125 NavigationCollector.prototype = {
    126   /**
    127    * Returns a somewhat unique ID for a given WebNavigation request.
    128    *
    129    * @param {!{tabId: ?number, frameId: ?number}} data Information
    130    *     about the navigation event we'd like an ID for.
    131    * @return {!string} ID created by combining the source tab ID and frame ID
    132    *     (or target tab/frame IDs if there's no source), as the API ensures
    133    *     that these will be unique across a single navigation event.
    134    * @private
    135    */
    136   parseId_: function(data) {
    137     return data.tabId + '-' + (data.frameId ? data.frameId : 0);
    138   },
    139 
    140 
    141   /**
    142    * Creates an empty entry in the pending array if one doesn't already exist,
    143    * and prepopulates the errored and completed arrays for ease of insertion
    144    * later.
    145    *
    146    * @param {!string} id The request's ID, as produced by parseId_.
    147    * @param {!string} url The request's URL.
    148    */
    149   prepareDataStorage_: function(id, url) {
    150     this.pending_[id] = this.pending_[id] || {
    151       openedInNewTab: false,
    152       source: {
    153         frameId: null,
    154         tabId: null
    155       },
    156       start: null,
    157       transitionQualifiers: [],
    158       transitionType: null
    159     };
    160     this.completed_[url] = this.completed_[url] || [];
    161     this.errored_[url] = this.errored_[url] || [];
    162   },
    163 
    164 
    165   /**
    166    * Retrieves our saved data from storage.
    167    * @private
    168    */
    169   loadDataStorage_: function() {
    170     chrome.storage.local.get({
    171       "completed": {},
    172       "errored": {},
    173     }, function(storage) {
    174       this.completed_ = storage.completed;
    175       this.errored_ = storage.errored;
    176     }.bind(this));
    177   },
    178 
    179 
    180   /**
    181    * Persists our state to the storage API.
    182    * @private
    183    */
    184   saveDataStorage_: function() {
    185     chrome.storage.local.set({
    186       "completed": this.completed_,
    187       "errored": this.errored_,
    188     });
    189   },
    190 
    191 
    192   /**
    193    * Resets our saved state to empty.
    194    */
    195   resetDataStorage: function() {
    196     this.completed_ = {};
    197     this.errored_ = {};
    198     this.saveDataStorage_();
    199     // Load again, in case there is an outstanding storage.get request. This
    200     // one will reload the newly-cleared data.
    201     this.loadDataStorage_();
    202   },
    203 
    204 
    205   /**
    206    * Handler for the 'onCreatedNavigationTarget' event. Updates the
    207    * pending request with a source frame/tab, and notes that it was opened in a
    208    * new tab.
    209    *
    210    * Pushes the request onto the
    211    * 'pending_' object, and stores it for later use.
    212    *
    213    * @param {!Object} data The event data generated for this request.
    214    * @private
    215    */
    216   onCreatedNavigationTargetListener_: function(data) {
    217     var id = this.parseId_(data);
    218     this.prepareDataStorage_(id, data.url);
    219     this.pending_[id].openedInNewTab = data.tabId;
    220     this.pending_[id].source = {
    221       tabId: data.sourceTabId,
    222       frameId: data.sourceFrameId
    223     };
    224     this.pending_[id].start = data.timeStamp;
    225   },
    226 
    227 
    228   /**
    229    * Handler for the 'onBeforeNavigate' event. Pushes the request onto the
    230    * 'pending_' object, and stores it for later use.
    231    *
    232    * @param {!Object} data The event data generated for this request.
    233    * @private
    234    */
    235   onBeforeNavigateListener_: function(data) {
    236     var id = this.parseId_(data);
    237     this.prepareDataStorage_(id, data.url);
    238     this.pending_[id].start = this.pending_[id].start || data.timeStamp;
    239   },
    240 
    241 
    242   /**
    243    * Handler for the 'onCommitted' event. Updates the pending request with
    244    * transition information.
    245    *
    246    * Pushes the request onto the
    247    * 'pending_' object, and stores it for later use.
    248    *
    249    * @param {!Object} data The event data generated for this request.
    250    * @private
    251    */
    252   onCommittedListener_: function(data) {
    253     var id = this.parseId_(data);
    254     if (!this.pending_[id]) {
    255       console.warn(
    256           chrome.i18n.getMessage('errorCommittedWithoutPending'),
    257           data.url,
    258           data);
    259     } else {
    260       this.prepareDataStorage_(id, data.url);
    261       this.pending_[id].transitionType = data.transitionType;
    262       this.pending_[id].transitionQualifiers =
    263           data.transitionQualifiers;
    264     }
    265   },
    266 
    267 
    268   /**
    269    * Handler for the 'onReferenceFragmentUpdated' event. Updates the pending
    270    * request with transition information.
    271    *
    272    * Pushes the request onto the
    273    * 'pending_' object, and stores it for later use.
    274    *
    275    * @param {!Object} data The event data generated for this request.
    276    * @private
    277    */
    278   onReferenceFragmentUpdatedListener_: function(data) {
    279     var id = this.parseId_(data);
    280     if (!this.pending_[id]) {
    281       this.completed_[data.url] = this.completed_[data.url] || [];
    282       this.completed_[data.url].push({
    283         duration: 0,
    284         openedInNewWindow: false,
    285         source: {
    286           frameId: null,
    287           tabId: null
    288         },
    289         transitionQualifiers: data.transitionQualifiers,
    290         transitionType: data.transitionType,
    291         url: data.url
    292       });
    293       this.saveDataStorage_();
    294     } else {
    295       this.prepareDataStorage_(id, data.url);
    296       this.pending_[id].transitionType = data.transitionType;
    297       this.pending_[id].transitionQualifiers =
    298           data.transitionQualifiers;
    299     }
    300   },
    301 
    302 
    303   /**
    304    * Handler for the 'onHistoryStateUpdated' event. Updates the pending
    305    * request with transition information.
    306    *
    307    * Pushes the request onto the
    308    * 'pending_' object, and stores it for later use.
    309    *
    310    * @param {!Object} data The event data generated for this request.
    311    * @private
    312    */
    313   onHistoryStateUpdatedListener_: function(data) {
    314     var id = this.parseId_(data);
    315     if (!this.pending_[id]) {
    316       this.completed_[data.url] = this.completed_[data.url] || [];
    317       this.completed_[data.url].push({
    318         duration: 0,
    319         openedInNewWindow: false,
    320         source: {
    321           frameId: null,
    322           tabId: null
    323         },
    324         transitionQualifiers: data.transitionQualifiers,
    325         transitionType: data.transitionType,
    326         url: data.url
    327       });
    328       this.saveDataStorage_();
    329     } else {
    330       this.prepareDataStorage_(id, data.url);
    331       this.pending_[id].transitionType = data.transitionType;
    332       this.pending_[id].transitionQualifiers =
    333           data.transitionQualifiers;
    334     }
    335   },
    336 
    337 
    338   /**
    339    * Handler for the 'onCompleted` event. Pulls the request's data from the
    340    * 'pending_' object, combines it with the completed event's data, and pushes
    341    * a new NavigationCollector.Request object onto 'completed_'.
    342    *
    343    * @param {!Object} data The event data generated for this request.
    344    * @private
    345    */
    346   onCompletedListener_: function(data) {
    347     var id = this.parseId_(data);
    348     if (!this.pending_[id]) {
    349       console.warn(
    350           chrome.i18n.getMessage('errorCompletedWithoutPending'),
    351           data.url,
    352           data);
    353     } else {
    354       this.completed_[data.url].push({
    355         duration: (data.timeStamp - this.pending_[id].start),
    356         openedInNewWindow: this.pending_[id].openedInNewWindow,
    357         source: this.pending_[id].source,
    358         transitionQualifiers: this.pending_[id].transitionQualifiers,
    359         transitionType: this.pending_[id].transitionType,
    360         url: data.url
    361       });
    362       delete this.pending_[id];
    363       this.saveDataStorage_();
    364     }
    365   },
    366 
    367 
    368   /**
    369    * Handler for the 'onErrorOccurred` event. Pulls the request's data from the
    370    * 'pending_' object, combines it with the completed event's data, and pushes
    371    * a new NavigationCollector.Request object onto 'errored_'.
    372    *
    373    * @param {!Object} data The event data generated for this request.
    374    * @private
    375    */
    376   onErrorOccurredListener_: function(data) {
    377     var id = this.parseId_(data);
    378     if (!this.pending_[id]) {
    379       console.error(
    380           chrome.i18n.getMessage('errorErrorOccurredWithoutPending'),
    381           data.url,
    382           data);
    383     } else {
    384       this.prepareDataStorage_(id, data.url);
    385       this.errored_[data.url].push({
    386         duration: (data.timeStamp - this.pending_[id].start),
    387         openedInNewWindow: this.pending_[id].openedInNewWindow,
    388         source: this.pending_[id].source,
    389         transitionQualifiers: this.pending_[id].transitionQualifiers,
    390         transitionType: this.pending_[id].transitionType,
    391         url: data.url
    392       });
    393       delete this.pending_[id];
    394       this.saveDataStorage_();
    395     }
    396   },
    397 
    398   /**
    399    * Handle request messages from the popup.
    400    *
    401    * @param {!{type:string}} request The external request to answer.
    402    * @param {!MessageSender} sender Info about the script context that sent
    403    *     the request.
    404    * @param {!function} sendResponse Function to call to send a response.
    405    * @private
    406    */
    407   onRequestListener_: function(request, sender, sendResponse) {
    408     if (request.type === 'getMostRequestedUrls')
    409       sendResponse({result: this.getMostRequestedUrls(request.num)});
    410     else
    411       sendResponse({});
    412   },
    413 
    414 ///////////////////////////////////////////////////////////////////////////////
    415 
    416   /**
    417    * @return {Object.<string, NavigationCollector.Request>} The complete list of
    418    *     successful navigation requests.
    419    */
    420   get completed() {
    421     return this.completed_;
    422   },
    423 
    424 
    425   /**
    426    * @return {Object.<string, Navigationcollector.Request>} The complete list of
    427    *     unsuccessful navigation requests.
    428    */
    429   get errored() {
    430     return this.errored_;
    431   },
    432 
    433 
    434   /**
    435    * Get a list of the X most requested URLs.
    436    *
    437    * @param {number=} num The number of successful navigation requests to
    438    *     return. If 0 is passed in, or the argument left off entirely, all
    439    *     successful requests are returned.
    440    * @return {Object.<string, NavigationCollector.Request>} The list of
    441    *     successful navigation requests, sorted in decending order of frequency.
    442    */
    443   getMostRequestedUrls: function(num) {
    444     return this.getMostFrequentUrls_(this.completed, num);
    445   },
    446 
    447 
    448   /**
    449    * Get a list of the X most errored URLs.
    450    *
    451    * @param {number=} num The number of unsuccessful navigation requests to
    452    *     return. If 0 is passed in, or the argument left off entirely, all
    453    *     successful requests are returned.
    454    * @return {Object.<string, NavigationCollector.Request>} The list of
    455    *     unsuccessful navigation requests, sorted in decending order
    456    *     of frequency.
    457    */
    458   getMostErroredUrls: function(num) {
    459     return this.getMostErroredUrls_(this.errored, num);
    460   },
    461 
    462 
    463   /**
    464    * Get a list of the most frequent URLs in a list.
    465    *
    466    * @param {NavigationCollector.Request} list A list of URLs to parse.
    467    * @param {number=} num The number of navigation requests to return. If
    468    *     0 is passed in, or the argument left off entirely, all requests
    469    *     are returned.
    470    * @return {Object.<string, NavigationCollector.Request>} The list of
    471    *     navigation requests, sorted in decending order of frequency.
    472    * @private
    473    */
    474   getMostFrequentUrls_: function(list, num) {
    475     var result = [];
    476     var avg;
    477     // Convert the 'completed_' object to an array.
    478     for (var x in list) {
    479       avg = 0;
    480       if (list.hasOwnProperty(x) && list[x].length) {
    481         list[x].forEach(function(o) {
    482           avg += o.duration;
    483         });
    484         avg = avg / list[x].length;
    485         result.push({
    486           url: x,
    487           numRequests: list[x].length,
    488           requestList: list[x],
    489           average: avg
    490         });
    491       }
    492     }
    493     // Sort the array.
    494     result.sort(function(a, b) {
    495       return b.numRequests - a.numRequests;
    496     });
    497     // Return the requested number of results.
    498     return num ? result.slice(0, num) : result;
    499   }
    500 };
    501