Add a 'usp' URL parameter that can be used for tracking

Well, actually this change *removes* the URL parameter. :-) So the
webserver will get and log it, but the user will not be bothered by it.

Change-Id: Ieb1374ce9ff5ec30b589a7b340d24cb6f50d5653
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 676ef7b..e0bd0b9 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -68,6 +68,7 @@
 import {firePageError} from '../../../utils/event-util';
 import {addQuotesWhen} from '../../../utils/string-util';
 import {windowLocationReload} from '../../../utils/dom-util';
+import {toPath, toPathname, toSearchParams} from '../../../utils/url-util';
 
 const RoutePattern = {
   ROOT: '/',
@@ -762,20 +763,21 @@
 
   /**  Page.js middleware that try parse the querystring into queryMap. */
   _queryStringMiddleware(ctx: PageContext, next: PageNextCallback) {
-    let queryMap: Map<string, string> | URLSearchParams = new Map<
-      string,
-      string
-    >();
+    (ctx as PageContextWithQueryMap).queryMap = this.createQueryMap(ctx);
+    next();
+  }
+
+  private createQueryMap(ctx: PageContext) {
     if (ctx.querystring) {
       // https://caniuse.com/#search=URLSearchParams
       if (window.URLSearchParams) {
-        queryMap = new URLSearchParams(ctx.querystring);
+        return new URLSearchParams(ctx.querystring);
       } else {
-        queryMap = new Map(this._parseQueryString(ctx.querystring));
+        this.reporting.reportExecution('noURLSearchParams');
+        return new Map(this._parseQueryString(ctx.querystring));
       }
     }
-    (ctx as PageContextWithQueryMap).queryMap = queryMap;
-    next();
+    return new Map<string, string>();
   }
 
   /**
@@ -806,13 +808,13 @@
       pattern,
       (ctx, next) => this._loadUserMiddleware(ctx, next),
       (ctx, next) => this._queryStringMiddleware(ctx, next),
-      data => {
+      ctx => {
         this.reporting.locationChanged(handlerName);
         const promise = authRedirect
-          ? this._redirectIfNotLoggedIn(data)
+          ? this._redirectIfNotLoggedIn(ctx)
           : Promise.resolve();
         promise.then(() => {
-          this[handlerName](data as PageContextWithQueryMap);
+          this[handlerName](ctx as PageContextWithQueryMap);
         });
       }
     );
@@ -846,6 +848,21 @@
       next();
     });
 
+    // Remove the tracking param 'usp' (User Source Parameter) from the URL,
+    // just to have users look at cleaner URLs.
+    page((ctx, next) => {
+      if (window.URLSearchParams) {
+        const pathname = toPathname(ctx.canonicalPath);
+        const searchParams = toSearchParams(ctx.canonicalPath);
+        if (searchParams.has('usp')) {
+          searchParams.delete('usp');
+          this._redirect(toPath(pathname, searchParams));
+          return;
+        }
+      }
+      next();
+    });
+
     // Middleware
     page((ctx, next) => {
       document.body.scrollTop = 0;
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
index 1be1d63..a7b5410 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -97,7 +97,7 @@
    *
    * Every execution is only reported once per session.
    */
-  reportExecution(id: string, details: EventDetails): void;
+  reportExecution(id: string, details?: EventDetails): void;
   trackApi(plugin: PluginApi, object: string, method: string): void;
   reportInteraction(eventName: string, details?: EventDetails): void;
   /**
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index e57670d..f1e09c1 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -792,7 +792,7 @@
     );
   }
 
-  reportExecution(id: string, details: EventDetails) {
+  reportExecution(id: string, details?: EventDetails) {
     if (this.executionReported.has(id)) return;
     this.executionReported.add(id);
     this.reporter(
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
index 7d66484..047bb4e 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
@@ -65,7 +65,7 @@
   error: () => {
     log('error');
   },
-  reportExecution: (id: string, details: EventDetails) => {
+  reportExecution: (id: string, details?: EventDetails) => {
     log(`reportExecution '${id}': ${JSON.stringify(details)}`);
   },
   trackApi: (plugin: PluginApi, object: string, method: string) => {
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index f977ab6..4115062 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -78,3 +78,33 @@
   const withoutPlus = url.replace(/\+/g, '%20');
   return decodeURIComponent(withoutPlus);
 }
+
+/**
+ * @param path URL path including search params, but without host
+ */
+export function toPathname(path: string) {
+  const i = path.indexOf('?');
+  const hasQuery = i > -1;
+  const pathname = hasQuery ? path.slice(0, i) : path;
+  return pathname;
+}
+
+/**
+ * @param path URL path including search params, but without host
+ */
+export function toSearchParams(path: string) {
+  const i = path.indexOf('?');
+  const hasQuery = i > -1;
+  const querystring = hasQuery ? path.slice(i + 1) : '';
+  return new URLSearchParams(querystring);
+}
+
+/**
+ * @param pathname URL path without search params
+ * @param params
+ */
+export function toPath(pathname: string, searchParams: URLSearchParams) {
+  const paramString = searchParams.toString();
+  const middle = paramString ? '?' : '';
+  return pathname + middle + paramString;
+}
diff --git a/polygerrit-ui/app/utils/url-util_test.js b/polygerrit-ui/app/utils/url-util_test.js
index b1b17f4..5cd4bb4 100644
--- a/polygerrit-ui/app/utils/url-util_test.js
+++ b/polygerrit-ui/app/utils/url-util_test.js
@@ -20,7 +20,11 @@
   getBaseUrl,
   getDocsBaseUrl,
   _testOnly_clearDocsBaseUrlCache,
-  encodeURL, singleDecodeURL,
+  encodeURL,
+  singleDecodeURL,
+  toPath,
+  toPathname,
+  toSearchParams,
 } from './url-util.js';
 
 suite('url-util tests', () => {
@@ -124,4 +128,23 @@
       });
     });
   });
+
+  test('toPathname', () => {
+    assert.equal(toPathname('asdf'), 'asdf');
+    assert.equal(toPathname('asdf?qwer=zxcv'), 'asdf');
+  });
+
+  test('toSearchParams', () => {
+    assert.equal(toSearchParams('asdf').toString(), '');
+    assert.equal(toSearchParams('asdf?qwer=zxcv').get('qwer'), 'zxcv');
+  });
+
+  test('toPathname', () => {
+    const params = new URLSearchParams();
+    assert.equal(toPath('asdf', params), 'asdf');
+    params.set('qwer', 'zxcv');
+    assert.equal(toPath('asdf', params), 'asdf?qwer=zxcv');
+    assert.equal(toPath(toPathname('asdf?qwer=zxcv'),
+        toSearchParams('asdf?qwer=zxcv')), 'asdf?qwer=zxcv');
+  });
 });