Merge "Add undefined check for project"
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 956a94d..ad3443b 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -326,227 +326,49 @@
 ----
 
 
-[[font-roboto-local-fonts-robotomono]]
-font-roboto-local-fonts-robotomono
+[[Polymer-2017]]
+Polymer-2017
 
-* @polymer/font-roboto-local - only the following file(s):
-** fonts/robotomono/LICENSE.txt
-** fonts/robotomono/METADATA.json
-** fonts/robotomono/RobotoMono-Bold.ttf
-** fonts/robotomono/RobotoMono-BoldItalic.ttf
-** fonts/robotomono/RobotoMono-Italic.ttf
-** fonts/robotomono/RobotoMono-Light.ttf
-** fonts/robotomono/RobotoMono-LightItalic.ttf
-** fonts/robotomono/RobotoMono-Medium.ttf
-** fonts/robotomono/RobotoMono-MediumItalic.ttf
-** fonts/robotomono/RobotoMono-Regular.ttf
-** fonts/robotomono/RobotoMono-Thin.ttf
-** fonts/robotomono/RobotoMono-ThinItalic.ttf
+* @polymer/decorators
+* @polymer/polymer
+* @webcomponents/shadycss
 
-[[font-roboto-local-fonts-robotomono_license]]
+[[Polymer-2017_license]]
 ----
+Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
 
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
 
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
 
-   1. Definitions.
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
 
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 ----
 
@@ -945,48 +767,227 @@
 ----
 
 
-[[Polymer-2017]]
-Polymer-2017
+[[font-roboto-local-fonts-robotomono]]
+font-roboto-local-fonts-robotomono
 
-* @polymer/polymer
-* @webcomponents/shadycss
+* @polymer/font-roboto-local - only the following file(s):
+** fonts/robotomono/LICENSE.txt
+** fonts/robotomono/METADATA.json
+** fonts/robotomono/RobotoMono-Bold.ttf
+** fonts/robotomono/RobotoMono-BoldItalic.ttf
+** fonts/robotomono/RobotoMono-Italic.ttf
+** fonts/robotomono/RobotoMono-Light.ttf
+** fonts/robotomono/RobotoMono-LightItalic.ttf
+** fonts/robotomono/RobotoMono-Medium.ttf
+** fonts/robotomono/RobotoMono-MediumItalic.ttf
+** fonts/robotomono/RobotoMono-Regular.ttf
+** fonts/robotomono/RobotoMono-Thin.ttf
+** fonts/robotomono/RobotoMono-ThinItalic.ttf
 
-[[Polymer-2017_license]]
+[[font-roboto-local-fonts-robotomono_license]]
 ----
-Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
 
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
 
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 
-   * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
-   * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
-   * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
+   1. Definitions.
 
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
 
 ----
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index d561596..98e99d4 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3268,227 +3268,49 @@
 ----
 
 
-[[font-roboto-local-fonts-robotomono]]
-font-roboto-local-fonts-robotomono
+[[Polymer-2017]]
+Polymer-2017
 
-* @polymer/font-roboto-local - only the following file(s):
-** fonts/robotomono/LICENSE.txt
-** fonts/robotomono/METADATA.json
-** fonts/robotomono/RobotoMono-Bold.ttf
-** fonts/robotomono/RobotoMono-BoldItalic.ttf
-** fonts/robotomono/RobotoMono-Italic.ttf
-** fonts/robotomono/RobotoMono-Light.ttf
-** fonts/robotomono/RobotoMono-LightItalic.ttf
-** fonts/robotomono/RobotoMono-Medium.ttf
-** fonts/robotomono/RobotoMono-MediumItalic.ttf
-** fonts/robotomono/RobotoMono-Regular.ttf
-** fonts/robotomono/RobotoMono-Thin.ttf
-** fonts/robotomono/RobotoMono-ThinItalic.ttf
+* @polymer/decorators
+* @polymer/polymer
+* @webcomponents/shadycss
 
-[[font-roboto-local-fonts-robotomono_license]]
+[[Polymer-2017_license]]
 ----
+Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
 
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
+This code may only be used under the BSD style license found at
+http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
+http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
+found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
+part of the polymer project is also subject to an additional IP rights grant
+found at http://polymer.github.io/PATENTS.txt
 
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
 
-   1. Definitions.
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
 
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 ----
 
@@ -3887,48 +3709,227 @@
 ----
 
 
-[[Polymer-2017]]
-Polymer-2017
+[[font-roboto-local-fonts-robotomono]]
+font-roboto-local-fonts-robotomono
 
-* @polymer/polymer
-* @webcomponents/shadycss
+* @polymer/font-roboto-local - only the following file(s):
+** fonts/robotomono/LICENSE.txt
+** fonts/robotomono/METADATA.json
+** fonts/robotomono/RobotoMono-Bold.ttf
+** fonts/robotomono/RobotoMono-BoldItalic.ttf
+** fonts/robotomono/RobotoMono-Italic.ttf
+** fonts/robotomono/RobotoMono-Light.ttf
+** fonts/robotomono/RobotoMono-LightItalic.ttf
+** fonts/robotomono/RobotoMono-Medium.ttf
+** fonts/robotomono/RobotoMono-MediumItalic.ttf
+** fonts/robotomono/RobotoMono-Regular.ttf
+** fonts/robotomono/RobotoMono-Thin.ttf
+** fonts/robotomono/RobotoMono-ThinItalic.ttf
 
-[[Polymer-2017_license]]
+[[font-roboto-local-fonts-robotomono_license]]
 ----
-Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
 
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
 
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 
-   * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
-   * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
-   * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
+   1. Definitions.
 
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
 
 ----
 
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index ebba3eb..bf1fcc6 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -197,16 +197,12 @@
     "jsdoc/require-param-description": 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-name
     "jsdoc/require-param-name": 2,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type
-    "jsdoc/require-param-type": 2,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns
     "jsdoc/require-returns": 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-check
     "jsdoc/require-returns-check": 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-description
     "jsdoc/require-returns-description": 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type
-    "jsdoc/require-returns-type": 2,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-valid-types
     "jsdoc/valid-types": 2,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-file-overview
@@ -253,6 +249,10 @@
       // .js-only rules
       "files": ["**/*.js"],
       "rules": {
+        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type
+        "jsdoc/require-param-type": 2,
+        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type
+        "jsdoc/require-returns-type": 2,
         // The rule is required for .js files only, because typescript compiler
         // always checks import.
         "import/no-unresolved": 2,
diff --git a/polygerrit-ui/app/constants/constants.js b/polygerrit-ui/app/constants/constants.js
deleted file mode 100644
index 3a4cb5e..0000000
--- a/polygerrit-ui/app/constants/constants.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-goog.declareModuleId('polygerrit.constants.constants');
-
-/**
- * @enum
- * @desc Tab names for primary tabs on change view page.
- */
-export const PrimaryTab = {
-  FILES: 'files',
-  /**
-   * When renaming this, the links in UrlFormatter must be updated.
-   */
-  COMMENT_THREADS: 'comments',
-  FINDINGS: 'findings',
-};
-
-/**
- * @enum
- * @desc Tab names for secondary tabs on change view page.
- */
-export const SecondaryTab = {
-  CHANGE_LOG: '_changeLog',
-};
-
-/**
- * @enum
- * @desc Tag names of change log messages.
- */
-export const MessageTag = {
-  TAG_DELETE_REVIEWER: 'autogenerated:gerrit:deleteReviewer',
-  TAG_NEW_PATCHSET: 'autogenerated:gerrit:newPatchSet',
-  TAG_NEW_WIP_PATCHSET: 'autogenerated:gerrit:newWipPatchSet',
-  TAG_REVIEWER_UPDATE: 'autogenerated:gerrit:reviewerUpdate',
-  TAG_SET_PRIVATE: 'autogenerated:gerrit:setPrivate',
-  TAG_UNSET_PRIVATE: 'autogenerated:gerrit:unsetPrivate',
-  TAG_SET_READY: 'autogenerated:gerrit:setReadyForReview',
-  TAG_SET_WIP: 'autogenerated:gerrit:setWorkInProgress',
-  TAG_SET_ASSIGNEE: 'autogenerated:gerrit:setAssignee',
-  TAG_UNSET_ASSIGNEE: 'autogenerated:gerrit:deleteAssignee',
-};
-
-/**
- * @enum
- * @desc Modes for gr-diff-cursor
- * The scroll behavior for the cursor. Values are 'never' and
- * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
- * the viewport.
- */
-export const ScrollMode = {
-  KEEP_VISIBLE: 'keep-visible',
-  NEVER: 'never',
-};
-
-/**
- * @enum
- * @desc Specifies status for a change
- */
-export const ChangeStatus = {
-  ABANDONED: 'ABANDONED',
-  MERGED: 'MERGED',
-  NEW: 'NEW',
-};
-
-/**
- * @enum
- * @desc Special file paths
- */
-export const SpecialFilePath = {
-  PATCHSET_LEVEL_COMMENTS: '/PATCHSET_LEVEL',
-  COMMIT_MESSAGE: '/COMMIT_MSG',
-  MERGE_LIST: '/MERGE_LIST',
-};
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
new file mode 100644
index 0000000..0b26a30
--- /dev/null
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -0,0 +1,138 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @desc Tab names for primary tabs on change view page.
+ */
+export enum PrimaryTab {
+  FILES = 'files',
+  /**
+   * When renaming this, the links in UrlFormatter must be updated.
+   */
+  COMMENT_THREADS = 'comments',
+  FINDINGS = 'findings',
+}
+
+/**
+ * @desc Tab names for secondary tabs on change view page.
+ */
+export enum SecondaryTab {
+  CHANGE_LOG = '_changeLog',
+}
+
+/**
+ * @desc Tag names of change log messages.
+ */
+export enum MessageTag {
+  TAG_DELETE_REVIEWER = 'autogenerated:gerrit:deleteReviewer',
+  TAG_NEW_PATCHSET = 'autogenerated:gerrit:newPatchSet',
+  TAG_NEW_WIP_PATCHSET = 'autogenerated:gerrit:newWipPatchSet',
+  TAG_REVIEWER_UPDATE = 'autogenerated:gerrit:reviewerUpdate',
+  TAG_SET_PRIVATE = 'autogenerated:gerrit:setPrivate',
+  TAG_UNSET_PRIVATE = 'autogenerated:gerrit:unsetPrivate',
+  TAG_SET_READY = 'autogenerated:gerrit:setReadyForReview',
+  TAG_SET_WIP = 'autogenerated:gerrit:setWorkInProgress',
+  TAG_SET_ASSIGNEE = 'autogenerated:gerrit:setAssignee',
+  TAG_UNSET_ASSIGNEE = 'autogenerated:gerrit:deleteAssignee',
+}
+
+/**
+ * @desc Modes for gr-diff-cursor
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ */
+export enum ScrollMode {
+  KEEP_VISIBLE = 'keep-visible',
+  NEVER = 'never',
+}
+
+/**
+ * @desc Specifies status for a change
+ */
+export enum ChangeStatus {
+  ABANDONED = 'ABANDONED',
+  MERGED = 'MERGED',
+  NEW = 'NEW',
+}
+
+/**
+ * @desc Special file paths
+ */
+export enum SpecialFilePath {
+  PATCHSET_LEVEL_COMMENTS = '/PATCHSET_LEVEL',
+  COMMIT_MESSAGE = '/COMMIT_MSG',
+  MERGE_LIST = '/MERGE_LIST',
+}
+
+/**
+ * @desc The reviewer state
+ */
+export enum RequirementStatus {
+  OK = 'OK',
+  NOT_READY = 'NOT_READY',
+  RULE_ERROR = 'RULE_ERROR',
+}
+
+/**
+ * @desc The reviewer state
+ */
+export enum ReviewerState {
+  REVIEWER = 'REVIEWER',
+  CC = 'CC',
+  REMOVED = 'REMOVED',
+}
+
+/**
+ * @desc The patchset kind
+ */
+export enum RevisionKind {
+  REWORK = 'REWORK',
+  TRIVIAL_REBASE = 'TRIVIAL_REBASE',
+  MERGE_FIRST_PARENT_UPDATE = 'MERGE_FIRST_PARENT_UPDATE',
+  NO_CODE_CHANGE = 'NO_CODE_CHANGE',
+  NO_CHANGE = 'NO_CHANGE',
+}
+
+/**
+ * @desc The status of fixing the problem
+ */
+export enum ProblemInfoStatus {
+  FIXED = 'FIXED',
+  FIX_FAILED = 'FIX_FAILED',
+}
+
+/**
+ * @desc The status of the file
+ */
+export enum FileInfoStatus {
+  ADDED = 'A',
+  DELETED = 'D',
+  RENAMED = 'R',
+  COPIED = 'C',
+  REWRITTEN = 'W',
+  // Modifed = 'M', but API not set it if the file was modified
+}
+
+/**
+ * @desc The status of the file
+ */
+export enum GpgKeyInfoStatus {
+  BAD = 'BAD',
+  OK = 'OK',
+  TRUSTED = 'TRUSTED',
+}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 15b1ebf..6ccca94 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -35,7 +35,7 @@
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {appContext} from '../../../services/app-context.js';
 import {SpecialFilePath} from '../../../constants/constants.js';
-import {ExperimentIds} from '../../../services/flags.js';
+import {KnownExperimentId} from '../../../services/flags/flags.js';
 import {fetchChangeUpdates} from '../../../utils/patch-set-util.js';
 import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {getDisplayName} from '../../../utils/display-name-util.js';
@@ -357,7 +357,7 @@
   ready() {
     super.ready();
     this._isPatchsetCommentsExperimentEnabled = this.flagsService
-        .isEnabled(ExperimentIds.PATCHSET_COMMENTS);
+        .isEnabled(KnownExperimentId.PATCHSET_COMMENTS);
     this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
   }
 
@@ -556,7 +556,7 @@
     }
 
     if (this._isAttentionSetEnabled(this.serverConfig)) {
-      reviewInput.ignore_default_attention_set_rules = true;
+      reviewInput.ignore_automatic_attention_set_rules = true;
       reviewInput.add_to_attention_set = [];
       for (const user of this._newAttentionSet) {
         if (!this._currentAttentionSet.has(user)) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 3a9cea7..6f24fb3 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -191,7 +191,7 @@
     flushAsynchronousOperations();
 
     stubSaveReview(review => {
-      assert.isTrue(review.ignore_default_attention_set_rules);
+      assert.isTrue(review.ignore_automatic_attention_set_rules);
       assert.deepEqual(review.add_to_attention_set, [{
         user: 314,
         reason: 'manually added in reply dialog',
diff --git a/polygerrit-ui/app/elements/gr-app-init.js b/polygerrit-ui/app/elements/gr-app-init.js
index ea10ce8..df2d58b 100644
--- a/polygerrit-ui/app/elements/gr-app-init.js
+++ b/polygerrit-ui/app/elements/gr-app-init.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 import {initAppContext} from '../services/app-context-init.js';
-import {initVisibilityReporter, initPerformanceReporter, initErrorReporter} from '../services/gr-reporting/gr-reporting.js';
+import {initVisibilityReporter, initPerformanceReporter, initErrorReporter} from '../services/gr-reporting/gr-reporting_impl.js';
 import {appContext} from '../services/app-context.js';
 
 if (!window.Polymer) {
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index 1141d6b..3315e50b 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -86,6 +86,10 @@
 
 const packages: PackageInfo[] = [
   {
+    name: "@polymer/decorators",
+    license: SharedLicenses.Polymer2017,
+  },
+  {
     name: "@polymer/font-roboto",
     license: SharedLicenses.Polymer2015,
   },
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 32560ff..d6e94fc 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -3,31 +3,32 @@
   "description": "Gerrit Code Review - Polygerrit dependencies",
   "browser": true,
   "dependencies": {
+    "@polymer/decorators": "^3.0.0",
     "@polymer/font-roboto-local": "^3.0.2",
     "@polymer/iron-a11y-keys-behavior": "^3.0.1",
-    "@polymer/iron-autogrow-textarea": "^3.0.1",
+    "@polymer/iron-autogrow-textarea": "^3.0.3",
     "@polymer/iron-dropdown": "^3.0.1",
-    "@polymer/iron-fit-behavior": "^3.0.1",
+    "@polymer/iron-fit-behavior": "^3.0.2",
     "@polymer/iron-icon": "^3.0.1",
     "@polymer/iron-iconset-svg": "^3.0.1",
     "@polymer/iron-input": "^3.0.1",
-    "@polymer/iron-overlay-behavior": "^3.0.2",
+    "@polymer/iron-overlay-behavior": "^3.0.3",
     "@polymer/iron-selector": "^3.0.1",
     "@polymer/paper-button": "^3.0.1",
     "@polymer/paper-dialog": "^3.0.1",
     "@polymer/paper-dialog-behavior": "^3.0.1",
     "@polymer/paper-dialog-scrollable": "^3.0.1",
-    "@polymer/paper-input": "^3.0.2",
+    "@polymer/paper-input": "^3.2.1",
     "@polymer/paper-item": "^3.0.1",
     "@polymer/paper-listbox": "^3.0.1",
     "@polymer/paper-tabs": "^3.1.0",
     "@polymer/paper-toggle-button": "^3.0.1",
-    "@polymer/polymer": "^3.3.0",
+    "@polymer/polymer": "^3.4.1",
     "@webcomponents/shadycss": "^1.9.2",
     "@webcomponents/webcomponentsjs": "^1.3.3",
+    "ba-linkify": "file:../../lib/ba-linkify/src/",
     "page": "^1.11.5",
     "polymer-bridges": "file:../../polymer-bridges/",
-    "ba-linkify": "file:../../lib/ba-linkify/src/",
     "polymer-resin": "^2.0.1"
   },
   "license": "Apache-2.0",
diff --git a/polygerrit-ui/app/services/app-context-init.js b/polygerrit-ui/app/services/app-context-init.js
deleted file mode 100644
index 531c361..0000000
--- a/polygerrit-ui/app/services/app-context-init.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {appContext} from './app-context.js';
-import {FlagsService} from './flags.js';
-import {GrReporting} from './gr-reporting/gr-reporting.js';
-import {EventEmitter} from './gr-event-interface/gr-event-interface.js';
-import {Auth} from './gr-auth.js';
-
-const initializedServices = new Map();
-
-function getService(serviceName, serviceInit) {
-  if (!initializedServices.has(serviceName)) {
-    initializedServices.set(serviceName, serviceInit());
-  }
-  return initializedServices.get(serviceName);
-}
-
-/**
- * The AppContext lazy initializator for all services
- */
-export function initAppContext() {
-  const registeredServices = {};
-  function addService(serviceName, serviceCreator) {
-    if (registeredServices[serviceName]) {
-      throw new Error(`Service ${serviceName} already registered.`);
-    }
-    registeredServices[serviceName] = {
-      get() {
-        return getService(serviceName, serviceCreator);
-      },
-    };
-  }
-
-  addService('flagsService', () => new FlagsService());
-  addService('reportingService',
-      () => new GrReporting(appContext.flagsService));
-  addService('eventEmitter', () => new EventEmitter());
-  addService('authService', () => new Auth(appContext.eventEmitter));
-  Object.defineProperties(appContext, registeredServices);
-}
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
new file mode 100644
index 0000000..b249d16
--- /dev/null
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {appContext, AppContext} from './app-context';
+import {FlagsServiceImplementation} from './flags/flags_impl';
+import {GrReporting} from './gr-reporting/gr-reporting_impl';
+import {EventEmitter} from './gr-event-interface/gr-event-interface_impl';
+import {Auth} from './gr-auth/gr-auth_impl';
+
+type ServiceName = keyof AppContext;
+type ServiceCreator<T> = () => T;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const initializedServices: Map<ServiceName, any> = new Map();
+
+function getService<K extends ServiceName>(
+  serviceName: K,
+  serviceCreator: ServiceCreator<AppContext[K]>
+): AppContext[K] {
+  if (!initializedServices.has(serviceName)) {
+    initializedServices.set(serviceName, serviceCreator());
+  }
+  return initializedServices.get(serviceName);
+}
+
+/**
+ * The AppContext lazy initializator for all services
+ */
+export function initAppContext() {
+  function populateAppContext(
+    serviceCreators: {[P in ServiceName]: ServiceCreator<AppContext[P]>}
+  ) {
+    const registeredServices = Object.keys(serviceCreators).reduce(
+      (registeredServices, key) => {
+        const serviceName = key as ServiceName;
+        const serviceCreator = serviceCreators[serviceName];
+        registeredServices[serviceName] = {
+          configurable: true, // Tests can mock properties
+          get() {
+            return getService(serviceName, serviceCreator);
+          },
+        };
+        return registeredServices;
+      },
+      {} as PropertyDescriptorMap
+    );
+    Object.defineProperties(appContext, registeredServices);
+  }
+
+  populateAppContext({
+    flagsService: () => new FlagsServiceImplementation(),
+    reportingService: () => new GrReporting(appContext.flagsService),
+    eventEmitter: () => new EventEmitter(),
+    authService: () => new Auth(appContext.eventEmitter),
+  });
+}
diff --git a/polygerrit-ui/app/services/app-context.js b/polygerrit-ui/app/services/app-context.ts
similarity index 60%
rename from polygerrit-ui/app/services/app-context.js
rename to polygerrit-ui/app/services/app-context.ts
index 3f86003..c08ee7a 100644
--- a/polygerrit-ui/app/services/app-context.js
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -14,16 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {FlagsService} from './flags/flags';
+import {EventEmitterService} from './gr-event-interface/gr-event-interface';
+import {ReportingService} from './gr-reporting/gr-reporting';
+import {AuthService} from './gr-auth/gr-auth';
+
+export interface AppContext {
+  flagsService: FlagsService;
+  reportingService: ReportingService;
+  eventEmitter: EventEmitterService;
+  authService: AuthService;
+}
 
 /**
  * The AppContext holds immortal singleton instances of services. It's a
  * convenient way to provide singletons that can be swapped out for testing.
  *
  * AppContext is initialized in ./app-context-init.js
+ *
+ * It is guaranteed that all fields in appContext are always initialized
+ * (except for shared gr-diff)
  */
-export const appContext = {
-  flagsService: null,
-  reportingService: null,
-  eventEmitter: null,
-  authService: null,
-};
\ No newline at end of file
+export const appContext: AppContext = {} as AppContext;
diff --git a/polygerrit-ui/app/constants/constants.d.ts b/polygerrit-ui/app/services/flags/flags.ts
similarity index 71%
rename from polygerrit-ui/app/constants/constants.d.ts
rename to polygerrit-ui/app/services/flags/flags.ts
index 036d6ea..aae8ac7 100644
--- a/polygerrit-ui/app/constants/constants.d.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -15,9 +15,14 @@
  * limitations under the License.
  */
 
-export type ChangeStatus = any;
-export namespace ChangeStatus {
-  export const ABANDONED: string;
-  export const MERGED: string;
-  export const NEW: string;
+export interface FlagsService {
+  isEnabled(experimentId: string): boolean;
+  enabledExperiments: string[];
+}
+
+/**
+ * @desc Experiment ids used in Gerrit.
+ */
+export enum KnownExperimentId {
+  PATCHSET_COMMENTS = 'UiFeature__patchset_comments',
 }
diff --git a/polygerrit-ui/app/services/flags.js b/polygerrit-ui/app/services/flags/flags_impl.ts
similarity index 71%
rename from polygerrit-ui/app/services/flags.js
rename to polygerrit-ui/app/services/flags/flags_impl.ts
index 6313255..835eb56 100644
--- a/polygerrit-ui/app/services/flags.js
+++ b/polygerrit-ui/app/services/flags/flags_impl.ts
@@ -14,37 +14,37 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {FlagsService} from './flags';
 
-/**
- * @enum
- * @desc Experiment ids used in Gerrit.
- */
-export const ExperimentIds = {
-  PATCHSET_COMMENTS: 'UiFeature__patchset_comments',
-};
+declare global {
+  interface Window {
+    ENABLED_EXPERIMENTS: string[];
+  }
+}
 
 /**
  * Flags service.
  *
  * Provides all related methods / properties regarding on feature flags.
  */
-export class FlagsService {
+export class FlagsServiceImplementation implements FlagsService {
+  private readonly _experiments: Set<string>;
+
   constructor() {
     // stores all enabled experiments
-    this._experiments = new Set();
-    this._loadExperiments();
+    this._experiments = this._loadExperiments();
   }
 
   /**
    * @param {string} experimentId
    * @returns {boolean}
    */
-  isEnabled(experimentId) {
+  isEnabled(experimentId: string): boolean {
     return this._experiments.has(experimentId);
   }
 
-  _loadExperiments() {
-    this._experiments = new Set(window.ENABLED_EXPERIMENTS);
+  _loadExperiments(): Set<string> {
+    return new Set(window.ENABLED_EXPERIMENTS);
   }
 
   /**
diff --git a/polygerrit-ui/app/services/flags_test.js b/polygerrit-ui/app/services/flags/flags_test.js
similarity index 88%
rename from polygerrit-ui/app/services/flags_test.js
rename to polygerrit-ui/app/services/flags/flags_test.js
index ae1033e..33508af 100644
--- a/polygerrit-ui/app/services/flags_test.js
+++ b/polygerrit-ui/app/services/flags/flags_test.js
@@ -15,8 +15,8 @@
  * limitations under the License.
  */
 
-import '../test/common-test-setup-karma.js';
-import {FlagsService} from './flags.js';
+import '../../test/common-test-setup-karma.js';
+import {FlagsServiceImplementation} from './flags_impl.js';
 
 suite('flags tests', () => {
   let originalEnabledExperiments;
@@ -25,7 +25,7 @@
   suiteSetup(() => {
     originalEnabledExperiments = window.ENABLED_EXPERIMENTS;
     window.ENABLED_EXPERIMENTS = ['a', 'a'];
-    flags = new FlagsService();
+    flags = new FlagsServiceImplementation();
   });
 
   suiteTeardown(() => {
diff --git a/polygerrit-ui/app/services/gr-auth.js b/polygerrit-ui/app/services/gr-auth.js
deleted file mode 100644
index 21081cc..0000000
--- a/polygerrit-ui/app/services/gr-auth.js
+++ /dev/null
@@ -1,265 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {getBaseUrl} from '../utils/url-util.js';
-
-const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
-const MAX_GET_TOKEN_RETRIES = 2;
-
-/**
- * Auth class.
- */
-export class Auth {
-  constructor(eventEmitter) {
-    this._type = null;
-    this._cachedTokenPromise = null;
-    this._defaultOptions = {};
-    this._retriesLeft = MAX_GET_TOKEN_RETRIES;
-    this._status = Auth.STATUS.UNDETERMINED;
-    this._authCheckPromise = null;
-    this._last_auth_check_time = Date.now();
-    this.eventEmitter = eventEmitter;
-  }
-
-  get baseUrl() {
-    return getBaseUrl();
-  }
-
-  /**
-   * Returns if user is authed or not.
-   *
-   * @returns {!Promise<boolean>}
-   */
-  authCheck() {
-    if (!this._authCheckPromise ||
-      (Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS)
-    ) {
-      // Refetch after last check expired
-      this._authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
-      this._last_auth_check_time = Date.now();
-    }
-
-    return this._authCheckPromise.then(res => {
-      // auth-check will return 204 if authed
-      // treat the rest as unauthed
-      if (res.status === 204) {
-        this._setStatus(Auth.STATUS.AUTHED);
-        return true;
-      } else {
-        this._setStatus(Auth.STATUS.NOT_AUTHED);
-        return false;
-      }
-    }).catch(e => {
-      this._setStatus(Auth.STATUS.ERROR);
-      // Reset _authCheckPromise to avoid caching the failed promise
-      this._authCheckPromise = null;
-      return false;
-    });
-  }
-
-  clearCache() {
-    this._authCheckPromise = null;
-  }
-
-  /**
-   * @param {Auth.STATUS} status
-   */
-  _setStatus(status) {
-    if (this._status === status) return;
-
-    if (this._status === Auth.STATUS.AUTHED) {
-      this.eventEmitter.emit('auth-error', {
-        message: Auth.CREDS_EXPIRED_MSG, action: 'Refresh credentials',
-      });
-    }
-    this._status = status;
-  }
-
-  get status() {
-    return this._status;
-  }
-
-  get isAuthed() {
-    return this._status === Auth.STATUS.AUTHED;
-  }
-
-  _getToken() {
-    return Promise.resolve(this._cachedTokenPromise);
-  }
-
-  /**
-   * Enable cross-domain authentication using OAuth access token.
-   *
-   * @param {
-   *   function(): !Promise<{
-   *     access_token: string,
-   *     expires_at: number
-   *   }>
-   * } getToken
-   * @param {?{credentials:string}} defaultOptions
-   */
-  setup(getToken, defaultOptions) {
-    this._retriesLeft = MAX_GET_TOKEN_RETRIES;
-    if (getToken) {
-      this._type = Auth.TYPE.ACCESS_TOKEN;
-      this._cachedTokenPromise = null;
-      this._getToken = getToken;
-    }
-    this._defaultOptions = {};
-    if (defaultOptions) {
-      for (const p of ['credentials']) {
-        this._defaultOptions[p] = defaultOptions[p];
-      }
-    }
-  }
-
-  /**
-   * Perform network fetch with authentication.
-   *
-   * @param {string} url
-   * @param {Object=} opt_options
-   * @return {!Promise<!Response>}
-   */
-  fetch(url, opt_options) {
-    const options = {
-      headers: new Headers(),
-      ...this._defaultOptions,
-      ...opt_options,
-    };
-    if (this._type === Auth.TYPE.ACCESS_TOKEN) {
-      return this._getAccessToken().then(
-          accessToken =>
-            this._fetchWithAccessToken(url, options, accessToken)
-      );
-    } else {
-      return this._fetchWithXsrfToken(url, options);
-    }
-  }
-
-  _getCookie(name) {
-    const key = name + '=';
-    let result = '';
-    document.cookie.split(';').some(c => {
-      c = c.trim();
-      if (c.startsWith(key)) {
-        result = c.substring(key.length);
-        return true;
-      }
-      return false;
-    });
-    return result;
-  }
-
-  _isTokenValid(token) {
-    if (!token) { return false; }
-    if (!token.access_token || !token.expires_at) { return false; }
-
-    const expiration = new Date(parseInt(token.expires_at, 10) * 1000);
-    if (Date.now() >= expiration.getTime()) { return false; }
-
-    return true;
-  }
-
-  _fetchWithXsrfToken(url, options) {
-    if (options.method && options.method !== 'GET') {
-      const token = this._getCookie('XSRF_TOKEN');
-      if (token) {
-        options.headers.append('X-Gerrit-Auth', token);
-      }
-    }
-    options.credentials = 'same-origin';
-    return fetch(url, options);
-  }
-
-  /**
-   * @return {!Promise<string>}
-   */
-  _getAccessToken() {
-    if (!this._cachedTokenPromise) {
-      this._cachedTokenPromise = this._getToken();
-    }
-    return this._cachedTokenPromise.then(token => {
-      if (this._isTokenValid(token)) {
-        this._retriesLeft = MAX_GET_TOKEN_RETRIES;
-        return token.access_token;
-      }
-      if (this._retriesLeft > 0) {
-        this._retriesLeft--;
-        this._cachedTokenPromise = null;
-        return this._getAccessToken();
-      }
-      // Fall back to anonymous access.
-      return null;
-    });
-  }
-
-  _fetchWithAccessToken(url, options, accessToken) {
-    const params = [];
-
-    if (accessToken) {
-      params.push(`access_token=${accessToken}`);
-      const baseUrl = this.baseUrl;
-      const pathname = baseUrl ?
-        url.substring(url.indexOf(baseUrl) + baseUrl.length) : url;
-      if (!pathname.startsWith('/a/')) {
-        url = url.replace(pathname, '/a' + pathname);
-      }
-    }
-
-    const method = options.method || 'GET';
-    let contentType = options.headers.get('Content-Type');
-
-    // For all requests with body, ensure json content type.
-    if (!contentType && options.body) {
-      contentType = 'application/json';
-    }
-
-    if (method !== 'GET') {
-      options.method = 'POST';
-      params.push(`$m=${method}`);
-      // If a request is not GET, and does not have a body, ensure text/plain
-      // content type.
-      if (!contentType) {
-        contentType = 'text/plain';
-      }
-    }
-
-    if (contentType) {
-      options.headers.set('Content-Type', 'text/plain');
-      params.push(`$ct=${encodeURIComponent(contentType)}`);
-    }
-
-    if (params.length) {
-      url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
-    }
-    return fetch(url, options);
-  }
-}
-
-Auth.TYPE = {
-  XSRF_TOKEN: 'xsrf_token',
-  ACCESS_TOKEN: 'access_token',
-};
-
-/** @enum {number} */
-Auth.STATUS = {
-  UNDETERMINED: 0,
-  AUTHED: 1,
-  NOT_AUTHED: 2,
-  ERROR: 3,
-};
-
-Auth.CREDS_EXPIRED_MSG = 'Credentials expired.';
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth.ts b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
new file mode 100644
index 0000000..f7fdadf
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export enum AuthType {
+  XSRF_TOKEN = 'xsrf_token',
+  ACCESS_TOKEN = 'access_token',
+}
+
+export enum AuthStatus {
+  UNDETERMINED = 0,
+  AUTHED = 1,
+  NOT_AUTHED = 2,
+  ERROR = 3,
+}
+
+export interface Token {
+  access_token?: string;
+  expires_at?: string;
+}
+
+export type GetTokenCallback = () => Promise<Token | null>;
+
+export interface DefaultAuthOptions {
+  credentials: RequestCredentials;
+}
+
+export interface AuthRequestInit extends RequestInit {
+  // RequestInit define headers as HeadersInit, i.e.
+  // Headers | string[][] | Record<string, string>
+  // Auth class supports only Headers in options
+  headers?: Headers;
+}
+
+export interface AuthService {
+  baseUrl: string;
+  isAuthed: boolean;
+
+  /**
+   * Returns if user is authed or not.
+   */
+  authCheck(): Promise<boolean>;
+
+  clearCache(): void;
+
+  /**
+   * Enable cross-domain authentication using OAuth access token.
+   */
+  setup(getToken: GetTokenCallback, defaultOptions: DefaultAuthOptions): void;
+
+  /**
+   * Perform network fetch with authentication.
+   */
+  fetch(url: string, opt_options?: AuthRequestInit): Promise<Response>;
+}
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
new file mode 100644
index 0000000..b5330e7
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -0,0 +1,291 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {getBaseUrl} from '../../utils/url-util';
+import {EventEmitterService} from '../gr-event-interface/gr-event-interface';
+import {
+  AuthRequestInit,
+  AuthService,
+  AuthStatus,
+  AuthType,
+  DefaultAuthOptions,
+  GetTokenCallback,
+  Token,
+} from './gr-auth';
+
+const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
+const MAX_GET_TOKEN_RETRIES = 2;
+
+interface ValidToken extends Token {
+  access_token: string;
+  expires_at: string;
+}
+
+interface AuthRequestInitWithHeaders extends AuthRequestInit {
+  // RequestInit define headers as optional property with a type
+  // Headers | string[][] | Record<string, string>
+  // In Auth class headers property is always set and has type Headers
+  headers: Headers;
+}
+
+/**
+ * Auth class.
+ */
+export class Auth implements AuthService {
+  // TODO(dmfilippov): Remove Type and Status properties, expose AuthType and
+  // AuthStatus to API
+  static TYPE = {
+    XSRF_TOKEN: AuthType.XSRF_TOKEN,
+    ACCESS_TOKEN: AuthType.ACCESS_TOKEN,
+  };
+
+  static STATUS = {
+    UNDETERMINED: AuthStatus.UNDETERMINED,
+    AUTHED: AuthStatus.AUTHED,
+    NOT_AUTHED: AuthStatus.NOT_AUTHED,
+    ERROR: AuthStatus.ERROR,
+  };
+
+  static CREDS_EXPIRED_MSG = 'Credentials expired.';
+
+  private _authCheckPromise?: Promise<Response>;
+
+  private _last_auth_check_time: number = Date.now();
+
+  private _status = AuthStatus.UNDETERMINED;
+
+  private _retriesLeft = MAX_GET_TOKEN_RETRIES;
+
+  private _cachedTokenPromise: Promise<Token | null> | null = null;
+
+  private _type?: AuthType;
+
+  private _defaultOptions: AuthRequestInit = {};
+
+  private _getToken: GetTokenCallback;
+
+  public eventEmitter: EventEmitterService;
+
+  constructor(eventEmitter: EventEmitterService) {
+    this._getToken = () => Promise.resolve(this._cachedTokenPromise);
+    this.eventEmitter = eventEmitter;
+  }
+
+  get baseUrl() {
+    return getBaseUrl();
+  }
+
+  /**
+   * Returns if user is authed or not.
+   */
+  authCheck(): Promise<boolean> {
+    if (
+      !this._authCheckPromise ||
+      Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS
+    ) {
+      // Refetch after last check expired
+      this._authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
+      this._last_auth_check_time = Date.now();
+    }
+
+    return this._authCheckPromise
+      .then(res => {
+        // auth-check will return 204 if authed
+        // treat the rest as unauthed
+        if (res.status === 204) {
+          this._setStatus(Auth.STATUS.AUTHED);
+          return true;
+        } else {
+          this._setStatus(Auth.STATUS.NOT_AUTHED);
+          return false;
+        }
+      })
+      .catch(() => {
+        this._setStatus(AuthStatus.ERROR);
+        // Reset _authCheckPromise to avoid caching the failed promise
+        this._authCheckPromise = undefined;
+        return false;
+      });
+  }
+
+  clearCache() {
+    this._authCheckPromise = undefined;
+  }
+
+  private _setStatus(status: AuthStatus) {
+    if (this._status === status) return;
+
+    if (this._status === AuthStatus.AUTHED) {
+      this.eventEmitter.emit('auth-error', {
+        message: Auth.CREDS_EXPIRED_MSG,
+        action: 'Refresh credentials',
+      });
+    }
+    this._status = status;
+  }
+
+  get status() {
+    return this._status;
+  }
+
+  get isAuthed() {
+    return this._status === Auth.STATUS.AUTHED;
+  }
+
+  /**
+   * Enable cross-domain authentication using OAuth access token.
+   */
+  setup(getToken: GetTokenCallback, defaultOptions: DefaultAuthOptions) {
+    this._retriesLeft = MAX_GET_TOKEN_RETRIES;
+    if (getToken) {
+      this._type = AuthType.ACCESS_TOKEN;
+      this._cachedTokenPromise = null;
+      this._getToken = getToken;
+    }
+    this._defaultOptions = {};
+    if (defaultOptions) {
+      this._defaultOptions.credentials = defaultOptions.credentials;
+    }
+  }
+
+  /**
+   * Perform network fetch with authentication.
+   */
+  fetch(url: string, opt_options?: AuthRequestInit): Promise<Response> {
+    const options: AuthRequestInitWithHeaders = {
+      headers: new Headers(),
+      ...this._defaultOptions,
+      ...opt_options,
+    };
+    if (this._type === AuthType.ACCESS_TOKEN) {
+      return this._getAccessToken().then(accessToken =>
+        this._fetchWithAccessToken(url, options, accessToken)
+      );
+    } else {
+      return this._fetchWithXsrfToken(url, options);
+    }
+  }
+
+  private _getCookie(name: string): string {
+    const key = name + '=';
+    let result = '';
+    document.cookie.split(';').some(c => {
+      c = c.trim();
+      if (c.startsWith(key)) {
+        result = c.substring(key.length);
+        return true;
+      }
+      return false;
+    });
+    return result;
+  }
+
+  private _isTokenValid(token: Token | null): token is ValidToken {
+    if (!token) {
+      return false;
+    }
+    if (!token.access_token || !token.expires_at) {
+      return false;
+    }
+
+    const expiration = new Date(parseInt(token.expires_at, 10) * 1000);
+    if (Date.now() >= expiration.getTime()) {
+      return false;
+    }
+
+    return true;
+  }
+
+  private _fetchWithXsrfToken(
+    url: string,
+    options: AuthRequestInitWithHeaders
+  ): Promise<Response> {
+    if (options.method && options.method !== 'GET') {
+      const token = this._getCookie('XSRF_TOKEN');
+      if (token) {
+        options.headers.append('X-Gerrit-Auth', token);
+      }
+    }
+    options.credentials = 'same-origin';
+    return fetch(url, options);
+  }
+
+  private _getAccessToken(): Promise<string | null> {
+    if (!this._cachedTokenPromise) {
+      this._cachedTokenPromise = this._getToken();
+    }
+    return this._cachedTokenPromise.then(token => {
+      if (this._isTokenValid(token)) {
+        this._retriesLeft = MAX_GET_TOKEN_RETRIES;
+        return token.access_token;
+      }
+      if (this._retriesLeft > 0) {
+        this._retriesLeft--;
+        this._cachedTokenPromise = null;
+        return this._getAccessToken();
+      }
+      // Fall back to anonymous access.
+      return null;
+    });
+  }
+
+  private _fetchWithAccessToken(
+    url: string,
+    options: AuthRequestInitWithHeaders,
+    accessToken: string | null
+  ): Promise<Response> {
+    const params = [];
+
+    if (accessToken) {
+      params.push(`access_token=${accessToken}`);
+      const baseUrl = this.baseUrl;
+      const pathname = baseUrl
+        ? url.substring(url.indexOf(baseUrl) + baseUrl.length)
+        : url;
+      if (!pathname.startsWith('/a/')) {
+        url = url.replace(pathname, '/a' + pathname);
+      }
+    }
+
+    const method = options.method || 'GET';
+    let contentType = options.headers.get('Content-Type');
+
+    // For all requests with body, ensure json content type.
+    if (!contentType && options.body) {
+      contentType = 'application/json';
+    }
+
+    if (method !== 'GET') {
+      options.method = 'POST';
+      params.push(`$m=${method}`);
+      // If a request is not GET, and does not have a body, ensure text/plain
+      // content type.
+      if (!contentType) {
+        contentType = 'text/plain';
+      }
+    }
+
+    if (contentType) {
+      options.headers.set('Content-Type', 'text/plain');
+      params.push(`$ct=${encodeURIComponent(contentType)}`);
+    }
+
+    if (params.length) {
+      url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
+    }
+    return fetch(url, options);
+  }
+}
diff --git a/polygerrit-ui/app/services/gr-auth_test.js b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
similarity index 98%
rename from polygerrit-ui/app/services/gr-auth_test.js
rename to polygerrit-ui/app/services/gr-auth/gr-auth_test.js
index 541cd42..432518c 100644
--- a/polygerrit-ui/app/services/gr-auth_test.js
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
@@ -15,10 +15,10 @@
  * limitations under the License.
  */
 
-import '../test/common-test-setup-karma.js';
-import {Auth} from './gr-auth.js';
-import {appContext} from './app-context.js';
-import {stubBaseUrl} from '../test/test-utils.js';
+import '../../test/common-test-setup-karma.js';
+import {Auth} from './gr-auth_impl.js';
+import {appContext} from '../app-context.js';
+import {stubBaseUrl} from '../../test/test-utils.js';
 
 suite('gr-auth', () => {
   let auth;
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts
new file mode 100644
index 0000000..d59a022
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts
@@ -0,0 +1,67 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type EventCallback = (...args: any) => void;
+export type UnsubscribeMethod = () => void;
+
+export interface EventEmitterService {
+  /**
+   * Register an event listener to an event.
+   */
+  addListener(eventName: string, cb: EventCallback): UnsubscribeMethod;
+
+  /**
+   * Alias for addListener.
+   */
+  on(eventName: string, cb: EventCallback): UnsubscribeMethod;
+
+  /**
+   * Attach event handler only once. Automatically removed.
+   */
+  once(eventName: string, cb: EventCallback): UnsubscribeMethod;
+
+  /**
+   * De-register an event listener to an event.
+   */
+  removeListener(eventName: string, cb: EventCallback): void;
+
+  /**
+   * Alias to removeListener
+   */
+  off(eventName: string, cb: EventCallback): void;
+
+  /**
+   * Synchronously calls each of the listeners registered for
+   * the event named eventName, in the order they were registered,
+   * passing the supplied detail to each.
+   *
+   * @returns true if the event had listeners, false otherwise.
+   */
+  emit(eventName: string, detail: any): boolean;
+
+  /**
+   * Alias to emit.
+   */
+  dispatch(eventName: string, detail: any): boolean;
+
+  /**
+   * Remove listeners for a specific event or all.
+   *
+   * @param eventName if not provided, will remove all
+   */
+  removeAllListeners(eventName: string): void;
+}
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.js b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_impl.ts
similarity index 69%
rename from polygerrit-ui/app/services/gr-event-interface/gr-event-interface.js
rename to polygerrit-ui/app/services/gr-event-interface/gr-event-interface_impl.ts
index 7705874..72afbda 100644
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.js
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_impl.ts
@@ -15,6 +15,11 @@
  * limitations under the License.
  */
 
+import {
+  EventCallback,
+  EventEmitterService,
+  UnsubscribeMethod,
+} from './gr-event-interface';
 /**
  * An lite implementation of
  * https://nodejs.org/api/events.html#events_class_eventemitter.
@@ -29,27 +34,16 @@
  * }
  *
  */
-export class EventEmitter {
-  constructor() {
-    /**
-     * Shared events map from name to the listeners.
-     *
-     * @type {!Object<string, Array<eventCallback>>}
-     */
-    this._listenersMap = new Map();
-  }
+export class EventEmitter implements EventEmitterService {
+  private _listenersMap = new Map<string, EventCallback[]>();
 
   /**
    * Register an event listener to an event.
-   *
-   * @param {string} eventName
-   * @param {eventCallback} cb
-   * @returns {Function} Unsubscribe method
    */
-  addListener(eventName, cb) {
+  addListener(eventName: string, cb: EventCallback): UnsubscribeMethod {
     if (!eventName || !cb) {
       console.warn('A valid eventname and callback is required!');
-      return;
+      return () => {};
     }
 
     const listeners = this._listenersMap.get(eventName) || [];
@@ -61,14 +55,18 @@
     };
   }
 
-  // Alias for addListener.
-  on(eventName, cb) {
+  /**
+   * Alias for addListener.
+   */
+  on(eventName: string, cb: EventCallback): UnsubscribeMethod {
     return this.addListener(eventName, cb);
   }
 
-  // Attach event handler only once. Automatically removed.
-  once(eventName, cb) {
-    const onceWrapper = (...args) => {
+  /**
+   * Attach event handler only once. Automatically removed.
+   */
+  once(eventName: string, cb: EventCallback): UnsubscribeMethod {
+    const onceWrapper = (...args: any[]) => {
       cb(...args);
       this.off(eventName, onceWrapper);
     };
@@ -77,18 +75,17 @@
 
   /**
    * De-register an event listener to an event.
-   *
-   * @param {string} eventName
-   * @param {eventCallback} cb
    */
-  removeListener(eventName, cb) {
+  removeListener(eventName: string, cb: EventCallback): void {
     let listeners = this._listenersMap.get(eventName) || [];
     listeners = listeners.filter(listener => listener !== cb);
     this._listenersMap.set(eventName, listeners);
   }
 
-  // Alias to removeListener
-  off(eventName, cb) {
+  /**
+   * Alias to removeListener
+   */
+  off(eventName: string, cb: EventCallback): void {
     this.removeListener(eventName, cb);
   }
 
@@ -97,12 +94,9 @@
    * the event named eventName, in the order they were registered,
    * passing the supplied detail to each.
    *
-   * Returns true if the event had listeners, false otherwise.
-   *
-   * @param {string} eventName
-   * @param {*} detail
+   * @returns true if the event had listeners, false otherwise.
    */
-  emit(eventName, detail) {
+  emit(eventName: string, detail: any): boolean {
     const listeners = this._listenersMap.get(eventName) || [];
     for (const listener of listeners) {
       try {
@@ -114,17 +108,19 @@
     return listeners.length !== 0;
   }
 
-  // Alias to emit.
-  dispatch(eventName, detail) {
+  /**
+   * Alias to emit.
+   */
+  dispatch(eventName: string, detail: any): boolean {
     return this.emit(eventName, detail);
   }
 
   /**
    * Remove listeners for a specific event or all.
    *
-   * @param {string} eventName if not provided, will remove all
+   * @param eventName if not provided, will remove all
    */
-  removeAllListeners(eventName) {
+  removeAllListeners(eventName: string): void {
     if (eventName) {
       this._listenersMap.set(eventName, []);
     } else {
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
index 1cdd6e3..6d0ab7b 100644
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
@@ -17,7 +17,7 @@
 
 import '../../test/common-test-setup-karma.js';
 import '../../elements/shared/gr-js-api-interface/gr-js-api-interface.js';
-import {EventEmitter} from './gr-event-interface.js';
+import {EventEmitter} from './gr-event-interface_impl.js';
 import {_testOnly_initGerritPluginApi} from '../../elements/shared/gr-js-api-interface/gr-gerrit.js';
 
 const basicFixture = fixtureFromElement('gr-js-api-interface');
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
deleted file mode 100644
index ea69d5f..0000000
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
+++ /dev/null
@@ -1,654 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Latency reporting constants.
-
-const TIMING = {
-  TYPE: 'timing-report',
-  CATEGORY: {
-    UI_LATENCY: 'UI Latency',
-    RPC: 'RPC Timing',
-  },
-  EVENT: {
-    APP_STARTED: 'App Started',
-  },
-};
-
-const LIFECYCLE = {
-  TYPE: 'lifecycle',
-  CATEGORY: {
-    DEFAULT: 'Default',
-    EXTENSION_DETECTED: 'Extension detected',
-    PLUGINS_INSTALLED: 'Plugins installed',
-  },
-};
-
-const INTERACTION = {
-  TYPE: 'interaction',
-  CATEGORY: {
-    DEFAULT: 'Default',
-    VISIBILITY: 'Visibility',
-  },
-};
-
-const NAVIGATION = {
-  TYPE: 'nav-report',
-  CATEGORY: {
-    LOCATION_CHANGED: 'Location Changed',
-  },
-  EVENT: {
-    PAGE: 'Page',
-  },
-};
-
-const ERROR = {
-  TYPE: 'error',
-  CATEGORY: {
-    EXCEPTION: 'exception',
-    ERROR_DIALOG: 'Error Dialog',
-  },
-};
-
-const TIMER = {
-  CHANGE_DISPLAYED: 'ChangeDisplayed',
-  CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
-  DASHBOARD_DISPLAYED: 'DashboardDisplayed',
-  DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent',
-  DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
-  DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded',
-  FILE_LIST_DISPLAYED: 'FileListDisplayed',
-  PLUGINS_LOADED: 'PluginsLoaded',
-  STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
-  STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
-  STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
-  STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent',
-  STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
-  STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded',
-  STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
-  WEB_COMPONENTS_READY: 'WebComponentsReady',
-  METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded',
-};
-
-const STARTUP_TIMERS = {};
-STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0;
-STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0;
-STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMING.EVENT.APP_STARTED] = 0;
-// WebComponentsReady timer is triggered from gr-router.
-STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
-
-const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
-const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
-const SLOW_RPC_THRESHOLD = 500;
-
-export function initErrorReporter(appContext) {
-  const reportingService = appContext.reportingService;
-  const onError = function(oldOnError, msg, url, line, column, error) {
-    if (oldOnError) {
-      oldOnError(msg, url, line, column, error);
-    }
-    if (error) {
-      line = line || error.lineNumber;
-      column = column || error.columnNumber;
-      let shortenedErrorStack = msg;
-      if (error.stack) {
-        const errorStackLines = error.stack.split('\n');
-        shortenedErrorStack = errorStackLines.slice(0,
-            Math.min(3, errorStackLines.length)).join('\n');
-      }
-      msg = shortenedErrorStack || error.toString();
-    }
-    const payload = {
-      url,
-      line,
-      column,
-      error,
-    };
-    reportingService.reporter(ERROR.TYPE, ERROR.CATEGORY.EXCEPTION,
-        msg, payload);
-    return true;
-  };
-
-  const catchErrors = function(opt_context) {
-    const context = opt_context || window;
-    context.onerror = onError.bind(null, context.onerror);
-    context.addEventListener('unhandledrejection', e => {
-      const msg = e.reason.message;
-      const payload = {
-        error: e.reason,
-      };
-      reportingService.reporter(ERROR.TYPE,
-          ERROR.CATEGORY.EXCEPTION, msg, payload);
-    });
-  };
-
-  catchErrors();
-
-  // for testing
-  return {catchErrors};
-}
-
-export function initPerformanceReporter(appContext) {
-  const reportingService = appContext.reportingService;
-  // PerformanceObserver interface is a browser API.
-  if (window.PerformanceObserver) {
-    const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
-    // Safari doesn't support longtask yet
-    if (supportedEntryTypes.includes('longtask')) {
-      const catchLongJsTasks = new PerformanceObserver(list => {
-        for (const task of list.getEntries()) {
-          // We are interested in longtask longer than 200 ms (default is 50 ms)
-          if (task.duration > 200) {
-            reportingService.reporter(TIMING.TYPE,
-                TIMING.CATEGORY.UI_LATENCY, `Task ${task.name}`,
-                Math.round(task.duration), {}, false);
-          }
-        }
-      });
-      catchLongJsTasks.observe({entryTypes: ['longtask']});
-    }
-  }
-}
-
-export function initVisibilityReporter(appContext) {
-  const reportingService = appContext.reportingService;
-  document.addEventListener('visibilitychange', () => {
-    reportingService.onVisibilityChange();
-  });
-}
-
-// Calculates the time of Gerrit being in a background tab. When Gerrit reports
-// a pageLoad metric it’s attached to its details for latency analysis.
-// It resets on locationChange.
-class HiddenDurationTimer {
-  constructor() {
-    this.reset();
-  }
-
-  reset() {
-    this.accHiddenDurationMs = 0;
-    this.lastVisibleTimestampMs = 0;
-  }
-
-  onVisibilityChange() {
-    if (document.visibilityState === 'hidden') {
-      this.lastVisibleTimestampMs = now();
-    } else if (document.visibilityState === 'visible') {
-      if (this.lastVisibleTimestampMs !== null) {
-        this.accHiddenDurationMs += now() - this.lastVisibleTimestampMs;
-        // Set to null for guarding against two 'visible' events in a row.
-        this.lastVisibleTimestampMs = null;
-      }
-    }
-  }
-
-  get hiddenDurationMs() {
-    if (document.visibilityState === 'hidden'
-      && this.lastVisibleTimestampMs !== null) {
-      return this.accHiddenDurationMs + now() - this.lastVisibleTimestampMs;
-    }
-    return this.accHiddenDurationMs;
-  }
-}
-
-export function now() {
-  return Math.round(window.performance.now());
-}
-
-export class GrReporting {
-  constructor(flagsService) {
-    this._flagsService = flagsService;
-    this._baselines = STARTUP_TIMERS;
-    this._timers = {
-      timeBetweenDraftActions: null,
-    };
-    this._reportRepoName = undefined;
-    this._pending = [];
-    this._slowRpcList = [];
-    this.hiddenDurationTimer = new HiddenDurationTimer();
-  }
-
-  get performanceTiming() {
-    return window.performance.timing;
-  }
-
-  get slowRpcSnapshot() {
-    return (this._slowRpcList || []).slice();
-  }
-
-  _arePluginsLoaded() {
-    return this._baselines &&
-      !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
-  }
-
-  _isMetricsPluginLoaded() {
-    return this._arePluginsLoaded() || this._baselines &&
-      !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED);
-  }
-
-  /**
-   * Reporter reports events. Events will be queued if metrics plugin is not
-   * yet installed.
-   *
-   * @param {string} type
-   * @param {string} category
-   * @param {string} eventName
-   * @param {string|number} eventValue
-   * @param {Object} eventDetails
-   * @param {boolean|undefined} opt_noLog If true, the event will not be
-   *     logged to the JS console.
-   */
-  reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) {
-    const eventInfo = this._createEventInfo(type, category,
-        eventName, eventValue, eventDetails);
-    if (type === ERROR.TYPE && category === ERROR.CATEGORY.EXCEPTION) {
-      console.error(eventValue && eventValue.error || eventName);
-    }
-
-    // We report events immediately when metrics plugin is loaded
-    if (this._isMetricsPluginLoaded() && !this._pending.length) {
-      this._reportEvent(eventInfo, opt_noLog);
-    } else {
-      // We cache until metrics plugin is loaded
-      this._pending.push([eventInfo, opt_noLog]);
-      if (this._isMetricsPluginLoaded()) {
-        this._pending.forEach(([eventInfo, opt_noLog]) => {
-          this._reportEvent(eventInfo, opt_noLog);
-        });
-        this._pending = [];
-      }
-    }
-  }
-
-  _reportEvent(eventInfo, opt_noLog) {
-    const {type, value, name} = eventInfo;
-    document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
-    if (opt_noLog) { return; }
-    if (type !== ERROR.TYPE) {
-      if (value !== undefined) {
-        console.info(`Reporting: ${name}: ${value}`);
-      } else {
-        console.info(`Reporting: ${name}`);
-      }
-    }
-  }
-
-  _createEventInfo(type, category, name, value, eventDetails) {
-    const eventInfo = {
-      type,
-      category,
-      name,
-      value,
-      eventStart: now(),
-    };
-
-    if (typeof(eventDetails) === 'object' &&
-      Object.entries(eventDetails).length !== 0) {
-      eventInfo.eventDetails = JSON.stringify(eventDetails);
-    }
-
-    if (this._reportRepoName) {
-      eventInfo.repoName = this._reportRepoName;
-    }
-
-    const isInBackgroundTab = document.visibilityState === 'hidden';
-    if (isInBackgroundTab !== undefined) {
-      eventInfo.inBackgroundTab = isInBackgroundTab;
-    }
-
-    if (this._flagsService.enabledExperiments.length) {
-      eventInfo.enabledExperiments =
-        JSON.stringify(this._flagsService.enabledExperiments);
-    }
-
-    return eventInfo;
-  }
-
-  /**
-   * User-perceived app start time, should be reported when the app is ready.
-   */
-  appStarted() {
-    this.timeEnd(TIMING.EVENT.APP_STARTED);
-    this._reportNavResTimes();
-  }
-
-  onVisibilityChange() {
-    this.hiddenDurationTimer.onVisibilityChange();
-    const eventName = `Visibility changed to ${document.visibilityState}`;
-    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.VISIBILITY,
-        eventName, undefined, {
-          hiddenDurationMs: this.hiddenDurationTimer.hiddenDurationMs,
-        }, true);
-  }
-
-  /**
-   * Browser's navigation and resource timings
-   */
-  _reportNavResTimes() {
-    const perfEvents = Object.keys(this.performanceTiming.toJSON());
-    perfEvents.forEach(
-        eventName => this._reportPerformanceTiming(eventName)
-    );
-  }
-
-  _reportPerformanceTiming(eventName, eventDetails) {
-    const eventTiming = this.performanceTiming[eventName];
-    if (eventTiming > 0) {
-      const elapsedTime = eventTiming -
-          this.performanceTiming.navigationStart;
-      // NavResTime - Navigation and resource timings.
-      this.reporter(TIMING.TYPE, TIMING.CATEGORY.UI_LATENCY,
-          `NavResTime - ${eventName}`, elapsedTime, eventDetails, true);
-    }
-  }
-
-  beforeLocationChanged() {
-    for (const prop of Object.keys(this._baselines)) {
-      delete this._baselines[prop];
-    }
-    this.time(TIMER.CHANGE_DISPLAYED);
-    this.time(TIMER.CHANGE_LOAD_FULL);
-    this.time(TIMER.DASHBOARD_DISPLAYED);
-    this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
-    this.time(TIMER.DIFF_VIEW_DISPLAYED);
-    this.time(TIMER.DIFF_VIEW_LOAD_FULL);
-    this.time(TIMER.FILE_LIST_DISPLAYED);
-    this._reportRepoName = undefined;
-    // reset slow rpc list since here start page loads which report these rpcs
-    this._slowRpcList = [];
-    this.hiddenDurationTimer.reset();
-  }
-
-  locationChanged(page) {
-    this.reporter(NAVIGATION.TYPE, NAVIGATION.CATEGORY.LOCATION_CHANGED,
-        NAVIGATION.EVENT.PAGE, page);
-  }
-
-  dashboardDisplayed() {
-    if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
-      this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, this._pageLoadDetails());
-    } else {
-      this.timeEnd(TIMER.DASHBOARD_DISPLAYED, this._pageLoadDetails());
-    }
-  }
-
-  changeDisplayed() {
-    if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) {
-      this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, this._pageLoadDetails());
-    } else {
-      this.timeEnd(TIMER.CHANGE_DISPLAYED, this._pageLoadDetails());
-    }
-  }
-
-  changeFullyLoaded() {
-    if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) {
-      this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL);
-    } else {
-      this.timeEnd(TIMER.CHANGE_LOAD_FULL);
-    }
-  }
-
-  diffViewDisplayed() {
-    if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
-      this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
-    } else {
-      this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
-    }
-  }
-
-  diffViewFullyLoaded() {
-    if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
-      this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL);
-    } else {
-      this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
-    }
-  }
-
-  diffViewContentDisplayed() {
-    if (this._baselines.hasOwnProperty(
-        TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) {
-      this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
-    } else {
-      this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
-    }
-  }
-
-  fileListDisplayed() {
-    if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
-      this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED);
-    } else {
-      this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
-    }
-  }
-
-  _pageLoadDetails() {
-    const details = {
-      rpcList: this.slowRpcSnapshot,
-    };
-
-    if (window.screen) {
-      details.screenSize = {
-        width: window.screen.width,
-        height: window.screen.height,
-      };
-    }
-
-    if (document && document.documentElement) {
-      details.viewport = {
-        width: document.documentElement.clientWidth,
-        height: document.documentElement.clientHeight,
-      };
-    }
-
-    if (window.performance && window.performance.memory) {
-      const toMb = bytes => Math.round((bytes / (1024 * 1024)) * 100) / 100;
-      details.usedJSHeapSizeMb =
-        toMb(window.performance.memory.usedJSHeapSize);
-    }
-
-    details.hiddenDurationMs = this.hiddenDurationTimer.hiddenDurationMs;
-    return details;
-  }
-
-  reportExtension(name) {
-    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.EXTENSION_DETECTED, name);
-  }
-
-  pluginLoaded(name) {
-    if (name.startsWith('metrics-')) {
-      this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
-    }
-  }
-
-  pluginsLoaded(pluginsList) {
-    this.timeEnd(TIMER.PLUGINS_LOADED);
-    this.reporter(
-        LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
-        LIFECYCLE.CATEGORY.PLUGINS_INSTALLED, undefined,
-        {pluginsList: pluginsList || []}, true);
-  }
-
-  /**
-   * Reset named timer.
-   */
-  time(name) {
-    this._baselines[name] = now();
-    window.performance.mark(`${name}-start`);
-  }
-
-  /**
-   * Finish named timer and report it to server.
-   */
-  timeEnd(name, eventDetails) {
-    if (!this._baselines.hasOwnProperty(name)) { return; }
-    const baseTime = this._baselines[name];
-    delete this._baselines[name];
-    this._reportTiming(name, now() - baseTime, eventDetails);
-
-    // Finalize the interval. Either from a registered start mark or
-    // the navigation start time (if baseTime is 0).
-    if (baseTime !== 0) {
-      window.performance.measure(name, `${name}-start`);
-    } else {
-      // Microsft Edge does not handle the 2nd param correctly
-      // (if undefined).
-      window.performance.measure(name);
-    }
-  }
-
-  /**
-   * Reports just line timeEnd, but additionally reports an average given a
-   * denominator and a separate reporiting name for the average.
-   *
-   * @param {string} name Timing name.
-   * @param {string} averageName Average timing name.
-   * @param {number} denominator Number by which to divide the total to
-   *     compute the average.
-   */
-  timeEndWithAverage(name, averageName, denominator) {
-    if (!this._baselines.hasOwnProperty(name)) { return; }
-    const baseTime = this._baselines[name];
-    this.timeEnd(name);
-
-    // Guard against division by zero.
-    if (!denominator) { return; }
-    const time = now() - baseTime;
-    this._reportTiming(averageName, time / denominator);
-  }
-
-  /**
-   * Send a timing report with an arbitrary time value.
-   *
-   * @param {string} name Timing name.
-   * @param {number} time The time to report as an integer of milliseconds.
-   * @param {Object} eventDetails non sensitive details
-   */
-  _reportTiming(name, time, eventDetails) {
-    this.reporter(TIMING.TYPE, TIMING.CATEGORY.UI_LATENCY, name, time,
-        eventDetails);
-  }
-
-  /**
-   * Get a timer object to for reporing a user timing. The start time will be
-   * the time that the object has been created, and the end time will be the
-   * time that the "end" method is called on the object.
-   *
-   * @param {string} name Timing name.
-   * @returns {!Object} The timer object.
-   */
-  getTimer(name) {
-    let called = false;
-    let start;
-    let max = null;
-
-    const timer = {
-
-      // Clear the timer and reset the start time.
-      reset: () => {
-        called = false;
-        start = now();
-        return timer;
-      },
-
-      // Stop the timer and report the intervening time.
-      end: () => {
-        if (called) {
-          throw new Error(`Timer for "${name}" already ended.`);
-        }
-        called = true;
-        const time = now() - start;
-
-        // If a maximum is specified and the time exceeds it, do not report.
-        if (max && time > max) { return timer; }
-
-        this._reportTiming(name, time);
-        return timer;
-      },
-
-      // Set a maximum reportable time. If a maximum is set and the timer is
-      // ended after the specified amount of time, the value is not reported.
-      withMaximum(maximum) {
-        max = maximum;
-        return timer;
-      },
-    };
-
-    // The timer is initialized to its creation time.
-    return timer.reset();
-  }
-
-  /**
-   * Log timing information for an RPC.
-   *
-   * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated.
-   * @param {number} elapsed The time elapsed of the RPC.
-   */
-  reportRpcTiming(anonymizedUrl, elapsed) {
-    this.reporter(TIMING.TYPE, TIMING.CATEGORY.RPC, 'RPC-' + anonymizedUrl,
-        elapsed, {}, true);
-    if (elapsed >= SLOW_RPC_THRESHOLD) {
-      this._slowRpcList.push({anonymizedUrl, elapsed});
-    }
-  }
-
-  reportLifeCycle(eventName, details) {
-    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.DEFAULT, eventName,
-        undefined, details, true);
-  }
-
-  reportInteraction(eventName, details) {
-    this.reporter(INTERACTION.TYPE, INTERACTION.CATEGORY.DEFAULT, eventName,
-        undefined, details, true);
-  }
-
-  /**
-   * A draft interaction was started. Update the time-betweeen-draft-actions
-   * timer.
-   */
-  recordDraftInteraction() {
-    // If there is no timer defined, then this is the first interaction.
-    // Set up the timer so that it's ready to record the intervening time when
-    // called again.
-    const timer = this._timers.timeBetweenDraftActions;
-    if (!timer) {
-      // Create a timer with a maximum length.
-      this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER)
-          .withMaximum(DRAFT_ACTION_TIMER_MAX);
-      return;
-    }
-
-    // Mark the time and reinitialize the timer.
-    timer.end().reset();
-  }
-
-  reportErrorDialog(message) {
-    this.reporter(ERROR.TYPE, ERROR.CATEGORY.ERROR_DIALOG,
-        'ErrorDialog: ' + message, {error: new Error(message)});
-  }
-
-  setRepoName(repoName) {
-    this._reportRepoName = repoName;
-  }
-}
-
-export const DEFAULT_STARTUP_TIMERS = {...STARTUP_TIMERS};
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
new file mode 100644
index 0000000..8c70df3
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type EventValue = string | number | {error: Error};
+
+// TODO(dmfilippov): TS-fix-any use more specific type instead if possible
+export type EventDetails = any;
+
+export interface Timer {
+  reset(): this;
+  end(): this;
+  withMaximum(maximum: number): this;
+}
+
+export interface ReportingService {
+  reporter(
+    type: string,
+    category: string,
+    eventName: string,
+    eventValue?: EventValue,
+    eventDetails?: EventDetails,
+    opt_noLog?: boolean
+  ): void;
+
+  appStarted(): void;
+  onVisibilityChange(): void;
+  beforeLocationChanged(): void;
+  locationChanged(page: string): void;
+  dashboardDisplayed(): void;
+  changeDisplayed(): void;
+  changeFullyLoaded(): void;
+  diffViewDisplayed(): void;
+  diffViewFullyLoaded(): void;
+  diffViewContentDisplayed(): void;
+  fileListDisplayed(): void;
+  reportExtension(name: string): void;
+  pluginLoaded(name: string): void;
+  pluginsLoaded(pluginsList?: string[]): void;
+  /**
+   * Reset named timer.
+   */
+  time(name: string): void;
+  /**
+   * Finish named timer and report it to server.
+   */
+  timeEnd(name: string, eventDetails?: EventDetails): void;
+  /**
+   * Reports just line timeEnd, but additionally reports an average given a
+   * denominator and a separate reporiting name for the average.
+   *
+   * @param name Timing name.
+   * @param averageName Average timing name.
+   * @param denominator Number by which to divide the total to
+   *     compute the average.
+   */
+  timeEndWithAverage(
+    name: string,
+    averageName: string,
+    denominator: number
+  ): void;
+  /**
+   * Get a timer object to for reporing a user timing. The start time will be
+   * the time that the object has been created, and the end time will be the
+   * time that the "end" method is called on the object.
+   */
+  getTimer(name: string): Timer;
+  /**
+   * Log timing information for an RPC.
+   *
+   * @param anonymizedUrl The URL of the RPC with tokens obfuscated.
+   * @param elapsed The time elapsed of the RPC.
+   */
+  reportRpcTiming(anonymizedUrl: string, elapsed: number): void;
+  reportLifeCycle(eventName: string, details: EventDetails): void;
+  reportInteraction(eventName: string, details: EventDetails): void;
+  /**
+   * A draft interaction was started. Update the time-betweeen-draft-actions
+   * timer.
+   */
+  recordDraftInteraction(): void;
+  reportErrorDialog(message: string): void;
+  setRepoName(repoName: string): 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
new file mode 100644
index 0000000..111820b
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -0,0 +1,820 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {AppContext} from '../app-context';
+import {FlagsService} from '../flags/flags';
+import {
+  EventDetails,
+  EventValue,
+  ReportingService,
+  Timer,
+} from './gr-reporting';
+import {hasOwnProperty} from '../../utils/common-util';
+
+// Latency reporting constants.
+
+const TIMING = {
+  TYPE: 'timing-report',
+  CATEGORY: {
+    UI_LATENCY: 'UI Latency',
+    RPC: 'RPC Timing',
+  },
+  EVENT: {
+    APP_STARTED: 'App Started',
+  },
+};
+
+const LIFECYCLE = {
+  TYPE: 'lifecycle',
+  CATEGORY: {
+    DEFAULT: 'Default',
+    EXTENSION_DETECTED: 'Extension detected',
+    PLUGINS_INSTALLED: 'Plugins installed',
+    VISIBILITY: 'Visibility',
+  },
+};
+
+const INTERACTION = {
+  TYPE: 'interaction',
+  CATEGORY: {
+    DEFAULT: 'Default',
+    VISIBILITY: 'Visibility',
+  },
+};
+
+const NAVIGATION = {
+  TYPE: 'nav-report',
+  CATEGORY: {
+    LOCATION_CHANGED: 'Location Changed',
+  },
+  EVENT: {
+    PAGE: 'Page',
+  },
+};
+
+const ERROR = {
+  TYPE: 'error',
+  CATEGORY: {
+    EXCEPTION: 'exception',
+    ERROR_DIALOG: 'Error Dialog',
+  },
+};
+
+const TIMER = {
+  CHANGE_DISPLAYED: 'ChangeDisplayed',
+  CHANGE_LOAD_FULL: 'ChangeFullyLoaded',
+  DASHBOARD_DISPLAYED: 'DashboardDisplayed',
+  DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent',
+  DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
+  DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded',
+  FILE_LIST_DISPLAYED: 'FileListDisplayed',
+  PLUGINS_LOADED: 'PluginsLoaded',
+  STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
+  STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded',
+  STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
+  STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent',
+  STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
+  STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded',
+  STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed',
+  WEB_COMPONENTS_READY: 'WebComponentsReady',
+  METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded',
+};
+
+const STARTUP_TIMERS = {
+  [TIMER.PLUGINS_LOADED]: 0,
+  [TIMER.METRICS_PLUGIN_LOADED]: 0,
+  [TIMER.STARTUP_CHANGE_DISPLAYED]: 0,
+  [TIMER.STARTUP_CHANGE_LOAD_FULL]: 0,
+  [TIMER.STARTUP_DASHBOARD_DISPLAYED]: 0,
+  [TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED]: 0,
+  [TIMER.STARTUP_DIFF_VIEW_DISPLAYED]: 0,
+  [TIMER.STARTUP_DIFF_VIEW_LOAD_FULL]: 0,
+  [TIMER.STARTUP_FILE_LIST_DISPLAYED]: 0,
+  [TIMING.EVENT.APP_STARTED]: 0,
+  // WebComponentsReady timer is triggered from gr-router.
+  [TIMER.WEB_COMPONENTS_READY]: 0,
+};
+
+const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
+const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
+const SLOW_RPC_THRESHOLD = 500;
+
+export function initErrorReporter(appContext: AppContext) {
+  const reportingService = appContext.reportingService;
+  // TODO(dmfilippo): TS-fix-any oldOnError - define correct type
+  const onError = function (
+    oldOnError: Function,
+    msg: string,
+    url: string,
+    line: number,
+    column: number,
+    error: Error
+  ) {
+    if (oldOnError) {
+      oldOnError(msg, url, line, column, error);
+    }
+    if (error) {
+      line = line || (error as any).lineNumber;
+      column = column || (error as any).columnNumber;
+      let shortenedErrorStack = msg;
+      if (error.stack) {
+        const errorStackLines = error.stack.split('\n');
+        shortenedErrorStack = errorStackLines
+          .slice(0, Math.min(3, errorStackLines.length))
+          .join('\n');
+      }
+      msg = shortenedErrorStack || error.toString();
+    }
+    const payload = {
+      url,
+      line,
+      column,
+      error,
+    };
+    reportingService.reporter(
+      ERROR.TYPE,
+      ERROR.CATEGORY.EXCEPTION,
+      msg,
+      payload
+    );
+    return true;
+  };
+  // TODO(dmfilippov): TS-fix-any unclear what is context
+  const catchErrors = function (opt_context?: any) {
+    const context = opt_context || window;
+    context.onerror = onError.bind(null, context.onerror);
+    context.addEventListener(
+      'unhandledrejection',
+      (e: PromiseRejectionEvent) => {
+        const msg = e.reason.message;
+        const payload = {
+          error: e.reason,
+        };
+        reportingService.reporter(
+          ERROR.TYPE,
+          ERROR.CATEGORY.EXCEPTION,
+          msg,
+          payload
+        );
+      }
+    );
+  };
+
+  catchErrors();
+
+  // for testing
+  return {catchErrors};
+}
+
+export function initPerformanceReporter(appContext: AppContext) {
+  const reportingService = appContext.reportingService;
+  // PerformanceObserver interface is a browser API.
+  if (window.PerformanceObserver) {
+    const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
+    // Safari doesn't support longtask yet
+    if (supportedEntryTypes.includes('longtask')) {
+      const catchLongJsTasks = new PerformanceObserver(list => {
+        for (const task of list.getEntries()) {
+          // We are interested in longtask longer than 200 ms (default is 50 ms)
+          if (task.duration > 200) {
+            reportingService.reporter(
+              TIMING.TYPE,
+              TIMING.CATEGORY.UI_LATENCY,
+              `Task ${task.name}`,
+              Math.round(task.duration),
+              {},
+              false
+            );
+          }
+        }
+      });
+      catchLongJsTasks.observe({entryTypes: ['longtask']});
+    }
+  }
+}
+
+export function initVisibilityReporter(appContext: AppContext) {
+  const reportingService = appContext.reportingService;
+  document.addEventListener('visibilitychange', () => {
+    reportingService.onVisibilityChange();
+  });
+}
+
+// Calculates the time of Gerrit being in a background tab. When Gerrit reports
+// a pageLoad metric it’s attached to its details for latency analysis.
+// It resets on locationChange.
+class HiddenDurationTimer {
+  public accHiddenDurationMs = 0;
+
+  public lastVisibleTimestampMs: number | null = null;
+
+  constructor() {
+    this.reset();
+  }
+
+  reset() {
+    this.accHiddenDurationMs = 0;
+    this.lastVisibleTimestampMs = 0;
+  }
+
+  onVisibilityChange() {
+    if (document.visibilityState === 'hidden') {
+      this.lastVisibleTimestampMs = now();
+    } else if (document.visibilityState === 'visible') {
+      if (this.lastVisibleTimestampMs !== null) {
+        this.accHiddenDurationMs += now() - this.lastVisibleTimestampMs;
+        // Set to null for guarding against two 'visible' events in a row.
+        this.lastVisibleTimestampMs = null;
+      }
+    }
+  }
+
+  get hiddenDurationMs() {
+    if (
+      document.visibilityState === 'hidden' &&
+      this.lastVisibleTimestampMs !== null
+    ) {
+      return this.accHiddenDurationMs + now() - this.lastVisibleTimestampMs;
+    }
+    return this.accHiddenDurationMs;
+  }
+}
+
+export function now() {
+  return Math.round(window.performance.now());
+}
+
+type PeformanceTimingEventName = keyof Omit<PerformanceTiming, 'toJSON'>;
+
+interface EventInfo {
+  type: string;
+  category: string;
+  name: string;
+  value?: EventValue;
+  eventStart: number;
+  eventDetails?: string;
+  repoName?: string;
+  inBackgroundTab?: boolean;
+  enabledExperiments?: string;
+}
+
+interface PageLoadDetails {
+  rpcList: SlowRpcCall[];
+  hiddenDurationMs: number;
+  screenSize?: {width: number; height: number};
+  viewport?: {width: number; height: number};
+  usedJSHeapSizeMb?: number;
+}
+
+interface SlowRpcCall {
+  anonymizedUrl: string;
+  elapsed: number;
+}
+
+type PendingReportInfo = [EventInfo, boolean | undefined];
+
+export class GrReporting implements ReportingService {
+  private readonly _flagsService: FlagsService;
+
+  private readonly _baselines = STARTUP_TIMERS;
+
+  private _reportRepoName: string | undefined;
+
+  private _timers: {timeBetweenDraftActions: Timer | null} = {
+    timeBetweenDraftActions: null,
+  };
+
+  private _pending: PendingReportInfo[] = [];
+
+  private _slowRpcList: SlowRpcCall[] = [];
+
+  public readonly hiddenDurationTimer = new HiddenDurationTimer();
+
+  constructor(flagsService: FlagsService) {
+    this._flagsService = flagsService;
+  }
+
+  private get performanceTiming() {
+    return window.performance.timing;
+  }
+
+  private get slowRpcSnapshot() {
+    return (this._slowRpcList || []).slice();
+  }
+
+  private _arePluginsLoaded() {
+    return (
+      this._baselines && !hasOwnProperty(this._baselines, TIMER.PLUGINS_LOADED)
+    );
+  }
+
+  private _isMetricsPluginLoaded() {
+    return (
+      this._arePluginsLoaded() ||
+      (this._baselines &&
+        !hasOwnProperty(this._baselines, TIMER.METRICS_PLUGIN_LOADED))
+    );
+  }
+
+  /**
+   * Reporter reports events. Events will be queued if metrics plugin is not
+   * yet installed.
+   *
+   * @param {string} type
+   * @param {string} category
+   * @param {string} eventName
+   * @param {string|number} eventValue
+   * @param {Object} eventDetails
+   * @param {boolean|undefined} opt_noLog If true, the event will not be
+   *     logged to the JS console.
+   */
+  reporter(
+    type: string,
+    category: string,
+    eventName: string,
+    eventValue?: EventValue,
+    eventDetails?: EventDetails,
+    opt_noLog?: boolean
+  ) {
+    const eventInfo = this._createEventInfo(
+      type,
+      category,
+      eventName,
+      eventValue,
+      eventDetails
+    );
+    if (type === ERROR.TYPE && category === ERROR.CATEGORY.EXCEPTION) {
+      console.error((eventValue && (eventValue as any).error) || eventName);
+    }
+
+    // We report events immediately when metrics plugin is loaded
+    if (this._isMetricsPluginLoaded() && !this._pending.length) {
+      this._reportEvent(eventInfo, opt_noLog);
+    } else {
+      // We cache until metrics plugin is loaded
+      this._pending.push([eventInfo, opt_noLog]);
+      if (this._isMetricsPluginLoaded()) {
+        this._pending.forEach(([eventInfo, opt_noLog]) => {
+          this._reportEvent(eventInfo, opt_noLog);
+        });
+        this._pending = [];
+      }
+    }
+  }
+
+  private _reportEvent(eventInfo: EventInfo, opt_noLog?: boolean) {
+    const {type, value, name} = eventInfo;
+    document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
+    if (opt_noLog) {
+      return;
+    }
+    if (type !== ERROR.TYPE) {
+      if (value !== undefined) {
+        console.info(`Reporting: ${name}: ${value}`);
+      } else {
+        console.info(`Reporting: ${name}`);
+      }
+    }
+  }
+
+  private _createEventInfo(
+    type: string,
+    category: string,
+    name: string,
+    value?: EventValue,
+    eventDetails?: EventDetails
+  ): EventInfo {
+    const eventInfo: EventInfo = {
+      type,
+      category,
+      name,
+      value,
+      eventStart: now(),
+    };
+
+    if (
+      typeof eventDetails === 'object' &&
+      Object.entries(eventDetails).length !== 0
+    ) {
+      eventInfo.eventDetails = JSON.stringify(eventDetails);
+    }
+
+    if (this._reportRepoName) {
+      eventInfo.repoName = this._reportRepoName;
+    }
+
+    const isInBackgroundTab = document.visibilityState === 'hidden';
+    if (isInBackgroundTab !== undefined) {
+      eventInfo.inBackgroundTab = isInBackgroundTab;
+    }
+
+    if (this._flagsService.enabledExperiments.length) {
+      eventInfo.enabledExperiments = JSON.stringify(
+        this._flagsService.enabledExperiments
+      );
+    }
+
+    return eventInfo;
+  }
+
+  /**
+   * User-perceived app start time, should be reported when the app is ready.
+   */
+  appStarted() {
+    this.timeEnd(TIMING.EVENT.APP_STARTED);
+    this._reportNavResTimes();
+  }
+
+  onVisibilityChange() {
+    this.hiddenDurationTimer.onVisibilityChange();
+    const eventName = `Visibility changed to ${document.visibilityState}`;
+    this.reporter(
+      LIFECYCLE.TYPE,
+      LIFECYCLE.CATEGORY.VISIBILITY,
+      eventName,
+      undefined,
+      {
+        hiddenDurationMs: this.hiddenDurationTimer.hiddenDurationMs,
+      },
+      true
+    );
+  }
+
+  /**
+   * Browser's navigation and resource timings
+   */
+  private _reportNavResTimes() {
+    const perfEvents = Object.keys(this.performanceTiming.toJSON());
+    perfEvents.forEach(eventName =>
+      this._reportPerformanceTiming(eventName as PeformanceTimingEventName)
+    );
+  }
+
+  private _reportPerformanceTiming(
+    eventName: PeformanceTimingEventName,
+    eventDetails?: EventDetails
+  ) {
+    const eventTiming = this.performanceTiming[eventName];
+    if (eventTiming > 0) {
+      const elapsedTime = eventTiming - this.performanceTiming.navigationStart;
+      // NavResTime - Navigation and resource timings.
+      this.reporter(
+        TIMING.TYPE,
+        TIMING.CATEGORY.UI_LATENCY,
+        `NavResTime - ${eventName}`,
+        elapsedTime,
+        eventDetails,
+        true
+      );
+    }
+  }
+
+  beforeLocationChanged() {
+    for (const prop of Object.keys(this._baselines)) {
+      delete this._baselines[prop];
+    }
+    this.time(TIMER.CHANGE_DISPLAYED);
+    this.time(TIMER.CHANGE_LOAD_FULL);
+    this.time(TIMER.DASHBOARD_DISPLAYED);
+    this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
+    this.time(TIMER.DIFF_VIEW_DISPLAYED);
+    this.time(TIMER.DIFF_VIEW_LOAD_FULL);
+    this.time(TIMER.FILE_LIST_DISPLAYED);
+    this._reportRepoName = undefined;
+    // reset slow rpc list since here start page loads which report these rpcs
+    this._slowRpcList = [];
+    this.hiddenDurationTimer.reset();
+  }
+
+  locationChanged(page: string) {
+    this.reporter(
+      NAVIGATION.TYPE,
+      NAVIGATION.CATEGORY.LOCATION_CHANGED,
+      NAVIGATION.EVENT.PAGE,
+      page
+    );
+  }
+
+  dashboardDisplayed() {
+    if (hasOwnProperty(this._baselines, TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
+      this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, this._pageLoadDetails());
+    } else {
+      this.timeEnd(TIMER.DASHBOARD_DISPLAYED, this._pageLoadDetails());
+    }
+  }
+
+  changeDisplayed() {
+    if (hasOwnProperty(this._baselines, TIMER.STARTUP_CHANGE_DISPLAYED)) {
+      this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, this._pageLoadDetails());
+    } else {
+      this.timeEnd(TIMER.CHANGE_DISPLAYED, this._pageLoadDetails());
+    }
+  }
+
+  changeFullyLoaded() {
+    if (hasOwnProperty(this._baselines, TIMER.STARTUP_CHANGE_LOAD_FULL)) {
+      this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL);
+    } else {
+      this.timeEnd(TIMER.CHANGE_LOAD_FULL);
+    }
+  }
+
+  diffViewDisplayed() {
+    if (hasOwnProperty(this._baselines, TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
+      this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
+    } else {
+      this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
+    }
+  }
+
+  diffViewFullyLoaded() {
+    if (hasOwnProperty(this._baselines, TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
+      this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL);
+    } else {
+      this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
+    }
+  }
+
+  diffViewContentDisplayed() {
+    if (
+      hasOwnProperty(this._baselines, TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)
+    ) {
+      this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
+    } else {
+      this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
+    }
+  }
+
+  fileListDisplayed() {
+    if (hasOwnProperty(this._baselines, TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
+      this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED);
+    } else {
+      this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
+    }
+  }
+
+  private _pageLoadDetails(): PageLoadDetails {
+    const details: PageLoadDetails = {
+      rpcList: this.slowRpcSnapshot,
+      hiddenDurationMs: this.hiddenDurationTimer.accHiddenDurationMs,
+    };
+
+    if (window.screen) {
+      details.screenSize = {
+        width: window.screen.width,
+        height: window.screen.height,
+      };
+    }
+
+    if (document && document.documentElement) {
+      details.viewport = {
+        width: document.documentElement.clientWidth,
+        height: document.documentElement.clientHeight,
+      };
+    }
+
+    if (window.performance && window.performance.memory) {
+      const toMb = (bytes: number) =>
+        Math.round((bytes / (1024 * 1024)) * 100) / 100;
+      details.usedJSHeapSizeMb = toMb(window.performance.memory.usedJSHeapSize);
+    }
+
+    details.hiddenDurationMs = this.hiddenDurationTimer.hiddenDurationMs;
+    return details;
+  }
+
+  reportExtension(name: string) {
+    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.EXTENSION_DETECTED, name);
+  }
+
+  pluginLoaded(name: string) {
+    if (name.startsWith('metrics-')) {
+      this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
+    }
+  }
+
+  pluginsLoaded(pluginsList?: string[]) {
+    this.timeEnd(TIMER.PLUGINS_LOADED);
+    this.reporter(
+      LIFECYCLE.TYPE,
+      LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
+      LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
+      undefined,
+      {pluginsList: pluginsList || []},
+      true
+    );
+  }
+
+  /**
+   * Reset named timer.
+   */
+  time(name: string) {
+    this._baselines[name] = now();
+    window.performance.mark(`${name}-start`);
+  }
+
+  /**
+   * Finish named timer and report it to server.
+   */
+  timeEnd(name: string, eventDetails?: EventDetails) {
+    if (!hasOwnProperty(this._baselines, name)) {
+      return;
+    }
+    const baseTime = this._baselines[name];
+    delete this._baselines[name];
+    this._reportTiming(name, now() - baseTime, eventDetails);
+
+    // Finalize the interval. Either from a registered start mark or
+    // the navigation start time (if baseTime is 0).
+    if (baseTime !== 0) {
+      window.performance.measure(name, `${name}-start`);
+    } else {
+      // Microsft Edge does not handle the 2nd param correctly
+      // (if undefined).
+      window.performance.measure(name);
+    }
+  }
+
+  /**
+   * Reports just line timeEnd, but additionally reports an average given a
+   * denominator and a separate reporiting name for the average.
+   *
+   * @param name Timing name.
+   * @param averageName Average timing name.
+   * @param denominator Number by which to divide the total to
+   *     compute the average.
+   */
+  timeEndWithAverage(name: string, averageName: string, denominator: number) {
+    if (!hasOwnProperty(this._baselines, name)) {
+      return;
+    }
+    const baseTime = this._baselines[name];
+    this.timeEnd(name);
+
+    // Guard against division by zero.
+    if (!denominator) {
+      return;
+    }
+    const time = now() - baseTime;
+    this._reportTiming(averageName, time / denominator);
+  }
+
+  /**
+   * Send a timing report with an arbitrary time value.
+   *
+   * @param name Timing name.
+   * @param time The time to report as an integer of milliseconds.
+   * @param eventDetails non sensitive details
+   */
+  private _reportTiming(
+    name: string,
+    time: number,
+    eventDetails?: EventDetails
+  ) {
+    this.reporter(
+      TIMING.TYPE,
+      TIMING.CATEGORY.UI_LATENCY,
+      name,
+      time,
+      eventDetails
+    );
+  }
+
+  /**
+   * Get a timer object to for reporing a user timing. The start time will be
+   * the time that the object has been created, and the end time will be the
+   * time that the "end" method is called on the object.
+   */
+  getTimer(name: string): Timer {
+    let called = false;
+    let start: number;
+    let max: number | null = null;
+
+    const timer: Timer = {
+      // Clear the timer and reset the start time.
+      reset: () => {
+        called = false;
+        start = now();
+        return timer;
+      },
+
+      // Stop the timer and report the intervening time.
+      end: () => {
+        if (called) {
+          throw new Error(`Timer for "${name}" already ended.`);
+        }
+        called = true;
+        const time = now() - start;
+
+        // If a maximum is specified and the time exceeds it, do not report.
+        if (max && time > max) {
+          return timer;
+        }
+
+        this._reportTiming(name, time);
+        return timer;
+      },
+
+      // Set a maximum reportable time. If a maximum is set and the timer is
+      // ended after the specified amount of time, the value is not reported.
+      withMaximum(maximum) {
+        max = maximum;
+        return timer;
+      },
+    };
+
+    // The timer is initialized to its creation time.
+    return timer.reset();
+  }
+
+  /**
+   * Log timing information for an RPC.
+   *
+   * @param anonymizedUrl The URL of the RPC with tokens obfuscated.
+   * @param elapsed The time elapsed of the RPC.
+   */
+  reportRpcTiming(anonymizedUrl: string, elapsed: number) {
+    this.reporter(
+      TIMING.TYPE,
+      TIMING.CATEGORY.RPC,
+      'RPC-' + anonymizedUrl,
+      elapsed,
+      {},
+      true
+    );
+    if (elapsed >= SLOW_RPC_THRESHOLD) {
+      this._slowRpcList.push({anonymizedUrl, elapsed});
+    }
+  }
+
+  reportLifeCycle(eventName: string, details: EventDetails) {
+    this.reporter(
+      LIFECYCLE.TYPE,
+      LIFECYCLE.CATEGORY.DEFAULT,
+      eventName,
+      undefined,
+      details,
+      true
+    );
+  }
+
+  reportInteraction(eventName: string, details: EventDetails) {
+    this.reporter(
+      INTERACTION.TYPE,
+      INTERACTION.CATEGORY.DEFAULT,
+      eventName,
+      undefined,
+      details,
+      true
+    );
+  }
+
+  /**
+   * A draft interaction was started. Update the time-betweeen-draft-actions
+   * timer.
+   */
+  recordDraftInteraction() {
+    // If there is no timer defined, then this is the first interaction.
+    // Set up the timer so that it's ready to record the intervening time when
+    // called again.
+    const timer = this._timers.timeBetweenDraftActions;
+    if (!timer) {
+      // Create a timer with a maximum length.
+      this._timers.timeBetweenDraftActions = this.getTimer(
+        DRAFT_ACTION_TIMER
+      ).withMaximum(DRAFT_ACTION_TIMER_MAX);
+      return;
+    }
+
+    // Mark the time and reinitialize the timer.
+    timer.end().reset();
+  }
+
+  reportErrorDialog(message: string) {
+    this.reporter(
+      ERROR.TYPE,
+      ERROR.CATEGORY.ERROR_DIALOG,
+      'ErrorDialog: ' + message,
+      {error: new Error(message)}
+    );
+  }
+
+  setRepoName(repoName: string) {
+    this._reportRepoName = repoName;
+  }
+}
+
+export const DEFAULT_STARTUP_TIMERS = {...STARTUP_TIMERS};
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js
index 7c70e10..73f8580 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.js
@@ -16,7 +16,7 @@
  */
 
 import '../../test/common-test-setup-karma.js';
-import {GrReporting} from './gr-reporting.js';
+import {GrReporting} from './gr-reporting_impl.js';
 import {grReportingMock} from './gr-reporting_mock.js';
 suite('gr-reporting_mock tests', () => {
   test('mocks all public methods', () => {
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
index 1e50766..08e4a55 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
@@ -16,7 +16,7 @@
  */
 
 import '../../test/common-test-setup-karma.js';
-import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting.js';
+import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting_impl.js';
 import {appContext} from '../app-context.js';
 suite('gr-reporting tests', () => {
   let service;
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index efe575a..bc6c2df 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -36,7 +36,8 @@
 
     /* Advanced Options */
     "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
-    "incremental": true
+    "incremental": true,
+    "experimentalDecorators": true
   },
   // With the * pattern (without an extension), only supported files
   // are included. The supported files are .ts, .tsx, .d.ts.
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
new file mode 100644
index 0000000..a8fc01f
--- /dev/null
+++ b/polygerrit-ui/app/types/common.ts
@@ -0,0 +1,417 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+  ChangeStatus,
+  FileInfoStatus,
+  GpgKeyInfoStatus,
+  ProblemInfoStatus,
+  RequirementStatus,
+  ReviewerState,
+  RevisionKind,
+} from '../constants/constants';
+
+export type BrandType<T, BrandName extends string> = T &
+  {[__brand in BrandName]: never};
+
+export type PatchSetNum = BrandType<'edit' | number, '_patchSet'>;
+export type ChangeId = BrandType<string, '_changeId'>;
+export type ChangeMessageId = BrandType<string, '_changeMessageId'>;
+export type LegacyChangeId = BrandType<number, '_legacyChangeId'>;
+export type NumericChangeId = BrandType<number, '_numericChangeId'>;
+export type ProjectName = BrandType<string, '_projectName'>;
+export type TopicName = BrandType<string, '_topicName'>;
+export type AccountId = BrandType<number, '_accountId'>;
+export type HttpMethod = BrandType<string, '_httpMethod'>;
+export type GitRef = BrandType<string, '_gitRef'>;
+export type RequirementType = BrandType<string, '_requirementType'>;
+export type TrackingId = BrandType<string, '_trackingId'>;
+export type ReviewInputTag = BrandType<string, '_reviewInputTag'>;
+
+// The 8-char hex GPG key ID.
+export type GpgKeyId = BrandType<string, '_gpgKeyId'>;
+
+// The 40-char (plus spaces) hex GPG key fingerprint
+export type GpgKeyFingerprint = BrandType<string, '_gpgKeyFingerprint'>;
+
+// OpenPGP User IDs (https://tools.ietf.org/html/rfc4880#section-5.11).
+export type OpenPgpUserIds = BrandType<string, '_openPgpUserIds'>;
+
+// This ID is equal to the numeric ID of the change that triggered the
+// submission. If the change that triggered the submission also has a topic, it
+// will be "<id>-<topic>" of the change that triggered the submission
+// The callers must not rely on the format of the submission ID.
+export type ChangeSubmissionId = BrandType<
+  string | number,
+  '_changeSubmissionId'
+>;
+
+// The refs/heads/ prefix is omitted in Branch name
+export type BranchName = BrandType<string, '_branchName'>;
+
+// The ID of the change in the format "'<project>~<branch>~<Change-Id>'"
+export type ChangeInfoId = BrandType<string, '_changeInfoId'>;
+export type Hashtag = BrandType<string, '_hashtag'>;
+export type StarLabel = BrandType<string, '_startLabel'>;
+export type SubmitType = BrandType<string, '_submitType'>;
+export type CommitId = BrandType<string, '_commitId'>;
+
+// The timezone offset from UTC in minutes
+export type TimezoneOffset = BrandType<number, '_timezoneOffset'>;
+
+// Timestamps are given in UTC and have the format
+// "'yyyy-mm-dd hh:mm:ss.fffffffff'"
+// where "'ffffffffff'" represents nanoseconds.
+export type Timestamp = BrandType<string, '_timestamp'>;
+
+export type IdToAttentionSetMap = {[accountId: string]: AttentionSetInfo};
+export type LabelNameToInfoMap = {[labelName: string]: LabelInfo};
+
+// The map maps the values (“-2”, “-1”, " `0`", “+1”, “+2”) to the value descriptions.
+export type LabelValueToDescriptionMap = {[labelValue: string]: string};
+
+/**
+ * The LabelInfo entity contains information about a label on a change, always corresponding to the current patch set.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#label-info
+ */
+type LabelInfo = QuickLabelInfo | DetailedLabelInfo;
+
+interface LabelCommonInfo {
+  optional?: boolean; // not set if false
+}
+
+export interface QuickLabelInfo extends LabelCommonInfo {
+  approved?: AccountInfo;
+  rejected?: AccountInfo;
+  recommended?: AccountInfo;
+  disliked?: AccountInfo;
+  blocking?: boolean; // not set if false
+  value?: number; // The voting value of the user who recommended/disliked this label on the change if it is not “+1”/“-1”.
+  default_value?: number;
+}
+
+export interface DetailedLabelInfo extends LabelCommonInfo {
+  all?: ApprovalInfo[];
+  values?: LabelValueToDescriptionMap; // A map of all values that are allowed for this label
+}
+
+/**
+ * The ChangeInfo entity contains information about a change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
+ */
+export interface ChangeInfo {
+  id: ChangeInfoId;
+  project: ProjectName;
+  branch: BranchName;
+  topic?: TopicName;
+  attention_set?: IdToAttentionSetMap;
+  assignee?: AccountInfo;
+  hashtags?: Hashtag[];
+  change_id: ChangeId;
+  subject: string;
+  status: ChangeStatus;
+  created: Timestamp;
+  updated: Timestamp;
+  submitted?: Timestamp;
+  submitter: AccountInfo;
+  starred?: boolean; // not set if false
+  stars?: StarLabel[];
+  reviewed?: boolean; // not set if false
+  submit_type?: SubmitType;
+  mergeable?: boolean;
+  submittable?: boolean;
+  insertions: number; // Number of inserted lines
+  deletions: number; // Number of deleted lines
+  total_comment_count?: number;
+  unresolved_comment_count?: number;
+  _number: LegacyChangeId;
+  owner: AccountInfo;
+  actions?: ActionInfo[];
+  requirements?: Requirement[];
+  labels?: LabelInfo[];
+  permitted_labels?: LabelNameToInfoMap;
+  removable_reviewers?: AccountInfo[];
+  reviewers?: AccountInfo[];
+  pending_reviewers?: AccountInfo[];
+  reviewer_updates?: ReviewerUpdateInfo[];
+  messages?: ChangeMessageInfo[];
+  current_revision?: CommitId;
+  revisions?: {[revisionId: string]: RevisionInfo};
+  tracking_ids?: TrackingIdInfo[];
+  _more_changes?: boolean; // not set if false
+  problems?: ProblemInfo[];
+  is_private?: boolean; // not set if false
+  work_in_progress?: boolean; // not set if false
+  has_review_started?: boolean; // not set if false
+  revert_of?: NumericChangeId;
+  submission_id?: ChangeSubmissionId;
+  cherry_pick_of_change?: NumericChangeId;
+  cherry_pick_of_patch_set?: PatchSetNum;
+  contains_git_conflicts?: boolean;
+}
+
+/**
+ * The AccountInfo entity contains information about an account.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-info
+ */
+export interface AccountInfo {
+  _account_id: AccountId;
+  name?: string;
+  display_name?: string;
+  email?: string;
+  secondary_emails?: string[];
+  username?: string;
+  avatars?: AvatarInfo[];
+  _more_accounts?: boolean; // not set if false
+  status?: string; // status message of the account
+  inactive?: boolean; // not set if false
+}
+
+/**
+ * The ActionInfo entity describes a REST API call the client canmake to
+ * manipulate a resource. These are frequently implemented by plugins and may
+ * be discovered at runtime.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#action-info
+ */
+export interface ActionInfo {
+  method?: HttpMethod; // Most actions use POST, PUT or DELETE to cause state changes.
+  label?: string; // Short title to display to a user describing the action
+  title?: string; // Longer text to display describing the action
+  enabled?: boolean; // not set if false
+}
+
+/**
+ * The Requirement entity contains information about a requirement relative to
+ * a change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#requirement
+ */
+export interface Requirement {
+  status: RequirementStatus;
+  fallbackText: string; // A human readable reason
+  type: RequirementType;
+}
+
+/**
+ * The ReviewerUpdateInfo entity contains information about updates tochange’s
+ * reviewers set.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-update-info
+ */
+export interface ReviewerUpdateInfo {
+  updated: Timestamp;
+  updated_by: AccountInfo;
+  reviewer: AccountInfo;
+  state: ReviewerState;
+}
+
+/**
+ * The ChangeMessageInfo entity contains information about a messageattached
+ * to a change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-message-info
+ */
+export interface ChangeMessageInfo {
+  id: ChangeMessageId;
+  author?: AccountInfo;
+  real_author?: AccountInfo;
+  date: Timestamp;
+  message: string;
+  tag?: ReviewInputTag;
+  _revision_number?: PatchSetNum;
+}
+
+/**
+ * The RevisionInfo entity contains information about a patch set.Not all
+ * fields are returned by default.  Additional fields can be obtained by
+ * adding o parameters as described in Query Changes.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-info
+ */
+export interface RevisionInfo {
+  kind: RevisionKind;
+  _number: PatchSetNum;
+  created: Timestamp;
+  uploader: AccountInfo;
+  ref: GitRef;
+  fetch?: {[protocol: string]: FetchInfo};
+  commit?: CommitInfo;
+  files?: {[filename: string]: FileInfo};
+  actions?: ActionInfo[];
+  reviewed?: boolean;
+  commit_with_footers?: boolean;
+  push_certificate?: PushCertificateInfo;
+  description?: string;
+}
+
+/**
+ * The TrackingIdInfo entity describes a reference to an external tracking
+ * system.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#tracking-id-info
+ */
+export interface TrackingIdInfo {
+  system: string;
+  id: TrackingId;
+}
+
+/**
+ * The ProblemInfo entity contains a description of a potential consistency
+ * problem with a change. These are not related to the code review process,
+ * but rather indicate some inconsistency in Gerrit’s database or repository
+ * metadata related to the enclosing change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#problem-info
+ */
+export interface ProblemInfo {
+  message: string;
+  status?: ProblemInfoStatus; // Only set if a fix was attempted
+  outcome?: string;
+}
+
+/**
+ * The AttentionSetInfo entity contains details of users that are in the attention set.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#attention-set-info
+ */
+export interface AttentionSetInfo {
+  account: AccountInfo;
+  last_update: Timestamp;
+}
+
+/**
+ * The ApprovalInfo entity contains information about an approval from auser
+ * for a label on a change.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#approval-info
+ */
+export interface ApprovalInfo extends AccountInfo {
+  value?: string;
+  permitted_voting_range?: VotingRangeInfo;
+  date?: Timestamp;
+  tag?: ReviewInputTag;
+  post_submit?: boolean; // not set if false
+}
+
+/**
+ * The AvartarInfo entity contains information about an avatar image ofan
+ * account.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#avatar-info
+ */
+export interface AvatarInfo {
+  url: string;
+  height: number;
+  width: number;
+}
+
+/**
+ * The FetchInfo entity contains information about how to fetch a patchset via
+ * a certain protocol.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#fetch-info
+ */
+export interface FetchInfo {
+  url: string;
+  ref: string;
+  commands?: {[commandName: string]: string};
+}
+
+/**
+ * The CommitInfo entity contains information about a commit.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#commit-info
+ */
+export interface CommitInfo {
+  commit?: CommitId;
+  parents: ParentCommitInfo[];
+  author: GitPersonInfo;
+  committer: GitPersonInfo;
+  subject: string;
+  message: string;
+  web_links?: WebLinkInfo[];
+}
+
+/**
+ * The parent commits of this commit as a list of CommitInfo entities.
+ * In each parent only the commit and subject fields are populated.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#commit-info
+ */
+export interface ParentCommitInfo {
+  commit: CommitId;
+  subject: string;
+}
+
+/**
+ * The FileInfo entity contains information about a file in a patch set.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#file-info
+ */
+export interface FileInfo {
+  status?: FileInfoStatus;
+  binary?: boolean; // not set if false
+  old_path?: string;
+  lines_inserted?: number;
+  lines_deleted?: number;
+  size_delta: number; // in bytes
+  size: number; // in bytes
+}
+
+/**
+ * The PushCertificateInfo entity contains information about a pushcertificate
+ * provided when the user pushed for review with git push
+ * --signed HEAD:refs/for/<branch>. Only used when signed push is
+ * enabled on the server.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#push-certificate-info
+ */
+export interface PushCertificateInfo {
+  certificate: string;
+  key: GpgKeyInfo;
+}
+
+/**
+ * The GpgKeyInfo entity contains information about a GPG public key.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#gpg-key-info
+ */
+export interface GpgKeyInfo {
+  id?: GpgKeyId;
+  fingerprint?: GpgKeyFingerprint;
+  user_ids?: OpenPgpUserIds[];
+  key?: string; // ASCII armored public key material
+  status?: GpgKeyInfoStatus;
+  problems?: string[];
+}
+
+/**
+ * The GitPersonInfo entity contains information about theauthor/committer of
+ * a commit.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#git-person-info
+ */
+export interface GitPersonInfo {
+  name: string;
+  email: string;
+  date: Timestamp;
+  tz: TimezoneOffset;
+}
+
+/**
+ * The WebLinkInfo entity describes a link to an external site.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#web-link-info
+ */
+export interface WebLinkInfo {
+  name: string;
+  url: string;
+  image_url: string;
+}
+
+/**
+ * The VotingRangeInfo entity describes the continuous voting range from minto
+ * max values.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#voting-range-info
+ */
+export interface VotingRangeInfo {
+  min: number;
+  max: number;
+}
diff --git a/polygerrit-ui/app/types/globals.ts b/polygerrit-ui/app/types/globals.ts
index ac2ae7c..116ae9e 100644
--- a/polygerrit-ui/app/types/globals.ts
+++ b/polygerrit-ui/app/types/globals.ts
@@ -20,4 +20,14 @@
   interface Window {
     CANONICAL_PATH?: string;
   }
+
+  interface Performance {
+    // typescript doesn't know about the memory property.
+    // Define it here, so it can be used everywhere
+    memory?: {
+      jsHeapSizeLimit: number;
+      totalJSHeapSize: number;
+      usedJSHeapSize: number;
+    };
+  }
 }
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
new file mode 100644
index 0000000..a7aca4c
--- /dev/null
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Functions in this file contains some widely used
+ * code patterns. If you noticed a repeated code and none of the existing util
+ * files are appropriate for it - you can wrap the code in a function and put it
+ * here. If you notice that several functions can be group together - create
+ * a separate util file for them.
+ */
+
+/**
+ * Wrapper for the Object.prototype.hasOwnProperty method
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function hasOwnProperty(obj: any, prop: PropertyKey) {
+  // Typescript rules don't allow to use obj.hasOwnProperty directly
+  return Object.prototype.hasOwnProperty.call(obj, prop);
+}
diff --git a/polygerrit-ui/app/utils/common-util_test.js b/polygerrit-ui/app/utils/common-util_test.js
new file mode 100644
index 0000000..60c0b0a
--- /dev/null
+++ b/polygerrit-ui/app/utils/common-util_test.js
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../test/common-test-setup-karma.js';
+import {hasOwnProperty} from './common-util.js';
+
+suite('common-util tests', () => {
+  suite('hasOwnProperty', () => {
+    test('object with the default prototype', () => {
+      const obj = {
+        'abc': 3,
+        'name with spaces': 5,
+      };
+      assert.isTrue(hasOwnProperty(obj, 'abc'));
+      assert.isTrue(hasOwnProperty(obj, 'name with spaces'));
+      assert.isFalse(hasOwnProperty(obj, 'def'));
+    });
+    test('object prototype has overriden hasOwnProperty', () => {
+      const F = function() {
+        this.abc = 23;
+      };
+      F.prototype.hasOwnProperty = function(key) {
+        return true;
+      };
+      const obj = new F();
+      assert.isTrue(hasOwnProperty(obj, 'abc'));
+      assert.isFalse(hasOwnProperty(obj, 'def'));
+    });
+  });
+});
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 8fb3eea..45e0ea7 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -2,6 +2,13 @@
 # yarn lockfile v1
 
 
+"@polymer/decorators@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@polymer/decorators/-/decorators-3.0.0.tgz#e4212ac976d9abd1210f560b6e1be4165c1c0183"
+  integrity sha512-qh+VID9nDV9q3ABvIfWgm7/+udl7v2HKsMLPXFm8tj1fI7qr7yWJMFwS3xWBkMmuNPtmkS8MDP0vqLAQIEOWzg==
+  dependencies:
+    "@polymer/polymer" "^3.0.5"
+
 "@polymer/font-roboto-local@^3.0.2":
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/@polymer/font-roboto-local/-/font-roboto-local-3.0.2.tgz#563cd6cabbcaef54999d654c0f3d476bcc49ce58"
@@ -13,9 +20,9 @@
   integrity sha512-tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA==
 
 "@polymer/iron-a11y-announcer@^3.0.0-pre.26":
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.0.2.tgz#730dd36ccb2e042ecd5160ba439c2bf2f8a97412"
-  integrity sha512-LqnMF39mXyxSSRbTHRzGbcJS8nU0NVTo2raBOgOlpxw5yfGJUVcwaTJ/qy5NtWCZLRfa4suycf0oAkuUjHTXHQ==
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.1.0.tgz#3d3712a165070ed3cdfc39e54f95515c913c9613"
+  integrity sha512-lc5i4NKB8kSQHH0Hwu8WS3ym93m+J69OHJWSSBxwd17FI+h2wmgxDzeG9LI4ojMMck17/uc2pLe7g/UHt5/K/A==
   dependencies:
     "@polymer/polymer" "^3.0.0"
 
@@ -26,10 +33,10 @@
   dependencies:
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/iron-autogrow-textarea@^3.0.0-pre.26", "@polymer/iron-autogrow-textarea@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.1.tgz#0205d9c5ca16f3afd505f41e9037989707d59dce"
-  integrity sha512-FgSL7APrOSL9Vu812sBCFlQ17hvnJsBAV2C2e1UAiaHhB+dyfLq8gGdGUpqVWuGJ50q4Y/49QwCNnLf85AdVYA==
+"@polymer/iron-autogrow-textarea@^3.0.0-pre.26", "@polymer/iron-autogrow-textarea@^3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.3.tgz#b75dbebc23ce47d428a26156709d4a8a4c05823e"
+  integrity sha512-5r0VkWrIlm0JIp5E5wlnvkw7slK72lFRZXncmrsLZF+6n1dg2rI8jt7xpFzSmUWrqpcyXwyKaGaDvUjl3j4JLA==
   dependencies:
     "@polymer/iron-behaviors" "^3.0.0-pre.26"
     "@polymer/iron-flex-layout" "^3.0.0-pre.26"
@@ -63,7 +70,7 @@
     "@polymer/neon-animation" "^3.0.0-pre.26"
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/iron-fit-behavior@^3.0.0-pre.26", "@polymer/iron-fit-behavior@^3.0.1":
+"@polymer/iron-fit-behavior@^3.0.0-pre.26", "@polymer/iron-fit-behavior@^3.0.2":
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/@polymer/iron-fit-behavior/-/iron-fit-behavior-3.0.2.tgz#2ec460d8a6b0151394b55631a72a68b92e14e2e0"
   integrity sha512-JndryJYbBR3gSN5IlST4rCHsd01+OyvYpRO6z5Zd3C6u5V/m07TwAtcf3aXwZ8WBNt2eLG28OcvdSO7XR2v2pg==
@@ -127,7 +134,7 @@
   dependencies:
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/iron-overlay-behavior@^3.0.0-pre.27", "@polymer/iron-overlay-behavior@^3.0.2":
+"@polymer/iron-overlay-behavior@^3.0.0-pre.27", "@polymer/iron-overlay-behavior@^3.0.3":
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/@polymer/iron-overlay-behavior/-/iron-overlay-behavior-3.0.3.tgz#29c198e19e05bb2bcf7d86d3c11848cb93301d00"
   integrity sha512-Q/Fp0+uOQQ145ebZ7T8Cxl4m1tUKYjyymkjcL2rXUm+aDQGb1wA1M1LYxUF5YBqd+9lipE0PTIiYwA2ZL/sznA==
@@ -227,10 +234,10 @@
     "@polymer/paper-styles" "^3.0.0-pre.26"
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/paper-input@^3.0.2":
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-input/-/paper-input-3.2.0.tgz#a07dbc1b009bac97a5a86eccb57d99b17bd96285"
-  integrity sha512-vYEBxq6LDR+QGDrAO/il0JNhCd+31TwSnv58MVV+ijaGKz1qAuSJw4NSsgF3lrXCwomqnpME19vbp2ktrcluVA==
+"@polymer/paper-input@^3.2.1":
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/@polymer/paper-input/-/paper-input-3.2.1.tgz#0fd0d30de3b43ba7d2c8d5d76f870d257b667ebf"
+  integrity sha512-6ghgwQKM6mS0hAQxQqj+tkeEY1VUBqAsrasAm8V5RpNcfSWQC/hhRFxU0beGuKTAhndzezDzWYP6Zz4b8fExGg==
   dependencies:
     "@polymer/iron-a11y-keys-behavior" "^3.0.0-pre.26"
     "@polymer/iron-autogrow-textarea" "^3.0.0-pre.26"
@@ -303,7 +310,14 @@
     "@polymer/paper-styles" "^3.0.0-pre.26"
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/polymer@^3.0.0", "@polymer/polymer@^3.0.2", "@polymer/polymer@^3.3.0":
+"@polymer/polymer@^3.0.0", "@polymer/polymer@^3.0.5", "@polymer/polymer@^3.4.1":
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.4.1.tgz#333bef25711f8411bb5624fb3eba8212ef8bee96"
+  integrity sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==
+  dependencies:
+    "@webcomponents/shadycss" "^1.9.1"
+
+"@polymer/polymer@^3.0.2":
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.3.1.tgz#9ad48992d2a96775f80b0673f3a615d6df8a3dfc"
   integrity sha512-8KaB48tzyMjdsHdxo5KvCAaqmTe7rYDzQAoj/pyEfq9Fp4YfUaS+/xqwYj0GbiDAUNzwkmEQ7dw9cgnRNdKO8A==
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index d03dada..5c9b300 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -168,17 +168,20 @@
 		writer.Header().Set("Content-Type", "text/html")
 	} else if isJsFile {
 	  // The following code updates import statements.
-	  // 1. Keep all imports started with '.' character unchanged (i.e. all relative
-	  // imports like import ... from './a.js' or import ... from '../b/c/d.js'
-	  // 2. For other imports it adds '/node_modules/' prefix. Additionally,
-	  //   if an in imported file has .js or .mjs extension, the code keeps
-	  //   the file extension unchanged. Otherwise, it adds .js extension.
+	  // 1. if an in imported file has .js or .mjs extension, the code keeps
+    //	  the file extension unchanged. Otherwise, it adds .js extension
+	  // 2. For module imports it adds '/node_modules/' prefix.
 	  //   Examples:
 	  //   '@polymer/polymer.js' -> '/node_modules/@polymer/polymer.js'
     //   'page/page.mjs' -> '/node_modules/page.mjs'
     //   '@polymer/iron-icon' -> '/node_modules/@polymer/iron-icon.js'
-		moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*?)(\\.(m?)js)?';$")
-		data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2.${4}js';"))
+    //   './element/file' -> './element/file.js'
+    moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'(.*?)(\\.(m?)js)?';$")
+    data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '$2.${4}js';"))
+
+		moduleImportRegexp = regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
+		data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2';"))
+
 		writer.Header().Set("Content-Type", "application/javascript")
 	} else if strings.HasSuffix(normalizedContentPath, ".css") {
 		writer.Header().Set("Content-Type", "text/css")