Merge "Annotate REST interface calls with anonymized labels"
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 5d68e01..3f25834 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -28,6 +28,15 @@
   Defs.patchRange;
 
   /**
+   * @typedef {{
+   *    url: string,
+   *    fetchOptions: (Object|null|undefined),
+   *    anonymizedUrl: (string|undefined),
+   * }}
+   */
+  Defs.FetchRequest;
+
+  /**
    * Object to describe a request for passing into _fetchJSON or _fetchRawJSON.
    * - url is the URL for the request (excluding get params)
    * - errFn is a function to invoke when the request fails.
@@ -40,6 +49,8 @@
    *    cancelCondition: (function()|null|undefined),
    *    params: (Object|null|undefined),
    *    fetchOptions: (Object|null|undefined),
+   *    anonymizedUrl: (string|undefined),
+   *    reportUrlAsIs: (boolean|undefined),
    * }}
    */
   Defs.FetchJSONRequest;
@@ -53,6 +64,8 @@
    *   cancelCondition: (function()|null|undefined),
    *   params: (Object|null|undefined),
    *   fetchOptions: (Object|null|undefined),
+   *   anonymizedEndpoint: (string|undefined),
+   *   reportEndpointAsIs: (boolean|undefined),
    * }}
    */
   Defs.ChangeFetchRequest;
@@ -78,6 +91,8 @@
    *   contentType: (string|null|undefined),
    *   headers: (Object|undefined),
    *   parseResponse: (boolean|undefined),
+   *   anonymizedUrl: (string|undefined),
+   *   reportUrlAsIs: (boolean|undefined),
    * }}
    */
   Defs.SendRequest;
@@ -93,6 +108,8 @@
    *   contentType: (string|null|undefined),
    *   headers: (Object|undefined),
    *   parseResponse: (boolean|undefined),
+   *   anonymizedEndpoint: (string|undefined),
+   *   reportEndpointAsIs: (boolean|undefined),
    * }}
    */
   Defs.ChangeSendRequest;
@@ -115,6 +132,9 @@
       'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)';
   const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i;
 
+  const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*';
+  const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
+      '/revisions/*';
 
   Polymer({
     is: 'gr-rest-api-interface',
@@ -182,15 +202,14 @@
     /**
      * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
      * with timing and logging.
-     * @param {string} url
-     * @param {Object=} opt_fetchOptions
+     * @param {Defs.FetchRequest} req
      */
-    _fetch(url, opt_fetchOptions) {
+    _fetch(req) {
       const start = Date.now();
-      const xhr = this._auth.fetch(url, opt_fetchOptions);
+      const xhr = this._auth.fetch(req.url, req.fetchOptions);
 
       // Log the call after it completes.
-      xhr.then(res => this._logCall(url, opt_fetchOptions, start, res.status));
+      xhr.then(res => this._logCall(req, start, res.status));
 
       // Return the XHR directly (without the log).
       return xhr;
@@ -200,18 +219,23 @@
      * Log information about a REST call. Because the elapsed time is determined
      * by this method, it should be called immediately after the request
      * finishes.
-     * @param {string} url
-     * @param {Object|undefined} fetchOptions
+     * @param {Defs.FetchRequest} req
      * @param {number} startTime the time that the request was started.
      * @param {number} status the HTTP status of the response. The status value
      *     is used here rather than the response object so there is no way this
      *     method can read the body stream.
      */
-    _logCall(url, fetchOptions, startTime, status) {
-      const method = (fetchOptions && fetchOptions.method) ?
-          fetchOptions.method : 'GET';
-      const elapsed = (Date.now() - startTime) + 'ms';
-      console.log(['HTTP', status, method, elapsed, url].join(' '));
+    _logCall(req, startTime, status) {
+      const method = (req.fetchOptions && req.fetchOptions.method) ?
+          req.fetchOptions.method : 'GET';
+      const elapsed = (Date.now() - startTime);
+      console.log([
+        'HTTP',
+        status,
+        method,
+        elapsed + 'ms',
+        req.anonymizedUrl || req.url,
+      ].join(' '));
     },
 
     /**
@@ -223,7 +247,12 @@
      */
     _fetchRawJSON(req) {
       const urlWithParams = this._urlWithParams(req.url, req.params);
-      return this._fetch(urlWithParams, req.fetchOptions).then(res => {
+      const fetchReq = {
+        url: urlWithParams,
+        fetchOptions: req.fetchOptions,
+        anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
+      };
+      return this._fetch(fetchReq).then(res => {
         if (req.cancelCondition && req.cancelCondition()) {
           res.body.cancel();
           return;
@@ -324,10 +353,16 @@
 
     getConfig(noCache) {
       if (!noCache) {
-        return this._fetchSharedCacheURL({url: '/config/server/info'});
+        return this._fetchSharedCacheURL({
+          url: '/config/server/info',
+          reportUrlAsIs: true,
+        });
       }
 
-      return this._fetchJSON({url: '/config/server/info'});
+      return this._fetchJSON({
+        url: '/config/server/info',
+        reportUrlAsIs: true,
+      });
     },
 
     getRepo(repo, opt_errFn) {
@@ -336,6 +371,7 @@
       return this._fetchSharedCacheURL({
         url: '/projects/' + encodeURIComponent(repo),
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*',
       });
     },
 
@@ -345,6 +381,7 @@
       return this._fetchSharedCacheURL({
         url: '/projects/' + encodeURIComponent(repo) + '/config',
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/config',
       });
     },
 
@@ -353,6 +390,7 @@
       // supports it.
       return this._fetchSharedCacheURL({
         url: '/access/?project=' + encodeURIComponent(repo),
+        anonymizedUrl: '/access/?project=*',
       });
     },
 
@@ -362,6 +400,7 @@
       return this._fetchSharedCacheURL({
         url: `/projects/${encodeURIComponent(repo)}/dashboards?inherited`,
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/dashboards?inherited',
       });
     },
 
@@ -374,6 +413,7 @@
         url: `/projects/${encodeName}/config`,
         body: config,
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/config',
       });
     },
 
@@ -387,6 +427,7 @@
         url: `/projects/${encodeName}/gc`,
         body: '',
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/gc',
       });
     },
 
@@ -404,6 +445,7 @@
         url: `/projects/${encodeName}`,
         body: config,
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*',
       });
     },
 
@@ -419,6 +461,7 @@
         url: `/groups/${encodeName}`,
         body: config,
         errFn: opt_errFn,
+        anonymizedUrl: '/groups/*',
       });
     },
 
@@ -426,6 +469,7 @@
       return this._fetchJSON({
         url: `/groups/${encodeURIComponent(group)}/detail`,
         errFn: opt_errFn,
+        anonymizedUrl: '/groups/*/detail',
       });
     },
 
@@ -445,6 +489,7 @@
         url: `/projects/${encodeName}/branches/${encodeRef}`,
         body: '',
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/branches/*',
       });
     },
 
@@ -464,6 +509,7 @@
         url: `/projects/${encodeName}/tags/${encodeRef}`,
         body: '',
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/tags/*',
       });
     },
 
@@ -484,6 +530,7 @@
         url: `/projects/${encodeName}/branches/${encodeBranch}`,
         body: revision,
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/branches/*',
       });
     },
 
@@ -504,6 +551,7 @@
         url: `/projects/${encodeName}/tags/${encodeTag}`,
         body: revision,
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/tags/*',
       });
     },
 
@@ -513,7 +561,11 @@
      */
     getIsGroupOwner(groupName) {
       const encodeName = encodeURIComponent(groupName);
-      return this._fetchSharedCacheURL({url: `/groups/?owned&q=${encodeName}`})
+      const req = {
+        url: `/groups/?owned&q=${encodeName}`,
+        anonymizedUrl: '/groups/owned&q=*',
+      };
+      return this._fetchSharedCacheURL(req)
           .then(configs => configs.hasOwnProperty(groupName));
     },
 
@@ -522,12 +574,15 @@
       return this._fetchJSON({
         url: `/groups/${encodeName}/members/`,
         errFn: opt_errFn,
+        anonymizedUrl: '/groups/*/members',
       });
     },
 
     getIncludedGroup(groupName) {
-      const encodeName = encodeURIComponent(groupName);
-      return this._fetchJSON({url: `/groups/${encodeName}/groups/`});
+      return this._fetchJSON({
+        url: `/groups/${encodeURIComponent(groupName)}/groups/`,
+        anonymizedUrl: '/groups/*/groups',
+      });
     },
 
     saveGroupName(groupId, name) {
@@ -536,6 +591,7 @@
         method: 'PUT',
         url: `/groups/${encodeId}/name`,
         body: {name},
+        anonymizedUrl: '/groups/*/name',
       });
     },
 
@@ -545,6 +601,7 @@
         method: 'PUT',
         url: `/groups/${encodeId}/owner`,
         body: {owner: ownerId},
+        anonymizedUrl: '/groups/*/owner',
       });
     },
 
@@ -554,6 +611,7 @@
         method: 'PUT',
         url: `/groups/${encodeId}/description`,
         body: {description},
+        anonymizedUrl: '/groups/*/description',
       });
     },
 
@@ -563,6 +621,7 @@
         method: 'PUT',
         url: `/groups/${encodeId}/options`,
         body: options,
+        anonymizedUrl: '/groups/*/options',
       });
     },
 
@@ -570,6 +629,7 @@
       return this._fetchSharedCacheURL({
         url: '/groups/' + group + '/log.audit',
         errFn: opt_errFn,
+        anonymizedUrl: '/groups/*/log.audit',
       });
     },
 
@@ -580,6 +640,7 @@
         method: 'PUT',
         url: `/groups/${encodeName}/members/${encodeMember}`,
         parseResponse: true,
+        anonymizedUrl: '/groups/*/members/*',
       });
     },
 
@@ -590,6 +651,7 @@
         method: 'PUT',
         url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
         errFn: opt_errFn,
+        anonymizedUrl: '/groups/*/groups/*',
       };
       return this._send(req).then(response => {
         if (response.ok) {
@@ -604,6 +666,7 @@
       return this._send({
         method: 'DELETE',
         url: `/groups/${encodeName}/members/${encodeMember}`,
+        anonymizedUrl: '/groups/*/members/*',
       });
     },
 
@@ -613,11 +676,15 @@
       return this._send({
         method: 'DELETE',
         url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
+        anonymizedUrl: '/groups/*/groups/*',
       });
     },
 
     getVersion() {
-      return this._fetchSharedCacheURL({url: '/config/server/version'});
+      return this._fetchSharedCacheURL({
+        url: '/config/server/version',
+        reportUrlAsIs: true,
+      });
     },
 
     getDiffPreferences() {
@@ -625,6 +692,7 @@
         if (loggedIn) {
           return this._fetchSharedCacheURL({
             url: '/accounts/self/preferences.diff',
+            reportUrlAsIs: true,
           });
         }
         // These defaults should match the defaults in
@@ -655,6 +723,7 @@
         if (loggedIn) {
           return this._fetchSharedCacheURL({
             url: '/accounts/self/preferences.edit',
+            reportUrlAsIs: true,
           });
         }
         // These defaults should match the defaults in
@@ -696,6 +765,7 @@
         url: '/accounts/self/preferences',
         body: prefs,
         errFn: opt_errFn,
+        reportUrlAsIs: true,
       });
     },
 
@@ -711,6 +781,7 @@
         url: '/accounts/self/preferences.diff',
         body: prefs,
         errFn: opt_errFn,
+        reportUrlAsIs: true,
       });
     },
 
@@ -726,12 +797,14 @@
         url: '/accounts/self/preferences.edit',
         body: prefs,
         errFn: opt_errFn,
+        reportUrlAsIs: true,
       });
     },
 
     getAccount() {
       return this._fetchSharedCacheURL({
         url: '/accounts/self/detail',
+        reportUrlAsIs: true,
         errFn: resp => {
           if (!resp || resp.status === 403) {
             this._cache['/accounts/self/detail'] = null;
@@ -741,7 +814,10 @@
     },
 
     getExternalIds() {
-      return this._fetchJSON({url: '/accounts/self/external.ids'});
+      return this._fetchJSON({
+        url: '/accounts/self/external.ids',
+        reportUrlAsIs: true,
+      });
     },
 
     deleteAccountIdentity(id) {
@@ -750,6 +826,7 @@
         url: '/accounts/self/external.ids:delete',
         body: id,
         parseResponse: true,
+        reportUrlAsIs: true,
       });
     },
 
@@ -760,11 +837,15 @@
     getAccountDetails(userId) {
       return this._fetchJSON({
         url: `/accounts/${encodeURIComponent(userId)}/detail`,
+        anonymizedUrl: '/accounts/*/detail',
       });
     },
 
     getAccountEmails() {
-      return this._fetchSharedCacheURL({url: '/accounts/self/emails'});
+      return this._fetchSharedCacheURL({
+        url: '/accounts/self/emails',
+        reportUrlAsIs: true,
+      });
     },
 
     /**
@@ -776,6 +857,7 @@
         method: 'PUT',
         url: '/accounts/self/emails/' + encodeURIComponent(email),
         errFn: opt_errFn,
+        anonymizedUrl: '/account/self/emails/*',
       });
     },
 
@@ -788,6 +870,7 @@
         method: 'DELETE',
         url: '/accounts/self/emails/' + encodeURIComponent(email),
         errFn: opt_errFn,
+        anonymizedUrl: '/accounts/self/email/*',
       });
     },
 
@@ -797,8 +880,13 @@
      */
     setPreferredAccountEmail(email, opt_errFn) {
       const encodedEmail = encodeURIComponent(email);
-      const url = `/accounts/self/emails/${encodedEmail}/preferred`;
-      return this._send({method: 'PUT', url, errFn: opt_errFn}).then(() => {
+      const req = {
+        method: 'PUT',
+        url: `/accounts/self/emails/${encodedEmail}/preferred`,
+        errFn: opt_errFn,
+        anonymizedUrl: '/accounts/self/emails/*/preferred',
+      };
+      return this._send(req).then(() => {
         // If result of getAccountEmails is in cache, update it in the cache
         // so we don't have to invalidate it.
         const cachedEmails = this._cache['/accounts/self/emails'];
@@ -840,6 +928,7 @@
         body: {name},
         errFn: opt_errFn,
         parseResponse: true,
+        reportUrlAsIs: true,
       };
       return this._send(req)
           .then(newName => this._updateCachedAccount({name: newName}));
@@ -856,6 +945,7 @@
         body: {username},
         errFn: opt_errFn,
         parseResponse: true,
+        reportUrlAsIs: true,
       };
       return this._send(req)
           .then(newName => this._updateCachedAccount({username: newName}));
@@ -872,6 +962,7 @@
         body: {status},
         errFn: opt_errFn,
         parseResponse: true,
+        reportUrlAsIs: true,
       };
       return this._send(req)
           .then(newStatus => this._updateCachedAccount({status: newStatus}));
@@ -880,15 +971,22 @@
     getAccountStatus(userId) {
       return this._fetchJSON({
         url: `/accounts/${encodeURIComponent(userId)}/status`,
+        anonymizedUrl: '/accounts/*/status',
       });
     },
 
     getAccountGroups() {
-      return this._fetchJSON({url: '/accounts/self/groups'});
+      return this._fetchJSON({
+        url: '/accounts/self/groups',
+        reportUrlAsIs: true,
+      });
     },
 
     getAccountAgreements() {
-      return this._fetchJSON({url: '/accounts/self/agreements'});
+      return this._fetchJSON({
+        url: '/accounts/self/agreements',
+        reportUrlAsIs: true,
+      });
     },
 
     saveAccountAgreement(name) {
@@ -896,6 +994,7 @@
         method: 'PUT',
         url: '/accounts/self/agreements',
         body: name,
+        reportUrlAsIs: true,
       });
     },
 
@@ -911,6 +1010,7 @@
       }
       return this._fetchSharedCacheURL({
         url: '/accounts/self/capabilities' + queryString,
+        anonymizedUrl: '/accounts/self/capabilities?q=*',
       });
     },
 
@@ -937,8 +1037,9 @@
         return;
       }
       this._credentialCheck.checking = true;
+      const req = {url: '/accounts/self/detail', reportUrlAsIs: true};
       // Skip the REST response cache.
-      return this._fetchRawJSON({url: '/accounts/self/detail'}).then(res => {
+      return this._fetchRawJSON(req).then(res => {
         if (!res) { return; }
         if (res.status === 403) {
           this.fire('auth-error');
@@ -958,21 +1059,24 @@
     },
 
     getDefaultPreferences() {
-      return this._fetchSharedCacheURL({url: '/config/server/preferences'});
+      return this._fetchSharedCacheURL({
+        url: '/config/server/preferences',
+        reportUrlAsIs: true,
+      });
     },
 
     getPreferences() {
       return this.getLoggedIn().then(loggedIn => {
         if (loggedIn) {
-          return this._fetchSharedCacheURL({url: '/accounts/self/preferences'})
-              .then(res => {
-                if (this._isNarrowScreen()) {
-                  res.default_diff_view = DiffViewMode.UNIFIED;
-                } else {
-                  res.default_diff_view = res.diff_view;
-                }
-                return Promise.resolve(res);
-              });
+          const req = {url: '/accounts/self/preferences', reportUrlAsIs: true};
+          return this._fetchSharedCacheURL(req).then(res => {
+            if (this._isNarrowScreen()) {
+              res.default_diff_view = DiffViewMode.UNIFIED;
+            } else {
+              res.default_diff_view = res.diff_view;
+            }
+            return Promise.resolve(res);
+          });
         }
 
         return Promise.resolve({
@@ -988,6 +1092,7 @@
     getWatchedProjects() {
       return this._fetchSharedCacheURL({
         url: '/accounts/self/watched.projects',
+        reportUrlAsIs: true,
       });
     },
 
@@ -1002,6 +1107,7 @@
         body: projects,
         errFn: opt_errFn,
         parseResponse: true,
+        reportUrlAsIs: true,
       });
     },
 
@@ -1015,6 +1121,7 @@
         url: '/accounts/self/watched.projects:delete',
         body: projects,
         errFn: opt_errFn,
+        reportUrlAsIs: true,
       });
     },
 
@@ -1079,7 +1186,12 @@
           this._maybeInsertInLookup(change);
         }
       };
-      return this._fetchJSON({url: '/changes/', params}).then(response => {
+      const req = {
+        url: '/changes/',
+        params,
+        reportUrlAsIs: true,
+      };
+      return this._fetchJSON(req).then(response => {
         // Response may be an array of changes OR an array of arrays of
         // changes.
         if (opt_query instanceof Array) {
@@ -1173,6 +1285,7 @@
           cancelCondition: opt_cancelCondition,
           params: {O: params},
           fetchOptions: this._etags.getOptions(urlWithParams),
+          anonymizedUrl: '/changes/*~*/detail?O=' + params,
         };
         return this._fetchRawJSON(req).then(response => {
           if (response && response.status === 304) {
@@ -1213,6 +1326,7 @@
         changeNum,
         endpoint: '/commit?links',
         patchNum,
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1233,6 +1347,7 @@
         endpoint: '/files',
         patchNum: patchRange.patchNum,
         params,
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1242,10 +1357,16 @@
      */
     getChangeEditFiles(changeNum, patchRange) {
       let endpoint = '/edit?list';
+      let anonymizedEndpoint = endpoint;
       if (patchRange.basePatchNum !== 'PARENT') {
         endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum + '');
+        anonymizedEndpoint += '&base=*';
       }
-      return this._getChangeURLAndFetch({changeNum, endpoint});
+      return this._getChangeURLAndFetch({
+        changeNum,
+        endpoint,
+        anonymizedEndpoint,
+      });
     },
 
     /**
@@ -1259,6 +1380,7 @@
         changeNum,
         endpoint: `/files?q=${encodeURIComponent(query)}`,
         patchNum,
+        anonymizedEndpoint: '/files?q=*',
       });
     },
 
@@ -1287,7 +1409,12 @@
     },
 
     getChangeRevisionActions(changeNum, patchNum) {
-      const req = {changeNum, endpoint: '/actions', patchNum};
+      const req = {
+        changeNum,
+        endpoint: '/actions',
+        patchNum,
+        reportEndpointAsIs: true,
+      };
       return this._getChangeURLAndFetch(req).then(revisionActions => {
         // The rebase button on change screen is always enabled.
         if (revisionActions.rebase) {
@@ -1312,6 +1439,7 @@
         endpoint: '/suggest_reviewers',
         errFn: opt_errFn,
         params,
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1319,7 +1447,11 @@
      * @param {number|string} changeNum
      */
     getChangeIncludedIn(changeNum) {
-      return this._getChangeURLAndFetch({changeNum, endpoint: '/in'});
+      return this._getChangeURLAndFetch({
+        changeNum,
+        endpoint: '/in',
+        reportEndpointAsIs: true,
+      });
     },
 
     _computeFilter(filter) {
@@ -1345,6 +1477,7 @@
       return this._fetchSharedCacheURL({
         url: `/groups/?n=${groupsPerPage + 1}&S=${offset}` +
             this._computeFilter(filter),
+        anonymizedUrl: '/groups/?*',
       });
     },
 
@@ -1362,6 +1495,7 @@
       return this._fetchSharedCacheURL({
         url: `/projects/?d&n=${reposPerPage + 1}&S=${offset}` +
             this._computeFilter(filter),
+        anonymizedUrl: '/projects/?*',
       });
     },
 
@@ -1372,6 +1506,7 @@
         method: 'PUT',
         url: `/projects/${encodeURIComponent(repo)}/HEAD`,
         body: {ref},
+        anonymizedUrl: '/projects/*/HEAD',
       });
     },
 
@@ -1391,7 +1526,11 @@
       const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`;
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._fetchJSON({url, errFn: opt_errFn});
+      return this._fetchJSON({
+        url,
+        errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/branches?*',
+      });
     },
 
     /**
@@ -1411,7 +1550,11 @@
           encodedFilter;
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._fetchJSON({url, errFn: opt_errFn});
+      return this._fetchJSON({
+        url,
+        errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/tags',
+      });
     },
 
     /**
@@ -1426,7 +1569,11 @@
       const encodedFilter = this._computeFilter(filter);
       const n = pluginsPerPage + 1;
       const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`;
-      return this._fetchJSON({url, errFn: opt_errFn});
+      return this._fetchJSON({
+        url,
+        errFn: opt_errFn,
+        anonymizedUrl: '/plugins/?all',
+      });
     },
 
     getRepoAccessRights(repoName, opt_errFn) {
@@ -1435,6 +1582,7 @@
       return this._fetchJSON({
         url: `/projects/${encodeURIComponent(repoName)}/access`,
         errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/access',
       });
     },
 
@@ -1445,6 +1593,7 @@
         method: 'POST',
         url: `/projects/${encodeURIComponent(repoName)}/access`,
         body: repoInfo,
+        anonymizedUrl: '/projects/*/access',
       });
     },
 
@@ -1454,6 +1603,7 @@
         url: `/projects/${encodeURIComponent(projectName)}/access:review`,
         body: projectInfo,
         parseResponse: true,
+        anonymizedUrl: '/projects/*/access:review',
       });
     },
 
@@ -1469,6 +1619,7 @@
         url: '/groups/',
         errFn: opt_errFn,
         params,
+        reportUrlAsIs: true,
       });
     },
 
@@ -1488,6 +1639,7 @@
         url: '/projects/',
         errFn: opt_errFn,
         params,
+        reportUrlAsIs: true,
       });
     },
 
@@ -1506,6 +1658,7 @@
         url: '/accounts/',
         errFn: opt_errFn,
         params,
+        anonymizedUrl: '/accounts/?n=*',
       });
     },
 
@@ -1541,6 +1694,7 @@
         changeNum,
         endpoint: '/related',
         patchNum,
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1548,6 +1702,7 @@
       return this._getChangeURLAndFetch({
         changeNum,
         endpoint: '/submitted_together',
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1560,7 +1715,11 @@
         O: options,
         q: 'status:open is:mergeable conflicts:' + changeNum,
       };
-      return this._fetchJSON({url: '/changes/', params});
+      return this._fetchJSON({
+        url: '/changes/',
+        params,
+        anonymizedUrl: '/changes/conflicts:*',
+      });
     },
 
     getChangeCherryPicks(project, changeID, changeNum) {
@@ -1578,7 +1737,11 @@
         O: options,
         q: query,
       };
-      return this._fetchJSON({url: '/changes/', params});
+      return this._fetchJSON({
+        url: '/changes/',
+        params,
+        anonymizedUrl: '/changes/change:*',
+      });
     },
 
     getChangesWithSameTopic(topic) {
@@ -1592,7 +1755,11 @@
         O: options,
         q: 'status:open topic:' + topic,
       };
-      return this._fetchJSON({url: '/changes/', params});
+      return this._fetchJSON({
+        url: '/changes/',
+        params,
+        anonymizedUrl: '/changes/topic:*',
+      });
     },
 
     getReviewedFiles(changeNum, patchNum) {
@@ -1600,6 +1767,7 @@
         changeNum,
         endpoint: '/files?reviewed',
         patchNum,
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1617,6 +1785,7 @@
         patchNum,
         endpoint: `/files/${encodeURIComponent(path)}/reviewed`,
         errFn: opt_errFn,
+        anonymizedEndpoint: '/files/*/reviewed',
       });
     },
 
@@ -1649,6 +1818,7 @@
           changeNum,
           endpoint: '/edit/',
           params,
+          reportEndpointAsIs: true,
         });
       });
     },
@@ -1679,6 +1849,7 @@
           base_commit: opt_baseCommit,
         },
         parseResponse: true,
+        reportUrlAsIs: true,
       });
     },
 
@@ -1725,6 +1896,7 @@
         endpoint: `/files/${encodeURIComponent(path)}/content`,
         errFn: opt_errFn,
         headers: {Accept: 'application/json'},
+        anonymizedEndpoint: '/files/*/content',
       });
     },
 
@@ -1739,6 +1911,7 @@
         method: 'GET',
         endpoint: '/edit/' + encodeURIComponent(path),
         headers: {Accept: 'application/json'},
+        anonymizedEndpoint: '/edit/*',
       });
     },
 
@@ -1747,6 +1920,7 @@
         changeNum,
         method: 'POST',
         endpoint: '/edit:rebase',
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1755,6 +1929,7 @@
         changeNum,
         method: 'DELETE',
         endpoint: '/edit',
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1764,6 +1939,7 @@
         method: 'POST',
         endpoint: '/edit',
         body: {restore_path},
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1773,6 +1949,7 @@
         method: 'POST',
         endpoint: '/edit',
         body: {old_path, new_path},
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1781,6 +1958,7 @@
         changeNum,
         method: 'DELETE',
         endpoint: '/edit/' + encodeURIComponent(path),
+        anonymizedEndpoint: '/edit/*',
       });
     },
 
@@ -1791,6 +1969,7 @@
         endpoint: '/edit/' + encodeURIComponent(path),
         body: contents,
         contentType: 'text/plain',
+        anonymizedEndpoint: '/edit/*',
       });
     },
 
@@ -1801,6 +1980,7 @@
         method: 'PUT',
         endpoint: '/edit:message',
         body: {message},
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1809,6 +1989,7 @@
         changeNum,
         method: 'POST',
         endpoint: '/edit:publish',
+        reportEndpointAsIs: true,
       });
     },
 
@@ -1818,13 +1999,16 @@
         method: 'PUT',
         endpoint: '/message',
         body: {message},
+        reportEndpointAsIs: true,
       });
     },
 
     saveChangeStarred(changeNum, starred) {
-      const url = '/accounts/self/starred.changes/' + changeNum;
-      const method = starred ? 'PUT' : 'DELETE';
-      return this._send({method, url});
+      return this._send({
+        method: starred ? 'PUT' : 'DELETE',
+        url: '/accounts/self/starred.changes/' + changeNum,
+        anonymizedUrl: '/accounts/self/starred.changes/*',
+      });
     },
 
     /**
@@ -1850,7 +2034,12 @@
       }
       const url = req.url.startsWith('http') ?
           req.url : this.getBaseUrl() + req.url;
-      const xhr = this._fetch(url, options).then(response => {
+      const fetchReq = {
+        url,
+        fetchOptions: options,
+        anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
+      };
+      const xhr = this._fetch(fetchReq).then(response => {
         if (!response.ok) {
           if (req.errFn) {
             return req.errFn.call(undefined, response);
@@ -1928,6 +2117,7 @@
         errFn: opt_errFn,
         cancelCondition: opt_cancelCondition,
         params,
+        anonymizedEndpoint: '/files/*/diff',
       });
     },
 
@@ -2019,6 +2209,7 @@
           changeNum,
           endpoint,
           patchNum: opt_patchNum,
+          reportEndpointAsIs: true,
         });
       };
 
@@ -2109,8 +2300,10 @@
     _sendDiffDraftRequest(method, changeNum, patchNum, draft) {
       const isCreate = !draft.id && method === 'PUT';
       let endpoint = '/drafts';
+      let anonymizedEndpoint = endpoint;
       if (draft.id) {
         endpoint += '/' + draft.id;
+        anonymizedEndpoint += '/*';
       }
       let body;
       if (method === 'PUT') {
@@ -2121,8 +2314,16 @@
         this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
       }
 
-      const promise = this._getChangeURLAndSend(
-          {changeNum, method, patchNum, endpoint, body});
+      const req = {
+        changeNum,
+        method,
+        patchNum,
+        endpoint,
+        body,
+        anonymizedEndpoint,
+      };
+
+      const promise = this._getChangeURLAndSend(req);
       this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
 
       if (isCreate) {
@@ -2136,11 +2337,12 @@
       return this._fetchJSON({
         url: '/projects/' + encodeURIComponent(project) +
             '/commits/' + encodeURIComponent(commit),
+        anonymizedUrl: '/projects/*/comments/*',
       });
     },
 
     _fetchB64File(url) {
-      return this._fetch(this.getBaseUrl() + url)
+      return this._fetch({url: this.getBaseUrl() + url})
           .then(response => {
             if (!response.ok) { return Promise.reject(response.statusText); }
             const type = response.headers.get('X-FYI-Content-Type');
@@ -2241,6 +2443,7 @@
         endpoint: '/topic',
         body: {topic},
         parseResponse: true,
+        reportUrlAsIs: true,
       });
     },
 
@@ -2256,6 +2459,7 @@
         endpoint: '/hashtags',
         body: hashtag,
         parseResponse: true,
+        reportUrlAsIs: true,
       });
     },
 
@@ -2263,6 +2467,7 @@
       return this._send({
         method: 'DELETE',
         url: '/accounts/self/password.http',
+        reportUrlAsIs: true,
       });
     },
 
@@ -2277,11 +2482,15 @@
         url: '/accounts/self/password.http',
         body: {generate: true},
         parseResponse: true,
+        reportUrlAsIs: true,
       });
     },
 
     getAccountSSHKeys() {
-      return this._fetchSharedCacheURL({url: '/accounts/self/sshkeys'});
+      return this._fetchSharedCacheURL({
+        url: '/accounts/self/sshkeys',
+        reportUrlAsIs: true,
+      });
     },
 
     addAccountSSHKey(key) {
@@ -2290,6 +2499,7 @@
         url: '/accounts/self/sshkeys',
         body: key,
         contentType: 'plain/text',
+        reportUrlAsIs: true,
       };
       return this._send(req)
           .then(response => {
@@ -2308,15 +2518,24 @@
       return this._send({
         method: 'DELETE',
         url: '/accounts/self/sshkeys/' + id,
+        anonymizedUrl: '/accounts/self/sshkeys/*',
       });
     },
 
     getAccountGPGKeys() {
-      return this._fetchJSON({url: '/accounts/self/gpgkeys'});
+      return this._fetchJSON({
+        url: '/accounts/self/gpgkeys',
+        reportUrlAsIs: true,
+      });
     },
 
     addAccountGPGKey(key) {
-      const req = {method: 'POST', url: '/accounts/self/gpgkeys', body: key};
+      const req = {
+        method: 'POST',
+        url: '/accounts/self/gpgkeys',
+        body: key,
+        reportUrlAsIs: true,
+      };
       return this._send(req)
           .then(response => {
             if (response.status < 200 && response.status >= 300) {
@@ -2334,6 +2553,7 @@
       return this._send({
         method: 'DELETE',
         url: '/accounts/self/gpgkeys/' + id,
+        anonymizedUrl: '/accounts/self/gpgkeys/*',
       });
     },
 
@@ -2342,6 +2562,7 @@
         changeNum,
         method: 'DELETE',
         endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`,
+        anonymizedEndpoint: '/reviewers/*/votes/*',
       });
     },
 
@@ -2351,6 +2572,7 @@
         method: 'PUT', patchNum,
         endpoint: '/description',
         body: {description: desc},
+        reportUrlAsIs: true,
       });
     },
 
@@ -2359,6 +2581,7 @@
         method: 'PUT',
         url: '/config/server/email.confirm',
         body: {token},
+        reportUrlAsIs: true,
       };
       return this._send(req).then(response => {
         if (response.status === 204) {
@@ -2372,6 +2595,7 @@
       return this._fetchJSON({
         url: '/config/server/capabilities',
         errFn: opt_errFn,
+        reportUrlAsIs: true,
       });
     },
 
@@ -2381,6 +2605,7 @@
         method: 'PUT',
         endpoint: '/assignee',
         body: {assignee},
+        reportUrlAsIs: true,
       });
     },
 
@@ -2389,6 +2614,7 @@
         changeNum,
         method: 'DELETE',
         endpoint: '/assignee',
+        reportUrlAsIs: true,
       });
     },
 
@@ -2408,7 +2634,13 @@
       if (opt_message) {
         body.message = opt_message;
       }
-      const req = {changeNum, method: 'POST', endpoint: '/wip', body};
+      const req = {
+        changeNum,
+        method: 'POST',
+        endpoint: '/wip',
+        body,
+        reportUrlAsIs: true,
+      };
       return this._getChangeURLAndSend(req).then(response => {
         if (response.status === 204) {
           return 'Change marked as Work In Progress.';
@@ -2428,6 +2660,7 @@
         endpoint: '/ready',
         body: opt_body,
         errFn: opt_errFn,
+        reportUrlAsIs: true,
       });
     },
 
@@ -2444,6 +2677,7 @@
         endpoint: `/comments/${commentID}/delete`,
         body: {reason},
         parseResponse: true,
+        anonymizedEndpoint: '/comments/*/delete',
       });
     },
 
@@ -2459,6 +2693,7 @@
       return this._fetchJSON({
         url: `/changes/?q=change:${changeNum}`,
         errFn: opt_errFn,
+        anonymizedUrl: '/changes/?q=change:*',
       }).then(res => {
         if (!res || !res.length) { return null; }
         return res[0];
@@ -2509,6 +2744,11 @@
      * @return {!Promise<!Object>}
      */
     _getChangeURLAndSend(req) {
+      const anonymizedBaseUrl = req.patchNum ?
+          ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
+      const anonymizedEndpoint = req.reportEndpointAsIs ?
+          req.endpoint : req.anonymizedEndpoint;
+
       return this._changeBaseURL(req.changeNum, req.patchNum).then(url => {
         return this._send({
           method: req.method,
@@ -2518,6 +2758,8 @@
           contentType: req.contentType,
           headers: req.headers,
           parseResponse: req.parseResponse,
+          anonymizedUrl: anonymizedEndpoint ?
+              (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
         });
       });
     },
@@ -2528,6 +2770,10 @@
      * @return {!Promise<!Object>}
      */
     _getChangeURLAndFetch(req) {
+      const anonymizedEndpoint = req.reportEndpointAsIs ?
+          req.endpoint : req.anonymizedEndpoint;
+      const anonymizedBaseUrl = req.patchNum ?
+          ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
       return this._changeBaseURL(req.changeNum, req.patchNum).then(url => {
         return this._fetchJSON({
           url: url + req.endpoint,
@@ -2535,6 +2781,8 @@
           cancelCondition: req.cancelCondition,
           params: req.params,
           fetchOptions: req.fetchOptions,
+          anonymizedUrl: anonymizedEndpoint ?
+              (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
         });
       });
     },
@@ -2577,6 +2825,7 @@
         endpoint: `/files/${encodedPath}/blame`,
         patchNum,
         params: opt_base ? {base: 't'} : undefined,
+        anonymizedEndpoint: '/files/*/blame',
       });
     },
 
@@ -2621,7 +2870,11 @@
     getDashboard(project, dashboard, opt_errFn) {
       const url = '/projects/' + encodeURIComponent(project) + '/dashboards/' +
           encodeURIComponent(dashboard);
-      return this._fetchSharedCacheURL({url, errFn: opt_errFn});
+      return this._fetchSharedCacheURL({
+        url,
+        errFn: opt_errFn,
+        anonymizedUrl: '/projects/*/dashboards/*',
+      });
     },
 
     getMergeable(changeNum) {
@@ -2629,6 +2882,7 @@
         changeNum,
         endpoint: '/revisions/current/mergeable',
         parseResponse: true,
+        reportEndpointAsIs: true,
       });
     },
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 70d1465..0d25509 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -711,11 +711,10 @@
       sandbox.spy(element, '_send');
       element.confirmEmail('foo');
       assert.isTrue(element._send.calledOnce);
-      assert.deepEqual(element._send.lastCall.args[0], {
-        method: 'PUT',
-        url: '/config/server/email.confirm',
-        body: {token: 'foo'},
-      });
+      assert.equal(element._send.lastCall.args[0].method, 'PUT');
+      assert.equal(element._send.lastCall.args[0].url,
+          '/config/server/email.confirm');
+      assert.deepEqual(element._send.lastCall.args[0].body, {token: 'foo'});
     });
 
     test('GrReviewerUpdatesParser.parse is used', () => {
@@ -924,11 +923,10 @@
       const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
           .returns(Promise.resolve());
       return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => {
-        assert.deepEqual(fetchStub.lastCall.args[0], {
-          changeNum: '42',
-          endpoint: '/files?q=test%2Fpath.js',
-          patchNum: 'edit',
-        });
+        assert.equal(fetchStub.lastCall.args[0].changeNum, '42');
+        assert.equal(fetchStub.lastCall.args[0].endpoint,
+            '/files?q=test%2Fpath.js');
+        assert.equal(fetchStub.lastCall.args[0].patchNum, 'edit');
       });
     });
 
@@ -1387,10 +1385,10 @@
       sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response));
       const startTime = 123;
       sandbox.stub(Date, 'now').returns(startTime);
-      return element._fetch(url, fetchOptions).then(() => {
+      const req = {url, fetchOptions};
+      return element._fetch(req).then(() => {
         assert.isTrue(logStub.calledOnce);
-        assert.isTrue(logStub.calledWith(
-            url, fetchOptions, startTime, response.status));
+        assert.isTrue(logStub.calledWith(req, startTime, response.status));
         assert.isFalse(response.text.called);
       });
     });